Spring Cloud Gateway编码实现地址跳转

Spring Cloud Gateway编码实现地址跳转一般路由规则

  • 先来看一个普通的路由规则,如下所示,意思是将所有/hello/**的请求转发到http://127.0.0.1:8082这个地址去:
<span style="color:#393939"><span style="background-color:#faf7ef"><code class="language-yaml language-yml"><span style="color:#ff0000">spring:</span>
  <span style="color:#ff0000">application:</span>
    <span style="color:#ff0000">name:</span> <span style="color:#a31515">hello-gateway</span>
  <span style="color:#ff0000">cloud:</span>
    <span style="color:#ff0000">gateway:</span>
      <span style="color:#ff0000">routes:</span>
        <span style="color:#00b0e8">-</span> <span style="color:#ff0000">id:</span> <span style="color:#a31515">path_route</span>
          <span style="color:#ff0000">uri:</span> <span style="color:#a31515">http://127.0.0.1:8082</span>
          <span style="color:#ff0000">predicates:</span>
          <span style="color:#00b0e8">-</span> <span style="color:#a31515">Path=/hello/**</span>
</code></span></span>
  • 上述规则的功能如下图所示,假设这就是生产环境的样子,192.168.50.99:8082是提供服务的后台应用:

特殊规则

  • 以上是常规情况,但也有些特殊情况,要求SpringCloud Gateway把浏览器的请求转发到不同的服务上去
  • 如下图所示,在之前的环境中增加了另一个服务(即蓝色块),假设蓝色服务代表测试环境

  • 浏览器发起的/hello/str请求中,如果header中带有tag-test-user,并且值等于true,此时要求SpringCloud Gateway把这个请求转发到测试环境
  • 如果浏览器的请求header中没有tag-test-user,SpringCloud Gateway需要像以前那样继续转发到192.168.50.99:8082
  • 很明显,上述需求难以通过配置来实现,因为转发的地址和转发逻辑都是围绕业务逻辑来定制的,这也就是本篇的目标:对同一个请求path,可以通过编码转发到不同的地方去
  • 实现上述功能的具体做法是:自定义过滤器

设计

  • 编码之前先设计,把关键点想清楚再动手
  • 今天咱们要开发一个SpringCloud Gateway应用,里面新增一个自定义过滤器
  • 实现这个功能需要三个知识点作为基础,也就是说,您会通过本篇实战掌握以下知识点:
  1. 自定义过滤器
  2. 自定义过滤器的配置参数和bean的映射
  3. 编码构造Route实例
  • 用思维导图将具体工作内容展开,如下图所示,咱们就按部就班的实现吧:

源码下载

  • 这个git项目中有多个文件夹,本篇的源码在spring-cloud-tutorials文件夹下,如下图红框所示:

    - spring-cloud-tutorials内部有多个子项目,本篇的源码在gateway-dynamic-route文件夹下,如下图红框所示:

编码

  • 新建名为gateway-dynamic-route的maven工程,其pom.xml内容如下:
<span style="color:#393939"><span style="background-color:#faf7ef"><code class="language-xml"><span style="color:#2b91af"><?xml version="1.0" encoding="UTF-8"?></span>
<span style="color:#0000ff"><<span style="color:#0000ff">project</span> <span style="color:#ff0000">xmlns</span>=<span style="color:#a31515">"http://maven.apache.org/POM/4.0.0"</span>
         <span style="color:#ff0000">xmlns:xsi</span>=<span style="color:#a31515">"http://www.w3.org/2001/XMLSchema-instance"</span>
         <span style="color:#ff0000">xsi:schemaLocation</span>=<span style="color:#a31515">"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</span>></span>
    <span style="color:#0000ff"><<span style="color:#0000ff">parent</span>></span>
        <span style="color:#0000ff"><<span style="color:#0000ff">artifactId</span>></span>spring-cloud-tutorials<span style="color:#0000ff"></<span style="color:#0000ff">artifactId</span>></span>
        <span style="color:#0000ff"><<span style="color:#0000ff">groupId</span>></span>com.bolingcavalry<span style="color:#0000ff"></<span style="color:#0000ff">groupId</span>></span>
        <span style="color:#0000ff"><<span style="color:#0000ff">version</span>></span>1.0-SNAPSHOT<span style="color:#0000ff"></<span style="color:#0000ff">version</span>></span>
    <span style="color:#0000ff"></<span style="color:#0000ff">parent</span>></span>
    <span style="color:#0000ff"><<span style="color:#0000ff">modelVersion</span>></span>4.0.0<span style="color:#0000ff"></<span style="color:#0000ff">modelVersion</span>></span>

    <span style="color:#0000ff"><<span style="color:#0000ff">artifactId</span>></span>gateway-dynamic-route<span style="color:#0000ff"></<span style="color:#0000ff">artifactId</span>></span>

    <span style="color:#0000ff"><<span style="color:#0000ff">dependencies</span>></span>
        <span style="color:#0000ff"><<span style="color:#0000ff">dependency</span>></span>
            <span style="color:#0000ff"><<span style="color:#0000ff">groupId</span>></span>com.bolingcavalry<span style="color:#0000ff"></<span style="color:#0000ff">groupId</span>></span>
            <span style="color:#0000ff"><<span style="color:#0000ff">artifactId</span>></span>common<span style="color:#0000ff"></<span style="color:#0000ff">artifactId</span>></span>
            <span style="color:#0000ff"><<span style="color:#0000ff">version</span>></span>${project.version}<span style="color:#0000ff"></<span style="color:#0000ff">version</span>></span>
        <span style="color:#0000ff"></<span style="color:#0000ff">dependency</span>></span>
        <span style="color:#0000ff"><<span style="color:#0000ff">dependency</span>></span>
            <span style="color:#0000ff"><<span style="color:#0000ff">groupId</span>></span>org.springframework.cloud<span style="color:#0000ff"></<span style="color:#0000ff">groupId</span>></span>
            <span style="color:#0000ff"><<span style="color:#0000ff">artifactId</span>></span>spring-cloud-starter-gateway<span style="color:#0000ff"></<span style="color:#0000ff">artifactId</span>></span>
        <span style="color:#0000ff"></<span style="color:#0000ff">dependency</span>></span>
    <span style="color:#0000ff"></<span style="color:#0000ff">dependencies</span>></span>
    <span style="color:#0000ff"><<span style="color:#0000ff">build</span>></span>
        <span style="color:#0000ff"><<span style="color:#0000ff">plugins</span>></span>
            <span style="color:#008000"><!-- 如果父工程不是springboot,就要用以下方式使用插件,才能生成正常的jar --></span>
            <span style="color:#0000ff"><<span style="color:#0000ff">plugin</span>></span>
                <span style="color:#0000ff"><<span style="color:#0000ff">groupId</span>></span>org.springframework.boot<span style="color:#0000ff"></<span style="color:#0000ff">groupId</span>></span>
                <span style="color:#0000ff"><<span style="color:#0000ff">artifactId</span>></span>spring-boot-maven-plugin<span style="color:#0000ff"></<span style="color:#0000ff">artifactId</span>></span>
                <span style="color:#0000ff"><<span style="color:#0000ff">configuration</span>></span>
                    <span style="color:#0000ff"><<span style="color:#0000ff">mainClass</span>></span>com.bolingcavalry.gateway.GatewayDynamicRouteApplication<span style="color:#0000ff"></<span style="color:#0000ff">mainClass</span>></span>
                <span style="color:#0000ff"></<span style="color:#0000ff">configuration</span>></span>
                <span style="color:#0000ff"><<span style="color:#0000ff">executions</span>></span>
                    <span style="color:#0000ff"><<span style="color:#0000ff">execution</span>></span>
                        <span style="color:#0000ff"><<span style="color:#0000ff">goals</span>></span>
                            <span style="color:#0000ff"><<span style="color:#0000ff">goal</span>></span>repackage<span style="color:#0000ff"></<span style="color:#0000ff">goal</span>></span>
                        <span style="color:#0000ff"></<span style="color:#0000ff">goals</span>></span>
                    <span style="color:#0000ff"></<span style="color:#0000ff">execution</span>></span>
                <span style="color:#0000ff"></<span style="color:#0000ff">executions</span>></span>
            <span style="color:#0000ff"></<span style="color:#0000ff">plugin</span>></span>
        <span style="color:#0000ff"></<span style="color:#0000ff">plugins</span>></span>
    <span style="color:#0000ff"></<span style="color:#0000ff">build</span>></span>
<span style="color:#0000ff"></<span style="color:#0000ff">project</span>></span>
</code></span></span>
  • 启动类是普通的SpringBoot启动类:
<span style="color:#393939"><span style="background-color:#faf7ef"><code class="language-java"><span style="color:#0000ff">package</span> com.bolingcavalry.gateway;

<span style="color:#0000ff">import</span> org.springframework.boot.SpringApplication;
<span style="color:#0000ff">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;

<span style="color:#2b91af">@SpringBootApplication</span>
<span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> <span style="color:#a31515">GatewayDynamicRouteApplication</span> {
    <span style="color:#0000ff">public</span> <span style="color:#0000ff">static</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">main</span>(String[] args) {
        SpringApplication.run(GatewayDynamicRouteApplication.class,args);
    }
}
</code></span></span>
  • 接下来是本篇的核心,自定义过滤器类,代码中已经添加了详细的注释,有几处要注意的地方稍后会提到:
<span style="color:#393939"><span style="background-color:#faf7ef"><code class="language-java"><span style="color:#0000ff">package</span> com.bolingcavalry.gateway.filter;

<span style="color:#0000ff">import</span> lombok.Data;
<span style="color:#0000ff">import</span> lombok.ToString;
<span style="color:#0000ff">import</span> lombok.extern.slf4j.Slf4j;
<span style="color:#0000ff">import</span> org.springframework.cloud.gateway.filter.GatewayFilter;
<span style="color:#0000ff">import</span> org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
<span style="color:#0000ff">import</span> org.springframework.cloud.gateway.route.Route;
<span style="color:#0000ff">import</span> org.springframework.http.HttpHeaders;
<span style="color:#0000ff">import</span> org.springframework.http.HttpMethod;
<span style="color:#0000ff">import</span> org.springframework.http.server.reactive.ServerHttpRequest;
<span style="color:#0000ff">import</span> org.springframework.stereotype.Component;
<span style="color:#0000ff">import</span> org.springframework.util.MultiValueMap;
<span style="color:#0000ff">import</span> org.springframework.web.util.UriComponentsBuilder;
<span style="color:#0000ff">import</span> java.net.URI;
<span style="color:#0000ff">import</span> <span style="color:#0000ff">static</span> org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

<span style="color:#2b91af">@Component</span>
<span style="color:#2b91af">@Slf4j</span>
<span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> <span style="color:#a31515">BizLogicRouteGatewayFilterFactory</span> <span style="color:#0000ff">extends</span> <span style="color:#a31515">AbstractGatewayFilterFactory</span><BizLogicRouteGatewayFilterFactory.BizLogicRouteConfig> {

    <span style="color:#0000ff">private</span> <span style="color:#0000ff">static</span> <span style="color:#0000ff">final</span> <span style="color:#a31515">String</span> <span style="color:#008000">TAG_TEST_USER</span> <span style="color:#ab5656">=</span> <span style="color:#a31515">"tag-test-user"</span>;

    <span style="color:#0000ff">public</span> <span style="color:#a31515">BizLogicRouteGatewayFilterFactory</span>() {
        <span style="color:#0000ff">super</span>(BizLogicRouteConfig.class);
    }

    <span style="color:#2b91af">@Override</span>
    <span style="color:#0000ff">public</span> GatewayFilter <span style="color:#a31515">apply</span>(BizLogicRouteConfig config) {

        <span style="color:#0000ff">return</span> (exchange, chain) -> {
            <span style="color:#008000">// 本次的请求对象</span>
            <span style="color:#a31515">ServerHttpRequest</span> <span style="color:#008000">request</span> <span style="color:#ab5656">=</span>  exchange.getRequest();

            <span style="color:#008000">// 调用方请求时的path</span>
            <span style="color:#a31515">String</span> <span style="color:#008000">rawPath</span> <span style="color:#ab5656">=</span> request.getURI().getRawPath();

            log.info(<span style="color:#a31515">"rawPath [{}]"</span>, rawPath);

            <span style="color:#008000">// 请求头</span>
            <span style="color:#a31515">HttpHeaders</span> <span style="color:#008000">headers</span> <span style="color:#ab5656">=</span> request.getHeaders();

            <span style="color:#008000">// 请求方法</span>
            <span style="color:#a31515">HttpMethod</span> <span style="color:#008000">httpMethod</span> <span style="color:#ab5656">=</span> request.getMethod();

            <span style="color:#008000">// 请求参数</span>
            MultiValueMap<String, String> queryParams = request.getQueryParams();

            <span style="color:#008000">// 这就是定制的业务逻辑,isTestUser等于ture代表当前请求来自测试用户,需要被转发到测试环境</span>
            <span style="color:#a31515">boolean</span> <span style="color:#008000">isTestUser</span> <span style="color:#ab5656">=</span> <span style="color:#a31515">false</span>;

            <span style="color:#008000">// 如果header中有tag-test-user这个key,并且值等于true(不区分大小写),</span>
            <span style="color:#008000">// 就认为当前请求是测试用户发来的</span>
            <span style="color:#0000ff">if</span> (headers.containsKey(TAG_TEST_USER)) {
                <span style="color:#a31515">String</span> <span style="color:#008000">tageTestUser</span> <span style="color:#ab5656">=</span> headers.get(TAG_TEST_USER).get(<span style="color:#880000">0</span>);

                <span style="color:#0000ff">if</span> (<span style="color:#a31515">"true"</span>.equalsIgnoreCase(tageTestUser)) {
                    isTestUser = <span style="color:#a31515">true</span>;
                }
            }

            URI uri;

            <span style="color:#0000ff">if</span> (isTestUser) {
                log.info(<span style="color:#a31515">"这是测试用户的请求"</span>);
                <span style="color:#008000">// 从配置文件中得到测试环境的uri</span>
                uri = UriComponentsBuilder.fromHttpUrl(config.getTestEnvUri() + rawPath).queryParams(queryParams).build().toUri();
            } <span style="color:#0000ff">else</span> {
                log.info(<span style="color:#a31515">"这是普通用户的请求"</span>);
                <span style="color:#008000">// 从配置文件中得到正式环境的uri</span>
                uri = UriComponentsBuilder.fromHttpUrl(config.getProdEnvUri() + rawPath).queryParams(queryParams).build().toUri();
            }

            <span style="color:#008000">// 生成新的Request对象,该对象放弃了常规路由配置中的spring.cloud.gateway.routes.uri字段</span>
            <span style="color:#a31515">ServerHttpRequest</span> <span style="color:#008000">serverHttpRequest</span> <span style="color:#ab5656">=</span> request.mutate().uri(uri).method(httpMethod).headers(httpHeaders -> httpHeaders = httpHeaders).build();

            <span style="color:#008000">// 取出当前的route对象</span>
            <span style="color:#a31515">Route</span> <span style="color:#008000">route</span> <span style="color:#ab5656">=</span> exchange.getAttribute(GATEWAY_ROUTE_ATTR);
            <span style="color:#008000">//从新设置Route地址</span>
            <span style="color:#a31515">Route</span> <span style="color:#008000">newRoute</span> <span style="color:#ab5656">=</span>
                    Route.async().asyncPredicate(route.getPredicate()).filters(route.getFilters()).id(route.getId())
                            .order(route.getOrder()).uri(uri).build();
            <span style="color:#008000">// 放回exchange中</span>
            exchange.getAttributes().put(GATEWAY_ROUTE_ATTR,newRoute);

            <span style="color:#008000">// 链式处理,交给下一个过滤器</span>
            <span style="color:#0000ff">return</span> chain.filter(exchange.mutate().request(serverHttpRequest).build());
        };
    }

    <span style="color:#008000">/**
     * 这是过滤器的配置类,配置信息会保存在此处
     */</span>
    <span style="color:#2b91af">@Data</span>
    <span style="color:#2b91af">@ToString</span>
    <span style="color:#0000ff">public</span> <span style="color:#0000ff">static</span> <span style="color:#0000ff">class</span> <span style="color:#a31515">BizLogicRouteConfig</span> {
        <span style="color:#008000">// 生产环境的服务地址</span>
        <span style="color:#0000ff">private</span> String prodEnvUri;

        <span style="color:#008000">// 测试环境的服务地址</span>
        <span style="color:#0000ff">private</span> String testEnvUri;
    }
}
</code></span></span>
  • 上述代码中要注意的地方如下:
  1. BizLogicRouteConfig是过滤器的配置类,可以在使用过滤器时在配置文件中配置prodEnvUri和testEnvUri的值,在代码中可以通过这两个字段取得配置值
  2. 过滤器的工厂类名为BizLogicRouteGatewayFilterFactory,按照规则,过滤器的名字是BizLogicRoute
  3. 在apply方法中,重新创建ServerHttpRequest和Route对象,它们的参数可以按照业务需求随意设置,然后再将这两个对象设置给SpringCloud gateway的处理链中,接下来,处理链上的其他过滤拿到的就是新的ServerHttpRequest和Route对象了

