likes
comments
collection
share

基于Spring Boot 实现简单的反向代理功能

作者站长头像
站长
· 阅读数 15

我正在参加「掘金·启航计划」

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

功能实现

如果只是需要反向代理功能,那么有很多中间件可以选择,比如: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"
}