SpringBoot实现简单的API代理服务器
背景
SpringBoot+K8S应用,没有SpringCloud, 想实现一个类似SpringCloudGateway的一个东西,可以把前端的请求路由到不同的后端服务上去,后端服务可能有多个,比如用户服务、订单服务,每一个服务都有自己的单独的访问域名。
思路
提供一个API代理服务,前端访问的时候,添加统一的前缀比如/api进入代理服务的ApiController,ApiController需要识别出来这个请求要访问的是哪一个后端服务,在SpringCloud中我们可以借助Nacos来做,现在只能手动做配置,把服务名追加到url中。
当ApiController收到请求的时候,首先从url中解析出来服务名,然后根据服务名查询配置文件找到实际的后端要访问的服务地址,拼接上url中剩余部分作为实际要转发的url,转发请求,返回响应。
举个例子:
前端实际要访问的后端地址是:http://www.userservice.com/use/1
,
前端发起的请求地址需要改成:http://www.api-proxy.com/api/userservice/use/1
在API代理服务中添加配置:
api:service:userservice: http://www.userservice.comorderservice: http://www.orderservice.com
这里url中的/api用于定位到后端的ApiController, userservice用于定位实际要访问的后端服务。
当API代理服务收到http://www.api-proxy.com/api/userservice/use/1
请求的时候,首先从url中解析出userservice,根据配置得到前缀是http://www.userservice.com
,然后拼接剩余部分/use/1
得到真正要访问的url是http://www.userservice.com/use/1
。
当然,如果可以配置多个后端服务地址然后可以实现自己的负载均衡,这样就更像一个网关了。
核心代码
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping(ApiController.API_CONTROLLER_PREFIX)
public class ApiController implements UserDetailAware {public static final String API_CONTROLLER_PREFIX = "/api";private static final String HEADER_USER_ID = "";private final RestTemplate restTemplate;private final ProxyConfig proxyConfig;@RequestMapping(value = "/{serviceName}/**")public ResponseEntity proxy(@PathVariable("serviceName") String serviceName, HttpServletRequest request, HttpServletResponse response) throws Exception{try {String serviceUrlPrefix = proxyConfig.getServices().get(serviceName);if (StringUtils.isEmpty(serviceUrlPrefix)) {return new ResponseEntity("No API Service Found!", HttpStatus.NOT_FOUND);}String url = buildUlr(serviceUrlPrefix, request);RequestEntity requestEntity = buildRequestEntity(url, request, response);ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class);return new ResponseEntity(responseEntity.getBody(), responseEntity.getStatusCode());} catch (Exception e) {log.error(ExceptionUtils.getStackTrace(e));return buildErrorResponseEntity(e);}}private ResponseEntity buildErrorResponseEntity(Exception e) throws Exception{if (e instanceof RestClientResponseException exception) {String errorBody = "Internal Server Error";byte[] errorBytes = exception.getResponseBodyAsByteArray();if (!ObjectUtils.isEmpty(errorBytes)) {errorBody = new String(errorBytes, StandardCharsets.UTF_8);}return new ResponseEntity(errorBody, exception.getStatusCode());} else {throw e;}}private RequestEntity buildRequestEntity(String url, HttpServletRequest request, HttpServletResponse response) throws IOException {HttpHeaders headers = buildHeader(request, response);if (isMultipart(request)) {return getMultipartEntity(url, request, headers);} else {byte[] body = StreamUtils.copyToByteArray(request.getInputStream());return new RequestEntity(body, headers, HttpMethod.valueOf(request.getMethod()), URI.create(url));}}private RequestEntity getMultipartEntity(String url, HttpServletRequest request, HttpHeaders headers) throws IOException {MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request;MultiValueMap<String, MultipartFile> multiFileMap = multipartHttpServletRequest.getMultiFileMap();// filesMultiValueMap formData = new LinkedMultiValueMap();for (Map.Entry<String, List<MultipartFile>> entry : multiFileMap.entrySet()) {String key = entry.getKey();for (MultipartFile multipartFile : entry.getValue()) {ByteArrayResource fileResource = new ByteArrayResource(multipartFile.getBytes()) {@Overridepublic long contentLength() {return multipartFile.getSize();}@Overridepublic String getFilename() {return multipartFile.getOriginalFilename();}};formData.add(key, fileResource);}}//non-filesMap<String, String[]> parameterMap = multipartHttpServletRequest.getParameterMap();for (Map.Entry<String, String[]> stringEntry : parameterMap.entrySet()) {String key = stringEntry.getKey();for (String value : stringEntry.getValue()) {formData.add(key, value);}}headers.setContentType(MediaType.MULTIPART_FORM_DATA);return new RequestEntity(formData, headers, HttpMethod.valueOf(request.getMethod()), URI.create(url));}private boolean isMultipart(HttpServletRequest request) {return request instanceof MultipartHttpServletRequest;}private HttpHeaders buildHeader(HttpServletRequest request, HttpServletResponse response) {HttpHeaders headers = new HttpHeaders();UserDetailsImpl userDetails = getUserDetails();Integer userId= userDetails==null?null:userDetails.getUserId();headers.add(HEADER_USER_ID , String.valueOf(userId));Iterator<String> iterator = request.getHeaderNames().asIterator();while (iterator.hasNext()) {String headerName = iterator.next();List<String> headerValues = Collections.list(request.getHeaders(headerName));for (String headerValue : headerValues) {headers.add(headerName, headerValue);}}return headers;}private String buildUlr(String serviceUrlPrefix, HttpServletRequest request) {String srcURI = request.getRequestURI();String dstURI = srcURI.substring(srcURI.indexOf(API_CONTROLLER_PREFIX) + API_CONTROLLER_PREFIX.length());dstURI = dstURI.substring(dstURI.indexOf("/", 1));return UriComponentsBuilder.fromUriString(serviceUrlPrefix).path(dstURI).query(request.getQueryString()).build().toUriString();}
}
参考文档
- https://blog.csdn.net/u013887008/article/details/128060629