Spring Cloud Gateway 通过过滤器动态改变路由规则

背景

公司需要做版本控制,但是又没有时间做服务改造,同时部署两个版本服务,但是对外提供一套域名,需要前面加一层网关来负载。
用图表示的话大概是下面这样子:


图1-1


关于网关,看了一下基于java语言实现的大概有Zuul还有Spring Cloud Gateway,最后感觉后者网上帖子多,所以
果断开始抄gateway的代码。(简单看了下gateway是zuul的升级版,并且gateway支持长连接,我们项目中使用了websocket,考虑到日后,所以开始摸索spring cloud gateway)。
图1-2

step1 创建项目(略过)
step2 配置文件一览
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.jw</groupId>
    <artifactId>gateway-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gateway-demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>
step3 日志开启debug模式

在sr/main/resources文件夹中新建logback.xml,然后复制下面的配置到文件中:

<?xml version="1.0" encoding="UTF-8" ?>

<configuration scan="true" scanPeriod="3 seconds">
    <!--设置日志输出为控制台-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%logger{32}] %msg%n</pattern>
        </encoder>
    </appender>
    <!--设置日志输出为文件-->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>logFile.log</File>
        <rollingPolicy  class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>logFile.%d{yyyy-MM-dd_HH-mm}.log.zip</FileNamePattern>
        </rollingPolicy>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>%d{HH:mm:ss,SSS} [%thread] %-5level %logger{32} - %msg%n</Pattern>
        </layout>
    </appender>
    <root>
        <level value="DEBUG"/>
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>
step4 通过日志探查源码
[OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@536b71b4}, order=-2147482648}, 
OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@67f63d26}, order=-1}, 
OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@6f5d0190}, order=0}, 
OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory$$Lambda$397/0x00000008403a9040@22046592, order=0}, 
OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@789c3057}, order=10000}, 
OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@67332b1e}, order=2147483646}, GatewayFilterAdapter{delegate=com.jw.gatewaydemo.MyFilter$$Lambda$399/0x00000008403a9840@da4cf09}, 
OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@1980a3f}, order=2147483647}, 
OrderedGatewayFilter{delegate=GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@39941489}, order=2147483647}]

通过日志可以看出经过的类,方便研究源码

step5 自定义路由定位器RouteLocator
@Configuration
public class GatewayRoutes {

    @Bean
    public RouteLocator getRouteLocator(RouteLocatorBuilder builder) {

        String url2 = "http://localhost:19102";
        RouteLocator build = builder.routes()
                .route(r ->
                        //这里指定了get方法
                        r.method(HttpMethod.GET).and()
                        .path("/**").uri(url2)
                )
                .build();
        return build;
    }

    @Bean
    public RouteLocator postRouteLocator(RouteLocatorBuilder builder) {

        String url2 = "http://localhost:19102";
        RouteLocator build = builder.routes()
                .route(r ->
                        //这里指定了post方法
                        r.method(HttpMethod.POST)
                                //readBody方法获取ReadBodyPredicateFactory对象,会将requestBody缓存在exchange对象中
                                .and().readBody(Object.class, requestBody -> {
                                    System.out.println(String.format("requestBody is %s", requestBody));
                                    // 这里不对body做判断处理
                                    return true;
                                })
                                //所有请求都会经过该路由
                                .and().path("/**")
                                //像请求头中增加version  这里可以换成traceId、startTime等其他有用信息
                                .filters(f -> f.addRequestHeader("version", "HASS2.0"))
                                .uri(url2)
                )
                .build();
        return build;
    }
}

  

ps:
ReadBodyPredicateFactory对象中缓存了requestBody,通过查看源码可以发现
在这里插入图片描述

step6 自定义全局过滤器
@Component
public class MyFilter  {

    @Bean
    @Order(-1)
    public GlobalFilter preFilter() {
        return (exchange, chain) -> {
            ServerHttpRequest req = exchange.getRequest();
            //ReadBodyPredicateFactory 对象在路由定位器中已经缓存了请求参数,这里直接取就可以
            Map<String, String> cachedRequestBodyObject = (Map<String, String>) exchange.getAttribute("cachedRequestBodyObject");
            //随便写了个规则来改变路由地址
            String roomNo = cachedRequestBodyObject.get("roomNo");
            if(Objects.equals(roomNo,"101")){
                //获取域名后的path
                String rawPath = req.getURI().getRawPath();
                URI uri = UriComponentsBuilder.fromHttpUrl("http://localhost:19101" + rawPath).build().toUri();
                //重新封装request对象
                ServerHttpRequest request = req.mutate().uri(uri).build();
                Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
                boolean encoded = containsEncodedParts(uri);
                URI routeUri = route.getUri();
                URI mergedUrl = UriComponentsBuilder.fromUri(uri)
                        .scheme(routeUri.getScheme())
                        .host(routeUri.getHost())
                        .port("19101")
                        .build(encoded)
                        .toUri();
                //NettyRoutingFilter 最终会从GATEWAY_REQUEST_URL_ATTR 取出uri对象进行http请求,所以这里需要将新的对象覆盖进去
                exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR,mergedUrl);
                return chain.filter(exchange.mutate().request(request).build()).then(Mono.fromRunnable(() -> {
                    //请求完成回调方法 可以再此完成计算请求耗时等操作
                }));
            }else{
                //模拟调用B服务 B服务无法掉通
                System.out.println("模拟调用B服务 B服务无法调通");
                return chain.filter(exchange);
            }
        };
    }
}
  

ps:
通过查看源码可以看出最后调用服务是在NettyRoutingFilter中完成的,而使用的URI对象缓存在这里在这里插入图片描述
因此在自定义filter中需要将已经更改的URI对象进行覆盖。
在这里插入图片描述

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