Spring Cloud Gateway 通过过滤器动态改变路由规则
背景
公司需要做版本控制,但是又没有时间做服务改造,同时部署两个版本服务,但是对外提供一套域名,需要前面加一层网关来负载。
用图表示的话大概是下面这样子:
关于网关,看了一下基于java语言实现的大概有Zuul还有Spring Cloud Gateway,最后感觉后者网上帖子多,所以
果断开始抄gateway的代码。(简单看了下gateway是zuul的升级版,并且gateway支持长连接,我们项目中使用了websocket,考虑到日后,所以开始摸索spring cloud gateway)。
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对象进行覆盖。