SpringBoot实现反向代理的示例代码
作者:RikyLee
最近收到一个新的需求,需要根据自定义的负载均衡策略从动态主机池选主之后,再通过反向代理到选中的主机上,这里面就涉及到服务注册、负载均衡策略、反向代理。本篇文章只涉及到如何实现反向代理功能。
功能实现
如果只是需要反向代理功能,那么有很多中间件可以选择,比如: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
参数说明:
- txt复制代码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()); } } java复制代码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" }
到此这篇关于SpringBoot实现反向代理的示例代码的文章就介绍到这了,更多相关SpringBoot 反向代理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!