配置

  • 假设生产环境地址是http://127.0.0.1:8082,测试环境地址是http://127.0.0.1:8087,整个SpringCloud Gateway应用的配置文件如下,可见使用了刚刚创建的过滤器,并且为此过滤器配置了两个参数:
<span style="color:#393939"><span style="background-color:#faf7ef"><code class="language-yaml language-yml"><span style="color:#ff0000">server:</span>
  <span style="color:#008000">#服务端口</span>
  <span style="color:#ff0000">port:</span> <span style="color:#880000">8086</span>
<span style="color:#ff0000">spring:</span>
  <span style="color:#ff0000">application:</span>
    <span style="color:#ff0000">name:</span> <span style="color:#a31515">gateway-dynamic-route</span>
  <span style="color:#ff0000">cloud:</span>
    <span style="color:#ff0000">gateway:</span>
      <span style="color:#ff0000">routes:</span>
        <span style="color:#00b0e8">-</span> <span style="color:#ff0000">id:</span> <span style="color:#a31515">path_route</span>
          <span style="color:#ff0000">uri:</span> <span style="color:#a31515">http://0.0.0.0:8082</span>
          <span style="color:#ff0000">predicates:</span>
          <span style="color:#00b0e8">-</span> <span style="color:#a31515">Path=/hello/**</span>
          <span style="color:#ff0000">filters:</span>
            <span style="color:#00b0e8">-</span> <span style="color:#ff0000">name:</span> <span style="color:#a31515">BizLogicRoute</span>
              <span style="color:#ff0000">args:</span>
                <span style="color:#ff0000">prodEnvUri:</span> <span style="color:#a31515">http://127.0.0.1:8082</span>
                <span style="color:#ff0000">testEnvUri:</span> <span style="color:#a31515">http://127.0.0.1:8087</span>
</code></span></span>
  • 至此,编码完成了,启动这个服务

开发和启动后台服务,模拟生产和测试环境

  • 接下来开始验证功能是否生效,咱们要准备两个后台服务:
  1. 模拟生产环境的后台服务是provider-hello,监听端口是8082,其/hello/str接口的返回值是Hello World, 2021-12-12 10:53:09
  2. 模拟测试环境的后台服务是provider-for-test-user,监听端口是8087,其/hello/str接口的返回值是Hello World, 2021-12-12 10:57:11 (from test enviroment)(和生产环境相比,返回内容多了个(from test enviroment)),对应Controller参考如下:
<span style="color:#393939"><span style="background-color:#faf7ef"><code class="language-java"><span style="color:#0000ff">package</span> com.bolingcavalry.provider.controller;

<span style="color:#0000ff">import</span> com.bolingcavalry.common.Constants;
<span style="color:#0000ff">import</span> org.springframework.web.bind.annotation.*;
<span style="color:#0000ff">import</span> java.text.SimpleDateFormat;
<span style="color:#0000ff">import</span> java.util.Date;
<span style="color:#0000ff">import</span> java.util.Map;

<span style="color:#2b91af">@RestController</span>
<span style="color:#2b91af">@RequestMapping("/hello")</span>
<span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> <span style="color:#a31515">Hello</span> {

    <span style="color:#0000ff">private</span> String <span style="color:#a31515">dateStr</span>(){
        <span style="color:#0000ff">return</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">SimpleDateFormat</span>(<span style="color:#a31515">"yyyy-MM-dd hh:mm:ss"</span>).format(<span style="color:#0000ff">new</span> <span style="color:#a31515">Date</span>());
    }

    <span style="color:#008000">/**
     * 返回字符串类型
     * <span style="color:#808080">@return</span>
     */</span>
    <span style="color:#2b91af">@GetMapping("/str")</span>
    <span style="color:#0000ff">public</span> String <span style="color:#a31515">helloStr</span>() {
        <span style="color:#0000ff">return</span> Constants.HELLO_PREFIX + <span style="color:#a31515">", "</span> + dateStr() + <span style="color:#a31515">" (from test enviroment)"</span>;
    }
}
</code></span></span>
  • 以上两个服务,对应的代码都在我的Github仓库中,如下图红框所示:

  • 启动gateway-dynamic-routeprovider-helloprovider-for-test-user服务
  • 此时,SpringCloud gateway应用和两个后台服务都启动完成,情况如下图,接下来验证刚才开发的过滤器能不能像预期那样转发:

验证

  • 用postman工具向gateway-dynamic-route应用发起一次请求,返回值如下图红框所示,证明这是provider-hello的响应,看来咱们的请求已经正常到达:

  • 再发送一次请求,如下图,这次在header中加入键值对,得到的结果是provider-for-test-user的响应

  • 至此,过滤器的开发和验证已经完成,通过编码,可以把外部请求转发到任何咱们需要的地址去,并且支持参数配置,这个过滤器还有一定的可配置下,减少了硬编码的比率,如果您正在琢磨如何深度操控SpringCloud Gateway,希望本文能给您一些参考;

猜你喜欢

转载自blog.csdn.net/yetaodiao/article/details/131415662