SpringBoot实现反向代理的示例代码

 更新时间:2023年06月13日 15:16:04   作者:RikyLee  
本文主要介绍了SpringBoot实现反向代理的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

最近收到一个新的需求,需要根据自定义的负载均衡策略从动态主机池选主之后,再通过反向代理到选中的主机上,这里面就涉及到服务注册、负载均衡策略、反向代理。本篇文章只涉及到如何实现反向代理功能。

功能实现

如果只是需要反向代理功能,那么有很多中间件可以选择,比如: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 反向代理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring Boot如何实现定时任务的动态增删启停详解

    Spring Boot如何实现定时任务的动态增删启停详解

    这篇文章主要给大家介绍了关于Spring Boot如何实现定时任务的动态增删启停的相关资料,文中通过示例代码以及图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2020-07-07
  • 关于Jackson的JSON工具类封装 JsonUtils用法

    关于Jackson的JSON工具类封装 JsonUtils用法

    这篇文章主要介绍了关于Jackson的JSON工具类封装 JsonUtils用法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • Java 8 Lambda 表达式比较器使用示例代码

    Java 8 Lambda 表达式比较器使用示例代码

    这篇文章主要介绍了Java 8 Lambda 表达式比较器使用示例代码,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-08-08
  • java启动jar包修改JVM默认内存问题

    java启动jar包修改JVM默认内存问题

    这篇文章主要介绍了java启动jar包修改JVM默认内存问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • 如何从eureka获取服务的ip和端口号进行Http的调用

    如何从eureka获取服务的ip和端口号进行Http的调用

    这篇文章主要介绍了如何从eureka获取服务的ip和端口号进行Http的调用,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • 使用maven一步一步构建spring mvc项目(图文详解)

    使用maven一步一步构建spring mvc项目(图文详解)

    这篇文章主要介绍了详解使用maven一步一步构建spring mvc项目,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-09-09
  • Spring的RestTemplata使用的具体方法

    Spring的RestTemplata使用的具体方法

    本篇文章主要介绍了Spring的RestTemplata使用的具体方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-01-01
  • Spring和Hibernate的整合操作示例

    Spring和Hibernate的整合操作示例

    这篇文章主要介绍了Spring和Hibernate的整合操作,结合实例形式详细分析了Spring和Hibernate的整合具体步骤、实现方法及相关操作注意事项,需要的朋友可以参考下
    2020-01-01
  • Restful API中的错误处理方法

    Restful API中的错误处理方法

    这篇文章主要给大家介绍了关于Restful API中错误处理方法的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-08-08
  • 深入IDEA Debug问题透析详解

    深入IDEA Debug问题透析详解

    这篇文章主要为大家介绍了深入IDEA Debug问题透析详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01

最新评论