目录
- 1、基本的对象
- 1.1 配置类
- 1.2 实体DTO
- 1.3 路由代理拓展器
- 1.4 请求对象 RestTemplate
- 2、核心转发代码
- 3、暴露接口
- 4、基础配置
前言:想实现一个轻量级的接口反向代理和转发的一个接口服务,可以通过这个服务做一些需要认证才能访问的接口给到前端使用,这样就实现了一种认证可以调用多种第三方系统的服务。
基本逻辑就是将请求的请求方式、请求头、请求体提取出来,将这些信息转发到另外一个接口
假设当前接口在 bizbook-api
这个服务上可以实现 /bizbook-api/common/**
的接口转发到 http://192.168.50.43:7612/**
列子:访问:/bizbook-api/common/bizweb-api/sj/list
就相当于访问 /bizweb-api/sj/list
1、基本的对象
1.1 配置类
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;import java.util.List;/*** 路由代理配置*/
@Configuration
@ConfigurationProperties(prefix = "delegate.config.api", ignoreUnknownFields = false)
public class RouterDelegateProperties {/*** 网关地址*/String rootPath;/*** 服务名称, 服务名称和服务网关和服务拓展器一一对应, 服务网关和服务拓展器没有则用default代替*/List<String> serviceName;/*** 服务网关, 服务名称和服务网关和服务拓展器一一对应, 服务网关和服务拓展器没有则用default代替*/List<String> serviceRoot;/*** 服务拓展器, 服务名称和服务网关和服务拓展器一一对应, 服务网关和服务拓展器没有则用default代替*/List<String> serviceExtractor;public String getRootPath() {return rootPath;}public void setRootPath(String rootPath) {this.rootPath = rootPath;}public List<String> getServiceName() {return serviceName;}public void setServiceName(List<String> serviceName) {this.serviceName = serviceName;}public List<String> getServiceRoot() {return serviceRoot;}public void setServiceRoot(List<String> serviceRoot) {this.serviceRoot = serviceRoot;}public List<String> getServiceExtractor() {return serviceExtractor;}public void setServiceExtractor(List<String> serviceExtractor) {this.serviceExtractor = serviceExtractor;}
}
1.2 实体DTO
import java.io.Serializable;/*** 代理路由配置 DTO*/
public class RouterDelegateConfigDTO implements Serializable {private static final long serialVersionUID = 1L;/*** 网关地址*/private String rootPath;/*** 服务名称*/private String serviceName;/*** 服务名称地址*/private String serviceRoot;/*** 服务名称处理器*/private String serviceExtractor;public String getRootPath() {return rootPath;}public void setRootPath(String rootPath) {this.rootPath = rootPath;}public String getServiceName() {return serviceName;}public void setServiceName(String serviceName) {this.serviceName = serviceName;}public String getServiceRoot() {return serviceRoot;}public void setServiceRoot(String serviceRoot) {this.serviceRoot = serviceRoot;}public String getServiceExtractor() {return serviceExtractor;}public void setServiceExtractor(String serviceExtractor) {this.serviceExtractor = serviceExtractor;}
}
1.3 路由代理拓展器
import org.springframework.http.HttpHeaders;
import zsoft.gov.datacenter.biztable.common.dto.router.RouterDelegateConfigDTO;import javax.servlet.http.HttpServletRequest;/*** 路由代理拓展器*/
public interface RouterDelegateExtractor {/*** 处理请求url, 返回null则使用通用处理逻辑** @param request 请求体对象* @param configDTO 服务配置对象* @param prefix 代理前缀* @return*/String getRequestRootUrl(HttpServletRequest request, RouterDelegateConfigDTO configDTO, String prefix);/*** 处理请求头** @param request 请求体对象* @param headers 请求头*/void parseRequestHeader(HttpServletRequest request, HttpHeaders headers);/*** 处理请求体, 返回null则使用通用处理逻辑** @param request 请求体对象* @return*/byte[] parseRequestBody(HttpServletRequest request);}
1.4 请求对象 RestTemplate
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;@Configuration
public class FetchApiRestTemplateConfig {@Bean({"fetchApiRestTemplate"})@Autowiredpublic RestTemplate restTemplate(@Qualifier("fetchApiClientHttpRequestFactory") ClientHttpRequestFactory factory) {RestTemplate restTemplate = new RestTemplate(factory);restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {@Overridepublic void handleError(ClientHttpResponse response) throws IOException {
// if (response.getRawStatusCode() != 401 && response.getRawStatusCode() != 404) {
// super.handleError(response);
// }// 处理返回 4xx 的状态码时不抛出异常if (!response.getStatusCode().is4xxClientError()) {super.handleError(response);}}});// 中文乱码问题List<HttpMessageConverter<?>> httpMessageConverters = restTemplate.getMessageConverters();httpMessageConverters.stream().forEach(httpMessageConverter -> {if (httpMessageConverter instanceof StringHttpMessageConverter) {StringHttpMessageConverter messageConverter = (StringHttpMessageConverter) httpMessageConverter;messageConverter.setDefaultCharset(Charset.forName("UTF-8"));}});return restTemplate;}@Bean({"fetchApiClientHttpRequestFactory"})public ClientHttpRequestFactory simpleClientHttpRequestFactory() {SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();factory.setReadTimeout(1000 * 50); // 读取超时(毫秒)factory.setConnectTimeout(1000 * 10); // 连接超时(毫秒)return factory;}}
2、核心转发代码
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestTemplate;
import zsoft.gov.datacenter.biztable.common.config.RouterDelegateProperties;
import zsoft.gov.datacenter.biztable.common.dto.router.RouterDelegateConfigDTO;
import zsoft.gov.datacenter.biztable.common.response.Result;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** 路由代理*/
@Service
public class RouterDelegate implements ApplicationRunner {protected Logger logger = LoggerFactory.getLogger(getClass());private static Map<String, RouterDelegateConfigDTO> configMap;@Resource@Qualifier("fetchApiRestTemplate")private RestTemplate restTemplate;@Resourceprivate RouterDelegateProperties properties;@Resourceprivate Map<String, RouterDelegateExtractor> stringRouterDelegateExtractorMap;/*** 初始化配置类** @param args* @throws Exception*/@Overridepublic void run(ApplicationArguments args) throws Exception {boolean intiFlag = false;logger.info(">>> -----开始初始化路由代理配置类!");/*** 最终configMap效果* {* 服务名称: {* rootPath: "系统网关地址",* serviceName: "服务名称",* serviceRoot: "服务网关",* serviceExtractor: "服务拓展器",* }* }*/String rootPath = properties.getRootPath();List<String> serviceName = properties.getServiceName();List<String> serviceRoot = properties.getServiceRoot();List<String> serviceExtractor = properties.getServiceExtractor();// 服务名称, 服务名称和服务网关和服务处理器一一对应, 如果没有对应的服务网关和服务处理器, 则用英文逗号隔开if (StringUtils.isNotBlank(rootPath)&& CollectionUtils.isNotEmpty(serviceName)&& CollectionUtils.isNotEmpty(serviceExtractor)&& CollectionUtils.isNotEmpty(serviceRoot)&& serviceName.size() == serviceRoot.size()&& serviceName.size() == serviceExtractor.size()) {intiFlag = true;// 初始化大小避免扩容int initialCapacity = (int) (serviceName.size() / 0.75) + 1;configMap = new ConcurrentHashMap<>(initialCapacity);for (int i = 0; i < serviceName.size(); i++) {RouterDelegateConfigDTO dto = new RouterDelegateConfigDTO();String serName = serviceName.get(i);dto.setRootPath(rootPath);dto.setServiceName(serName);// default 是占位符, 配置成default相当于没有配置dto.setServiceRoot("default".equals(serviceRoot.get(i)) ? null : serviceRoot.get(i));dto.setServiceExtractor("default".equals(serviceExtractor.get(i)) ? null : serviceExtractor.get(i));configMap.put(serName, dto);}}if (intiFlag) logger.info(">>> 初始化路由代理配置类成功!");else logger.error(">>> 初始化路由代理配置类失败!");}public ResponseEntity<byte[]> redirect(HttpServletRequest request, HttpServletResponse response, String prefix, String serviceName) {String requestURI = request.getRequestURI();RouterDelegateConfigDTO currentConfig = getCurrentServiceConfig(serviceName);if (currentConfig == null) {return buildErrorResponseEntity("SERVICE ERROR! 服务不存在!", HttpStatus.NOT_FOUND);}RouterDelegateExtractor extractorCallBack = getRouterDelegateExtractor(serviceName);try {// 创建urlString redirectUrl = createRequestUrl(request, currentConfig, prefix, extractorCallBack);logger.info(">>> redirectUrl代理后的完整地址: [{}]", redirectUrl);RequestEntity requestEntity = createRequestEntity(request, redirectUrl, extractorCallBack);// return route(request, redirectUrl, extractorCallBack);ResponseEntity<byte[]> result = route(requestEntity);if (result.getHeaders() != null && result.getHeaders().containsKey(HttpHeaders.TRANSFER_ENCODING)) {// 移除响应头 Transfer-Encoding, 因为高版本的nginx会自动添加该响应头, 多个响应值nginx会报错// 多个响应值nginx报错: *6889957 upstream sent duplicate header line: "Transfer-Encoding: chunked", previous value: "Transfer-Encoding: chunked" while reading response header from upstreamHttpHeaders headers = HttpHeaders.writableHttpHeaders(result.getHeaders());headers.remove(HttpHeaders.TRANSFER_ENCODING);}//logger.info(">>> [{}] 代理成功, 请求耗时: [{}]", requestURI, System.currentTimeMillis() - l1);return result;} catch (Exception e) {logger.error("REDIRECT ERROR", e);//logger.error(">>> [{}] 代理失败, 请求耗时: [{}]", requestURI, System.currentTimeMillis() - l1);return buildErrorResponseEntity("REDIRECT ERROR! " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);}}private ResponseEntity buildErrorResponseEntity(String msg, HttpStatus httpStatus) {HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);Result body = Result.build(httpStatus.value(), msg);return new ResponseEntity(body, headers, httpStatus);}/*** 获取当前服务配置** @param serviceName* @return*/public RouterDelegateConfigDTO getCurrentServiceConfig(String serviceName) {if (configMap == null || !configMap.containsKey(serviceName)) {return null;}return configMap.get(serviceName);}/*** 获取当前路由服务拓展器** @param serviceName* @return*/private RouterDelegateExtractor getRouterDelegateExtractor(String serviceName) {RouterDelegateConfigDTO currentConfig = getCurrentServiceConfig(serviceName);if (currentConfig == null) {return null;}String serviceExtractor = currentConfig.getServiceExtractor();if (StringUtils.isBlank(serviceExtractor)) {return null;}RouterDelegateExtractor extractor = stringRouterDelegateExtractorMap.get(serviceExtractor + "RouterDelegateExtractor");return extractor;}/*** 创建请求地址** @param request* @param configDTO* @param prefix* @param extractorCallback* @return*/private String createRequestUrl(HttpServletRequest request, RouterDelegateConfigDTO configDTO, String prefix, RouterDelegateExtractor extractorCallback) {String routeUrl = configDTO.getRootPath();// 拓展器不为null, 并且有返回结果才使用if (extractorCallback != null) {String hostUrl = extractorCallback.getRequestRootUrl(request, configDTO, prefix);if (hostUrl != null) routeUrl = hostUrl;}String queryString = request.getQueryString();
// return routeUrl + request.getRequestURI().replace(prefix, "") +
// (queryString != null ? "?" + queryString : "");// request.getRequestURI() 包括 server.servlet.context-path// request.getServletPath() 不包括 server.servlet.context-path// http://127.0.0.1/databook-api/graphdb/sj/tianda/openapi/v1/applets?name=ts// request.getRequestURI() = /databook-api/graphdb/sj/tianda/openapi/v1/applets// request.getServletPath() = /graphdb/sj/tianda/openapi/v1/appletsString serviceName = configDTO.getServiceName();return routeUrl + request.getServletPath().replaceFirst(prefix + "/" + serviceName, "") +(queryString != null ? "?" + queryString : "");}private RequestEntity createRequestEntity(HttpServletRequest request, String url, RouterDelegateExtractor extractorCallBack) throws URISyntaxException, IOException {String method = request.getMethod();HttpMethod httpMethod = HttpMethod.resolve(method);HttpHeaders headers = parseRequestHeader(request, extractorCallBack);byte[] body = parseRequestBody(request, extractorCallBack);return new RequestEntity<>(body, headers, httpMethod, new URI(url));}private ResponseEntity<byte[]> route(HttpServletRequest request, String url, RouterDelegateExtractor extractorCallBack) throws IOException, URISyntaxException {String method = request.getMethod();HttpMethod httpMethod = HttpMethod.resolve(method);HttpHeaders headers = parseRequestHeader(request, extractorCallBack);byte[] body = parseRequestBody(request, extractorCallBack);// 设置请求实体HttpEntity<byte[]> httpEntity = new HttpEntity<>(body, headers);URI uri = new URI(url);return restTemplate.exchange(uri, httpMethod, httpEntity, byte[].class);}private ResponseEntity<byte[]> route(RequestEntity requestEntity) {return restTemplate.exchange(requestEntity, byte[].class);}/*** 处理请求头** @param request* @param extractorCallBack* @return*/private HttpHeaders parseRequestHeader(HttpServletRequest request, RouterDelegateExtractor extractorCallBack) {List<String> headerNames = Collections.list(request.getHeaderNames());HttpHeaders headers = new HttpHeaders();for (String headerName : headerNames) {List<String> headerValues = Collections.list(request.getHeaders(headerName));for (String headerValue : headerValues) {headers.add(headerName, headerValue);}}if (extractorCallBack != null) {extractorCallBack.parseRequestHeader(request, headers);}// 移除请求头accept-encoding, 不移除会导致响应体转成String时会乱码headers.remove("accept-encoding");return headers;}/*** 处理请求体** @param request* @param extractorCallBack* @return* @throws IOException*/private byte[] parseRequestBody(HttpServletRequest request, RouterDelegateExtractor extractorCallBack) throws IOException {// 拓展器不为null, 并且返回的结果也不为null才使用返回结果, 否则使用通用处理逻辑if (extractorCallBack != null) {byte[] body = extractorCallBack.parseRequestBody(request);if (body != null) return body;}InputStream inputStream = request.getInputStream();return StreamUtils.copyToByteArray(inputStream);}}
3、暴露接口
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import zsoft.gov.datacenter.biztable.common.router.RouterDelegate;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** 路由代理接口*/
@RestController
@RequestMapping
public class RouterDelegateController {public final static String DELEGATE_PREFIX = "/delegate";@Autowiredprivate RouterDelegate routerDelegate;/*** 路由代理接口** @param serviceName* @param request* @param response* @return*/@RequestMapping(value = DELEGATE_PREFIX + "/{serviceName}/**", method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE})public ResponseEntity redirect(@PathVariable("serviceName") String serviceName, HttpServletRequest request, HttpServletResponse response) {return routerDelegate.redirect(request, response, DELEGATE_PREFIX, serviceName);}}
4、基础配置
#路由代理配置-网关地址
delegate.config.api.rootPath=http://192.168.50.43:7612
#路由代理配置-服务名称, 服务名称和服务网关和服务拓展器一一对应, 服务网关和服务拓展器没有则用default代替
delegate.config.api.serviceName=common,csdn
#路由代理配置-服务网关, 服务名称和服务网关和服务拓展器一一对应, 服务网关和服务拓展器没有则用default代替
delegate.config.api.serviceRoot=default,https://csdn.net
#路由代理配置-服务拓展器, 服务名称和服务网关和服务拓展器一一对应, 服务网关和服务拓展器没有则用default代替
delegate.config.api.serviceExtractor=default,csdnBlog