基于Spring Boot 实现简单的反向代理功能
最近收到一个新的需求,需要根据自定义的负载均衡策略从动态主机池选主之后,再通过反向代理到选中的主机上,这里面就涉及到服务注册、负载均衡策略、反向代理。本篇文章只涉及到如何实现反向代理功能。
功能实现
如果只是需要反向代理功能,那么有很多中间件可以选择,比如:Nginx、Kong、Spring Cloud Gateway,Zuul等都可以实现,但是还有一些客制化的需求,所以只能自己撸代码实现了,附上源码。
请求拦截
实现请求拦截有两种方式,过滤器和拦截器,我们采用过滤器的方式去实现请求拦截。
在Spring 体系中最常用到的过滤器应该就是OncePerRequestFilter,这是一个抽象类。我们创建一个类叫ForwardRoutingFilter去继承这个类,同时实现Ordered,用于设置过滤器的优先级
@Slf4j
@Component
public class ForwardRoutingFilter extends OncePerRequestFilter implements Ordered {
@Override
public int getOrder() {
return 0; // 值越小,优先级别越高
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("ForwardRoutingFilter doFilterInternal,request uri: {}", request.getRequestURI());
filterChain.doFilter(request, response);
}
}
启动服务之后,浏览器中输入http://127.0.0.1:8080/aa
,查看console 中的日志,可以看到过滤器以及开始工作了。
2023-06-12T14:25:09.059+08:00 INFO 17472 --- [nio-8080-exec-2] c.r.b.filter.ForwardRoutingFilter : ForwardRoutingFilter doFilterInternal,request uri: /aa
2023-06-12T14:25:09.735+08:00 INFO 17472 --- [nio-8080-exec-1] c.r.b.filter.ForwardRoutingFilter : ForwardRoutingFilter doFilterInternal,request uri: /favicon.ico
接下来,我们的实现就围绕着这个过滤器去做了。
配置规则定义
通常情况下,我们会在application.yml
去配置哪些path需要被转发到具体的服务上去,例如
my:
routes:
- uri: lb://ai-server
path: /ai/**
rewrite: false
- uri: https://api.oioweb.cn
path: /oioweb/**
rewrite: true
参数说明:
uri: 最终请求的服务地址,如果是lb:// 开头的,说明需要进行负责均衡
path: 用于匹配代理的路径,命中的会被进行代理转发
rewrite: 是否重写path,如果true, 访问 http://127.0.0.1:8080/uomg/api/rand.img1 请求path中/uomg会被删除,最终访问的是 https://api.uomg.com/api/rand.img1
在pom.xml
dependencies 中添加新的依赖,用于自动装填配置
<!--读取文件配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
创建实体类RouteInstance
和配置类MyRoutes
,这样服务启动之后就会自动读取装填my.routes
下所有配置的实例到配置类了
@Data
public class RouteInstance {
private String uri;
private String path;
private boolean rewrite;
}
@Configuration
@ConfigurationProperties(prefix = "my", ignoreInvalidFields = true)
@Data
public class MyRoutes {
private List<RouteInstance> routes;
}
代理实现
在pom.xml
dependencies 中添加需要用到的依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
接下来就是改造我们之前的ForwardRoutingFilter
过滤器类了
@Slf4j
@Component
public class ForwardRoutingFilter extends OncePerRequestFilter implements Ordered {
@Resource
private MyRoutes routes;
@Resource
private RoutingDelegateService routingDelegate;
@Override
public int getOrder() {
return 0;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("ForwardRoutingFilter doFilterInternal,request uri: {}", request.getRequestURI());
String currentURL = StringUtils.isEmpty(request.getContextPath()) ? request.getRequestURI() :
StringUtils.substringAfter(request.getRequestURI(), request.getContextPath());
AntPathMatcher matcher = new AntPathMatcher();
RouteInstance instance = routes.getRoutes().stream().filter(i -> matcher.match(i.getPath(), currentURL)).findFirst().orElse(new RouteInstance());
if (instance.getUri() == null) {
//转发的uri为空,不进行代理转发,交由过滤器链后续过滤器处理
filterChain.doFilter(request, response);
} else {
// 创建一个service 去处理代理转发逻辑
routingDelegate.doForward(instance, request, response);
return;
}
}
}
代理转发会使用到RestTemplate
,默认使用的是java.net.URLConnection去进行http请求,我们这边替换成httpclient,具体配置就不贴出来了。
编写两个工具栏,分别用于转换 HttpServletRequest 为 RequestEntity 和 HttpServletResponse 为 ResponseEntity,并把结果写回客户端
@Slf4j
public class HttpRequestMapper {
public RequestEntity<byte[]> map(HttpServletRequest request, RouteInstance instance) throws IOException {
byte[] body = extractBody(request);
HttpHeaders headers = extractHeaders(request);
HttpMethod method = extractMethod(request);
URI uri = extractUri(request, instance);
return new RequestEntity<>(body, headers, method, uri);
}
private URI extractUri(HttpServletRequest request, RouteInstance instance) throws UnsupportedEncodingException {
//如果content path 不为空,移除content path
String requestURI = StringUtils.isEmpty(request.getContextPath()) ? request.getRequestURI() :
StringUtils.substringAfter(request.getRequestURI(), request.getContextPath());
//处理中文被自动编码问题
String query = request.getQueryString() == null ? EMPTY : URLDecoder.decode(request.getQueryString(), "utf-8");
// 需要重写path
if (instance.isRewrite()) {
String prefix = StringUtils.substringBefore(instance.getPath(), "/**");
requestURI = StringUtils.substringAfter(requestURI, prefix);
}
URI redirectURL = UriComponentsBuilder.fromUriString(instance.getUri() + requestURI).query(query).build().encode().toUri();
log.info("real request url: {}", redirectURL.toString());
return redirectURL;
}
private HttpMethod extractMethod(HttpServletRequest request) {
return valueOf(request.getMethod());
}
private HttpHeaders extractHeaders(HttpServletRequest request) {
HttpHeaders headers = new HttpHeaders();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
List<String> value = list(request.getHeaders(name));
headers.put(name, value);
}
return headers;
}
private byte[] extractBody(HttpServletRequest request) throws IOException {
return toByteArray(request.getInputStream());
}
}
public class HttpResponseMapper {
public void map(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) throws IOException {
setStatus(responseEntity, response);
setHeaders(responseEntity, response);
setBody(responseEntity, response);
}
private void setStatus(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) {
response.setStatus(responseEntity.getStatusCode().value());
}
private void setHeaders(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) {
responseEntity.getHeaders().forEach((name, values) -> values.forEach(value -> response.addHeader(name, value)));
}
/**
* 把结果写回客户端
*
* @param responseEntity
* @param response
* @throws IOException
*/
private void setBody(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) throws IOException {
if (responseEntity.getBody() != null) {
response.getOutputStream().write(responseEntity.getBody());
}
}
}
以下为实际处理逻辑RoutingDelegateService
的代码
@Slf4j
@Service
public class RoutingDelegateService {
private HttpResponseMapper responseMapper;
private HttpRequestMapper requestMapper;
@Resource
private RestTemplate restTemplate;
/**
* 根据相应策略转发请求到对应后端服务
*
* @param instance RouteInstance
* @param request HttpServletRequest
* @param response HttpServletResponse
*/
public void doForward(RouteInstance instance, HttpServletRequest request, HttpServletResponse response) {
boolean shouldLB = StringUtils.startsWith(instance.getUri(), MyConstants.LB_PREFIX);
if (shouldLB) {
// 需要负载均衡,获取appName
String appName = StringUtils.substringAfter(instance.getUri(), MyConstants.LB_PREFIX);
//从请求头中获取是否必须按user去路由到同一节点
// 可用节点
ServerInstance chooseInstance = chooseLBInstance(appName);
if (chooseInstance == null) {
// 无可用节点,返回异常,
JSONObject result = new JSONObject();
result.put("status", MyConstants.NO_AVAILABLE_NODE_STATUS);
result.put("msg", MyConstants.NO_AVAILABLE_NODE_MSG);
renderString(response, result.toJSONString());
return;
} else {
//设置route instance uri 为负载均衡之后的URI地址
String uri = MyConstants.HTTP_PREFIX + chooseInstance.getHost() + ":" + chooseInstance.getPort();
instance.setUri(uri);
}
}
// 转发请求
try {
goForward(request, response, instance);
} catch (Exception e) {
// 连接超时、返回异常
e.printStackTrace();
log.error("request error {}", e.getMessage());
JSONObject result = new JSONObject();
result.put("status", MyConstants.UNKNOWN_EXCEPTION_STATUS);
result.put("msg", e.getMessage());
renderString(response, result.toJSONString());
}
}
/**
* 发送请求到对应后端服务
*
* @param request HttpServletRequest
* @param response HttpServletResponse
* @param instance RouteInstance
* @throws IOException
*/
private void goForward(HttpServletRequest request, HttpServletResponse response, RouteInstance instance) throws IOException {
requestMapper = new HttpRequestMapper();
RequestEntity<byte[]> requestEntity = requestMapper.map(request, instance);
//用byte数组处理返回结果,因为返回结果可能是字符串也可能是数据流
ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class);
responseMapper = new HttpResponseMapper();
responseMapper.map(responseEntity, response);
}
private ServerInstance chooseLBInstance(String appName) {
//TODO 根据appName 选择对应的host
ServerInstance instance = new ServerInstance();
instance.setHost("127.0.0.1");
instance.setPort(10000);
return instance;
}
/**
* 写回字符串结果到客户端
*
* @param response
* @param string
*/
public void renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
}
}
启动server,浏览器中输入http://127.0.0.1:8080/oioweb/api/common/rubbish?name=香蕉
,就可以把请求代理到https://api.oioweb.cn/api/common/rubbish?name=香蕉
了
{
"code": 200,
"result": [
{
"name": "香蕉",
"type": 2,
"aipre": 0,
"explain": "厨余垃圾是指居民日常生活及食品加工、饮食服务、单位供餐等活动中产生的垃圾。",
"contain": "常见包括菜叶、剩菜、剩饭、果皮、蛋壳、茶渣、骨头等",
"tip": "纯流质的食物垃圾、如牛奶等,应直接倒进下水口。有包装物的湿垃圾应将包装物去除后分类投放、包装物请投放到对应的可回收物或干垃圾容器"
},
{
"name": "香蕉干",
"type": 2,
"aipre": 0,
"explain": "厨余垃圾是指居民日常生活及食品加工、饮食服务、单位供餐等活动中产生的垃圾。",
"contain": "常见包括菜叶、剩菜、剩饭、果皮、蛋壳、茶渣、骨头等",
"tip": "纯流质的食物垃圾、如牛奶等,应直接倒进下水口。有包装物的湿垃圾应将包装物去除后分类投放、包装物请投放到对应的可回收物或干垃圾容器"
},
{
"name": "香蕉皮",
"type": 2,
"aipre": 0,
"explain": "厨余垃圾是指居民日常生活及食品加工、饮食服务、单位供餐等活动中产生的垃圾。",
"contain": "常见包括菜叶、剩菜、剩饭、果皮、蛋壳、茶渣、骨头等",
"tip": "纯流质的食物垃圾、如牛奶等,应直接倒进下水口。有包装物的湿垃圾应将包装物去除后分类投放、包装物请投放到对应的可回收物或干垃圾容器"
}
],
"msg": "success"
}
转载自:https://juejin.cn/post/7243725197911359547