一个简单的Http根据规则自动路由
在日常项目中,有时候会根据一些规则/标识进行Http路由匹配,下面我实现一个简单的Http根据systemCode自动路由;
- Controller
- Service
- Properties
- Properties类
- Yaml
- 测试
- 路由到baidu
Controller
import com.example.demo.service.GatewayRoutingService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/gateway-routing")
public class GatewayRoutingController {
private Logger log = LoggerFactory.getLogger(GatewayRoutingController.class);
@Autowired
private GatewayRoutingService service;
/**
* 根据systemCode,自行匹配路由到systemCode对应的域名上
*
* @param request
* @param response
*/
@RequestMapping("/invoke/**")
public void invoke(HttpServletRequest request, HttpServletResponse response) {
String systemCode = request.getHeader("system-code");
if (StringUtils.isEmpty(systemCode)) {
// return 404
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
try {
// 路由调用
service.invoke(systemCode, request, response);
} catch (Exception e) {
log.info("genericInvoke>>>exception", e);
throw new RuntimeException("系统错误,请联系管理");
}
}
}
Service
import com.example.demo.config.RequestInvokeProperties;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
@Service
public class GatewayRoutingService {
private Logger log = LoggerFactory.getLogger(GatewayRoutingService.class);
/**
* 系统域名映射 key :系统唯一标识, value 系统
*/
@Autowired
private RequestInvokeProperties properties;
/**
* 路由调用
*/
public void invoke(String systemCode, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 解析获取真实请求路径
final URI realUrl = this.resolveRealUrl(systemCode, request);
final HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
if (httpMethod == null || realUrl == null) {
throw new RuntimeException("当前请求不合法, 无法转发");
}
log.info("routing and forwarding>>>originalUrl = {}, realUrl = {}", request.getRequestURI(), realUrl);
ClientHttpRequest delegateRequest = new SimpleClientHttpRequestFactory().createRequest(realUrl, httpMethod);
// set header
setRequestHeader(request, delegateRequest);
// set body
setRequestBody(request, delegateRequest);
// http调用
try (ClientHttpResponse clientHttpResponse = delegateRequest.execute();
ServletOutputStream responseOutputStream = response.getOutputStream()) {
response.setStatus(clientHttpResponse.getStatusCode().value());
clientHttpResponse.getHeaders().forEach((key, values) -> {
// 处理响应头重复情况,在请求返回时该响应头会出现重复,postman 调用成功,但是实际前端调用会出错
if (!"Transfer-Encoding".equalsIgnoreCase(key)) {
values.forEach(value -> response.setHeader(key, value));
}
});
// 拷贝响应流
ioCopy(clientHttpResponse.getBody(), responseOutputStream, 2024);
}
}
/**
* 拷贝响应流
*
* @param in
* @param out
*/
private long ioCopy(InputStream in, OutputStream out, int bufferSize) throws IOException {
Assert.notNull(in, "InputStream is null !");
Assert.notNull(out, "OutputStream is null !");
long size = 0L;
try {
if (bufferSize <= 0) {
bufferSize = 2048;
}
byte[] buffer = new byte[bufferSize];
int readSize;
while ((readSize = in.read(buffer)) != -1) {
out.write(buffer, 0, readSize);
size += (long) readSize;
out.flush();
}
} finally {
in.close();
out.close();
}
return size;
}
/**
* 解析获取真实请求路径
*
* @param system
* @param request
* @return
*/
private URI resolveRealUrl(String system, HttpServletRequest request) throws URISyntaxException, UnsupportedEncodingException {
if (properties.getSystemMap() == null || properties.getSystemMap().isEmpty()) {
return null;
}
// 获取真实的请求url:去除前缀 /invoke/
StringBuilder requestUrl = new StringBuilder(StringUtils.substringAfter(request.getRequestURI(), "/invoke/"));
// 根据systemCode获取对应的域名
String domain = properties.getSystemMap().get(system);
if (StringUtils.isNoneBlank(requestUrl.toString(), domain)) {
final Enumeration<String> parameterNames = request.getParameterNames();
StringBuilder uriVariables = new StringBuilder("?");
while (parameterNames.hasMoreElements()) {
// 转义部分特殊字符,如果请求参数里带有 +、空格等不转义请求路由过去会出问题
final String parameterName = URLEncoder.encode(parameterNames.nextElement(), "UTF-8");
final String parameter = URLEncoder.encode(request.getParameter(parameterName), "UTF-8");
uriVariables.append(parameterName).append("=").append(parameter).append("&");
}
domain = domain.endsWith("/") ? domain : domain + "/";
return new URI(domain + requestUrl + (uriVariables.length() == 1 ? "" : uriVariables.substring(0, uriVariables.length() - 1)));
}
return null;
}
/**
* 设置请求体
*
* @throws IOException
*/
private void setRequestBody(HttpServletRequest request, ClientHttpRequest delegateRequest) throws IOException {
StreamUtils.copy(request.getInputStream(), delegateRequest.getBody());
}
/**
* 设置请求头
*/
private void setRequestHeader(HttpServletRequest request, ClientHttpRequest delegateRequest) {
Enumeration<String> headerNames = request.getHeaderNames();
// 设置请求头
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
Enumeration<String> header = request.getHeaders(headerName);
List<String> arr = new ArrayList<>();
while (header.hasMoreElements()) {
arr.add(header.nextElement());
}
delegateRequest.getHeaders().addAll(headerName, arr);
}
if (!delegateRequest.getHeaders().containsKey("Content-Type")) {
delegateRequest.getHeaders().add("Content-Type", "application/json; charset=utf-8");
}
}
}
Properties
Properties类
@Data
@Component
@ConfigurationProperties(prefix = "request.invoke")
public class RequestInvokeProperties {
private Map<String, String> systemMap;
}
Yaml
request:
invoke:
systemMap:
baidu: http://www.baidu.com/
测试
路由到baidu
请求:
curl --location --request GET 'http://localhost:8080/gateway-routing/invoke/s?wd=spring' \
--header 'system-code: baidu' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: localhost:8080' \
--header 'Connection: keep-alive' \
--data-raw ''
实际请求:
http://www.baidu.com/s?wd=spring