Spring Cloud Gateway自定义过滤器
编辑背景
最近项目需要切换网关,由zuul切换为spring cloud gateway,研究了部分spring cloud gateway能力,本文主要记录了spring cloud gateway如何自定义过滤器。
创建Spring Cloud Gateway工程
添加如下maven依赖引入Spring Cloud Gateway,这边我测试的工程使用的Spring Cloud以及Spring Boot版本分别为Hoxton.SR12和2.3.12.RELEASE
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--SpringCloud 和 SpringBoot版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR12</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
在工程中,我添加了一个路由,请求对于地址会被转发到百度。application.yml文件如下:
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: baidu
uri: https://www.baidu.com
predicates:
- Path=/baidu/**
filters:
- RedirectTo=302, https://www.baidu.com
开启debug日志模式,方便跟踪网关进程:
logging:
level:
org.springframework.cloud.gateway: DEBUG
reactor.netty.http.client: DEBUG
自定义Global Filters全局过滤器
全局过滤器,顾名思义,应用于全局,对于每一个经过网关的请求都会走到全局过滤器。全局过滤器可以做日志的监控以及鉴权等等功能。
前置全局过滤器
新增加一个过滤器,需要实现GlobalFilter接口,重写接口中的filter方法,并将其作为bean添加到Spring应用上下文中。
前置过滤器是在请求执行之前,比如在进行路由之前,我们可以添加对应的执行逻辑。
案例代码(代码中只记录了一行日志):
/**
* @author yuanzhihao
* @since 2022/4/22
*/
@Component
@Slf4j
public class PreGlobalCustomFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Global Pre Filter Execute...");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
后置全局过滤器
后置过滤器是在调用链执行完成之后,运行一个新的Mono实例,这边没有具体仔细烟酒,只是先了解了如何编写和使用。
之前在zuul中,pre和post过滤器是根据重写filterType方法,自定义返回的类型来区分前置还是后置过滤器的,这边和Spring Cloud Gateway有区别。
案例代码(这边还是记录一行日志):
/**
* @author yuanzhihao
* @since 2022/4/22
*/
@Component
@Slf4j
public class PostGlobalCustomFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange)
.then(Mono.fromRunnable(
() -> log.info("Global Post Filter Execute...")
));
}
@Override
public int getOrder() {
return 1;
}
}
启动网关服务,通过网关服务请求/baidu/**地址,可以看到具体打印的日志信息 我们可以将前置过滤器和后置过滤器组合在一个过滤器中,效果和上面是一样的:
/**
* @author yuanzhihao
* @since 2022/4/22
*/
@Component
@Slf4j
public class GlobalCustomFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Global Pre Filter Execute...");
return chain.filter(exchange)
.then(Mono.fromRunnable(
() -> log.info("Global Post Filter Execute...")
));
}
@Override
public int getOrder() {
return 1;
}
}
过滤器优先级
如果我们有多个自定义的过滤器,我们想让这些过滤器按照一定的顺序去执行,可以实现Ordered接口,重写getorder方法来定义过滤器执行的先后顺序,这边执行的逻辑是order的值越小,前置过滤器越先执行,后置过滤器越后执行。通过下面这张图可以更清晰的理解。这边大家可以自己编写几个过滤器进行验证,文章中就不具体举例了,可以参考我的代码库中的代码。
自定义GatewayFilters
上面的全局过滤器应用于全局,Spring Cloud Gateway也提供了另一种自定义过滤器,它的粒度更小,可以应用于我们需要指定的某些路由上。
编写过滤器
首先需要继承AbstractGatewayFilterFactory抽象类,它是一个泛型类,实现这个抽象类需要指定一个配置类,通过配置类中的定义来具体实现我们的过滤器。这边需要注意,编写的过滤器必须以"GatewayFilterFactory"为结尾。
/**
* @author yuanzhihao
* @since 2022/4/22
*/
@Slf4j
@Component
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomGatewayFilterFactory.Config> {
public CustomGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (config.isPreLogger()) {
log.info("CustomGatewayFilterFactory pre message is {}", config.getMessage());
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger()) {
log.info("CustomGatewayFilterFactory post message is {}", config.getMessage());
}
}));
}
};
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config {
private String message;
private boolean preLogger;
private boolean postLogger;
}
}
上面的示例代码中,Config配置类有三个属性:
- message指定需要打印的日志
- preLogger是一个boolean类型,用于判断是否在前置过滤器中打印日志
- postLogger判断是否在后置过滤器中打印日志
通过配置文件生效过滤器
在配置文件中新增如下配置生效自定义的过滤器,这边的名称就是自定义过滤器的前缀:
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: baidu
uri: https://www.baidu.com
predicates:
- Path=/baidu/**
filters:
- name: Custom
args:
message: My Custom Message
preLogger: true
postLogger: true
- RedirectTo=302, https://www.baidu.com
还有一种更简洁的定义方式:
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: baidu
uri: https://www.baidu.com
predicates:
- Path=/baidu/**
filters:
- Custom= My Custom Message, true, true
- RedirectTo=302, https://www.baidu.com
不过这边需要重写shortcutFieldOrder方法,该方法返回一个List列表,列表中指定参数使用的顺序和数量:
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("message", "preLogger", "postLogger");
}
启动网关服务,通过网关服务请求/baidu/**地址,可以看到具体打印的日志信息 如果我们想要指定过滤器的执行顺序,可以返回一个OrderedGatewayFilter实例,它提供了一个构造函数可以传入GatewayFilter以及order信息。
@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (config.isPreLogger()) {
log.info("CustomGatewayFilterFactory pre message is {}", config.getMessage());
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger()) {
log.info("CustomGatewayFilterFactory post message is {}", config.getMessage());
}
}));
}
}, -1);
}
通过编码的方式生效过滤器
除了在配置文件中指定,也可以通过编码的方式注入我们自定义的过滤器,代码中需要注入一个RouteLocator的bean(通过编码的方式没有配置文件方式清晰)
@Bean
public RouteLocator routes(RouteLocatorBuilder builder, CustomGatewayFilterFactory filterFactory) {
return builder
.routes()
.route("163", p -> p.path("/163/**")
.filters(f -> f
.filter(filterFactory
.apply(new CustomGatewayFilterFactory.Config("Base 163 mesage", true, false)))
.redirect(302, "https://www.163.com"))
.uri("http://localhost"))
.build();
}
其他应用场景
到目前为止,示例中只是添加了一行日志,在自定义过滤器里面我们可以做比如检查或者修改经过网关的请求,也可以修改具体响应。下面例举两个简单的应用场景。
修改请求
在前置过滤器中,我们可以获取当前的请求头,并对请求头进行业务处理,然后将信息透传到下面的调用链中,比如现在我在请求到达微服务之前,添加了一个header头。
首先在自定义过滤器中添加header头:
/**
* @author yuanzhihao
* @since 2022/4/22
*/
@Slf4j
@Component
public class ModifyRequestGatewayFilterFactory extends AbstractGatewayFilterFactory<ModifyRequestGatewayFilterFactory.Config> {
public ModifyRequestGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange
.getRequest()
.mutate()
.header(config.getName(), config.getValue())
.build();
log.info("Begin add header [{}]", config);
return chain.filter(exchange.mutate().request(request).build());
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("name", "value");
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config {
private String name;
private String value;
}
}
在配置文件中添加过滤器信息,这边是路由到eureka-client1服务时,会添加一个name为yzh1996的header头:
- id: client1
uri: lb://eureka-client1
predicates:
- Path=/client1/**
filters:
- ModifyRequest=name, yzh1996
client1中接口打印header头信息:
@GetMapping("/headerName")
public String headerName(HttpServletRequest request) {
String name = request.getHeader("name");
return "Header name is " + name;
}
浏览器请求对应接口,可以看到我们上游添加的header头已经透传到下游的微服务
修改响应
在后置过滤器里面,我们可以修改响应的请求,比如我们可以修改响应的响应码。
/**
* @author yuanzhihao
* @since 2022/4/22
*/
@Slf4j
@Component
public class ModifyResponseGatewayFilterFactory extends AbstractGatewayFilterFactory<ModifyResponseGatewayFilterFactory.Config> {
public ModifyResponseGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("Modify Response status code begin...");
exchange.getResponse().setRawStatusCode(Integer.parseInt(config.getStatusCode()));
}));
}
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("statusCode");
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config {
private String statusCode;
}
}
添加配置信息:
- id: client1
uri: lb://eureka-client1
predicates:
- Path=/client1/**
filters:
- ModifyRequest=name, yzh1996
- ModifyResponse=205
之后请求服务,可以看到服务的响应已经被设置为205
结语
关于自定义过滤器就整理到这边,文章中所有的代码都能在我的代码仓库找到,后续有时间还会继续整理相关其他用法。
参考链接:
https://docs.spring.io/spring-cloud-gateway/docs/3.0.4/reference/html/#developer-guide
https://www.baeldung.com/spring-cloud-custom-gateway-filters
代码地址:https://github.com/yzh19961031/SpringCloudDemo/tree/main/gateway