route-forward springboot实现路由转发程序
目录
前言:想实现一个轻量级的接口反向代理和转发的一个接口服务,可以通过这个服务做一些需要认证才能访问的接口给到前端使用,这样就实现了一种认证可以调用多种第三方系统的服务。
基本逻辑就是将请求的请求方式、请求头、请求体提取出来,将这些信息转发到另外一个接口
- 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"})
@Autowired
public RestTemplate restTemplate(@Qualifier("fetchApiClientHttpRequestFactory") ClientHttpRequestFactory factory) {
RestTemplate restTemplate = new RestTemplate(factory);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public 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;
@Resource
private RouterDelegateProperties properties;
@Resource
private Map<String, RouterDelegateExtractor> stringRouterDelegateExtractorMap;
/**
* 初始化配置类
*
* @param args
* @throws Exception
*/
@Override
public 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 {
// 创建url
String 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 upstream
HttpHeaders 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/applets
String 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";
@Autowired
private 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