微服务网关Spring Cloud Gateway之Filter

Spring Cloud Gateway 中,除断言外,Filter(过滤器)也是一个重要知识点。

提起过滤器,做过 Java 开发的读者应该不会陌生,Spring Cloud Gateway 网关中的过滤器也如此,可以在网关路由到具体的微服务请求之前或网关收到具体的微服务响应之后,为请求对象和响应对象添加一些自定义的编码。

在实际项目的开发过程中,使用过滤器的场景要比使用断言工厂的场景多一些。Spring Cloud Gateway 内置的断言工厂中最常用的其实是路径判断,即 PathRoutePredicateFactory。而过滤器的适用场景和功能比断言的适用场景和功能多一些,如路由规则匹配了,但是还需要对 Request 对象或 Response 对象做额外的定制操作,就可以把过滤器搬过来。与断言工厂不同的一点是,断言工厂只负责判断并返回一个布尔值,并没有额外的操作,而在过滤器中可以直接修改 Request 对象和 Response 对象,如果在进入过滤器后某些请求依然不符合规则,就可以直接自定义响应内容。

Spring Cloud Gateway 根据作用范围可将过滤器分为 GatewayFilterGlobalFilter,通俗一点理解就是局部过滤器和全局过滤器。其中,局部过滤器只对某一个路由配置生效,而全局过滤器作用于所有路由配置。Spring Cloud Gateway 网关中的这两类过滤器都支持开发人员自定义操作,除 Spring Cloud Gateway 内置的过滤器外,也可以自行添加过滤器来实现一些特殊的需求。

Spring Cloud Gateway的内置过滤器

内置过滤器列表及功能

Spring Cloud Gateway 组件中内置的局部过滤器都实现了 AbstractGatewayFilterFactory 抽象类,在 3.1.1 版本中共有 30 多个,部分内置的局部过滤器列表如图 9-12 所示。

image 2025 04 16 18 44 51 565
Figure 1. 图9-12 部分内置的局部过滤器列表

常见的内置过滤器介绍如下。

  • StripPrefixGatewayFilterFactory:该过滤器接收一个 parts 参数,parts 参数表示在将请求发送到微服务实例之前要从请求 URL 中剥离的路径数量。比如,发送的请求路径为 /newbee-ltd/manage/goods/save,如果配置参数为 1,则实际路由到微服务实例的 URL/manage/goods/save;如果配置参数为 2,则实际路由到微服务实例的 URL/goods/save。配置格式如下:

    - StripPrefix=2
  • SetStatusGatewayFilterFactory:该过滤器接收一个 status 参数,修改 Response 对象的 HTTP 状态码。status 参数必须是有效的状态码,可以用整数值 404 或枚举的字符串 NOT_FOUND 表示。配置格式如下:

    - SetStatus=404
  • AddRequestHeaderGatewayFilterFactory:该过滤器会给当前的 Request 对象添加一个 Header 参数及参数值。配置格式如下:

    - AddRequestHeader=os,HarmonyOS
  • AddRequestParameterGatewayFilterFactory:该过滤器会给当前的 Request 对象添加一个请求参数及参数值。配置格式如下:

    - AddRequestParameter=name,newbee-mall-cloud
  • AddResponseHeaderGatewayFilterFactory:该过滤器会给当前的 Response 对象添加一个 Header 参数及参数值。配置格式如下:

    - AddResponseHeader=token,newbee*
  • PrefixPathGatewayFilterFactory:该过滤器接收一个 Prefix 参数,Prefix 参数表示在将请求发送到微服务实例之前要在请求 URL 中添加的路径。配置格式如下:

    - PrefixPath=/newbee-cloud

比如,发送的请求路径为 /goods/save,那么实际路由到微服务实例的 URL/newbee-cloud/goods/save

  • PreserveHostGatewayFilterFactory:此过滤器无须配置参数值,主要用于确定是否发送原始主机头,而不是由 HTTP 客户端确定主机头。配置格式如下:

    - PreserveHost
  • RedirectToGatewayFilterFactory:该过滤器接收两个参数 statusurlstatus 参数应为 300 系列的 HTTP 状态码,如 301302url 参数应为有效 URL 地址。配置格式如下:

    - RedirectTo=302,https://juejin.cn

Spring Cloud Gateway 组件中内置的全局过滤器都实现了 GlobalFilter 接口,在 3.1.1 版本中共有十几个,列表如图 9-13 所示。

image 2025 04 16 18 52 18 767
Figure 2. 图9-13 内置的全局过滤器列表

全局过滤器功能非常全面,包括请求的基本处理、负载均衡功能、响应结果处理、监控等。感兴趣的读者可以阅读这些类的源码,逻辑并不复杂。代码量比较多的是 ReactiveLoadBalancerClientFilter 过滤器,其实看到这个类的名称,读者应该很熟悉,在前面章节中介绍过 LoadBalancerClient 这个类,它们非常相似,该过滤器的功能是结合服务发现机制进行负载均衡操作。

Spring Cloud Gateway 中内置的过滤器较多,功能也非常丰富,更多内容可以参考 Spring Cloud Gateway 的官方文档,见网址6。

使用内置的局部过滤器配置路由规则

Spring Cloud Gateway 内置过滤器介绍完毕,接下来笔者使用内置的 StripPrefixGatewayFilterFactoryRedirectToGatewayFilterFactory 编写一个示例演示它们的作用。本节代码是在 spring-cloud-alibaba-gateway-demo 项目的基础上修改的,具体步骤如下。

  1. 修改项目名称和基本配置。

    修改项目名称为 spring-cloud-alibaba-gateway-filter-demo,之后把各个模块中 pom.xml 文件的 artifactId 修改为 spring-cloud-alibaba-gateway-filter-demo。为了做章节区分,这里把 gateway-demo 项目中的端口号修改为 8147,并且在 gateway-demo 项目配置文件中添加注册中心的配置。

  2. 使用内置过滤器。

    Path 路径规则外,分别增加参数规则和请求方法的规则,代码如下:

    spring.cloud.gateway.routes[0].id=goods-service-route
    spring.cloud.gateway.routes[0].uri=lb://newbee-cloud-goods-service
    spring.cloud.gateway.routes[0].order=1
    spring.cloud.gateway.routes[0].predicates[0]=Path=/newbee-cloud/goods/**
    ## 访问/newbee-cloud/goods 开头的请求,都会被设置为/goods 开头的请求
    spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
    
    spring.cloud.gateway.routes[1].id=shopcart-service-route
    spring.cloud.gateway.routes[1].uri=lb://newbee-cloud-shopcart-service
    spring.cloud.gateway.routes[1].order=1
    spring.cloud.gateway.routes[1].predicates[0]=Path=/shop-cart/**
    ## 访问/shop-cart 开头的请求,都会被重定向到掘金官网
    spring.cloud.gateway.routes[1].filters[0]=RedirectTo=302,https://juejin.cn

    在原有路由配置的基础上,分别给 goods-service-route 路由和 shopcart-service-route 路由新增一条过滤器配置。访问 /newbee-cloud/goods 开头的请求,都会被设置为 /goods 开头的请求。访问 /shop-cart 开头的请求,都会被重定向到掘金官网。

    编码完成后进行功能验证,需要启动 Nacos Server,之后依次启动 gateway-demogoods-service-demoshopcart-service-demo 项目。如果未能成功启动,则开发人员需要查看控制台中的日志是否报错,并及时确认问题和修复。启动成功后进入 Nacos 控制台,单击 “服务管理” 中的服务列表,可以看到列表中已经存在三个服务的服务信息。

    依次使用不同的地址进行测试,结果见表 9-4。

    image 2025 04 16 18 56 39 012
    Figure 3. 表9-4 使用不同的地址进行测试的结果

    这里重点解释一下为什么第 1 条请求和第 2 条请求的结果不同。

    首先,网关项目的端口号为 8147,商品服务项目的端口号为 8140。商品详情接口是在商品服务中的,网关项目中并没有这个接口,如果想通过网关访问这个接口,就必须路由到商品服务。

    那么,访问 http://localhost:8147/goods?goodsId=2035 这个链接时,请求的是网关项目,而网关项目中的路由配置无法匹配 /goods 开头的这个链接。因为路由配置中到商品服务的 Path 断言为 Path=/newbee-cloud/goods/**,无法匹配,直接报错 404。

    其次,在访问 http://localhost:8147/newbee-cloud/goods?goodsId=2035 这个链接时,因为被网关中的路由配置匹配到了,所以会路由到商品服务。此时读者可能会有疑问,商品服务中并没有处理 /newbee-cloud/goods 路径的接口,为什么结果是正常的呢?因为增加了一个 StripPrefix=1 的配置,在请求路由到商品服务之前,这个请求地址已经由 /newbee-cloud/goods?goodsId=2035 变成了 /goods?goodsId=2035,所以能够获取正确的接口数据,即过滤器生效。

还有其他内置的局部过滤器可供开发人员使用,因篇幅有限,就不再一一举例了,读者可以自行对照前文中介绍的内容进行编码和测试。

自定义网关过滤器

自定义局部过滤器编码实践

与自定义断言工厂一样,Spring Cloud Gateway 同样支持开发人员自定义过滤器。前文中介绍的内置断言工厂都实现了 AbstractGatewayFilterFactory 抽象类,命名方式为 xxxGatewayFilterFactory,在配置文件中写上 -xxx 即可。根据这几个固定的写法,可以自行实现一个局部过滤器类。与前文中自定义断言工厂一样,这里实现一个局部过滤器只允许查询 goodsId10000~100000 的商品数据,以下为具体的实现步骤。

  1. 编写 GoodsIdGatewayFilterFactory 类。

    gateway-demo 项目中,新建 ltd.gateway.cloud.newbee.filter 包,并新建 GoodsIdGatewayFilterFactory.java 文件,具体代码及注释如下:

    package ltd.gateway.cloud.newbee.filter;
    
    import org.springframework.cloud.gateway.filter.GatewayFilter;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFa
    ctory;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    import java.nio.charset.StandardCharsets;
    import java.util.Arrays;
    import java.util.List;
    
    @Component
    public class GoodsIdGatewayFilterFactory extends AbstractGatewayFilterFactory<GoodsIdGatewayFilterFactory.Config> {
    
        public GoodsIdGatewayFilterFactory() { //构造函数
            super(Config.class);
        }
    
        @Override
        public List<String> shortcutFieldOrder() {
            // 定义配置文件中的参数项(最小值和最大值)
            return Arrays.asList("minValue", "maxValue");
        }
    
        @Override
        public GatewayFilter apply(Config config) {
            return new GatewayFilter() {
                @Override
                public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                    // 获取参数值
                    String goodsIdParam = exchange.getRequest().getQueryParams().getFirst("goodsId");
                    // 判空
                    if (!StringUtils.isEmpty(goodsIdParam)) {
                        int goodsId = Integer.parseInt(goodsIdParam);
                        // 判断goodsId是否在配置区间内,直接放行
                        if (goodsId > config.getMinValue() && goodsId < config.getMaxValue()) {
                            return chain.filter(exchange);
                        } else {
                            // 不符合条件,返回错误的提示信息,不进行后续的路由
                            byte[] bytes = ("BAD REQUEST").getBytes(StandardCharsets.UTF_8);
                            DataBuffer wrap = exchange.getResponse().bufferFactory().wrap(bytes);
                            exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
                            return exchange.getResponse().writeWith(Flux.just(wrap));
                        }
                    }
                    // 直接放行
                    return chain.filter(exchange);
                }
            };
        }
    
        //接收配置文件中定义的最大值和最小值
        public static class Config {
            private int minValue;
            private int maxValue;
    
            public int getMinValue() {
                return minValue;
            }
    
            public void setMinValue(int minValue) {
                this.minValue = minValue;
            }
    
            public int getMaxValue() {
                return maxValue;
            }
    
            public void setMaxValue(int maxValue) {
                this.maxValue = maxValue;
            }
        }
    }

    在该类中,分别定义了配置文件中定义的区间参数 minValuemaxValue。在 filter() 方法中定义了具体的判断逻辑,获取 goodsId 参数值后判断是否在配置的区间内,若符合条件,则直接放行请求;若不符合条件,则返回错误的提示信息,不进行后续的路由。

  2. 配置自定义局部过滤器

    application.properties 配置文件中配置自定义的断言工厂。路由配置如下:

    spring.cloud.gateway.routes[0].id=goods-service-route
    spring.cloud.gateway.routes[0].uri=lb://newbee-cloud-goods-service
    spring.cloud.gateway.routes[0].order=1
    spring.cloud.gateway.routes[0].predicates[0]=Path=/goods/**
    # 自定义过滤器配置,配置项为 goodsId,最大值为 100000,最小值为 10000
    spring.cloud.gateway.routes[0].filters[0]=GoodsId=10000,100000
  3. 功能验证。

    编码完成后,需要启动 Nacos Server,之后依次启动 gateway-demogoods-service-demo 项目。如果未能成功启动,则开发人员需要查看控制台中的日志是否报错,并及时确认问题和修复。启动成功后进入 Nacos 控制台,单击 “服务管理” 中的服务列表,可以看到列表中已经存在两个服务的服务信息。

  4. 打开浏览器进行功能验证,依次使用不同的地址进行测试,页面显示内容如图 9-14 所示,结果整理见表 9-5。

    image 2025 04 16 19 06 41 261
    Figure 4. 图9-14 使用GoodsIdGatewayFilterFactory局部过滤器后的请求测试结果
    image 2025 04 16 19 06 59 685
    Figure 5. 表9-5 使用不同的地址进行测试的结果

所有请求都被自定义过滤器处理,并且最终结果与预期结果一致。自定义局部过滤器验证成功!

自定义全局过滤器编码实践

Spring Cloud Gateway 组件中,全局过滤器同样支持开发人员的自定义操作。编码实现也不复杂,只需要实现 GlobalFilterOrdered 这两个接口即可。过滤器的命名没有限定的规则,比较自由。接下来,笔者将实现一个全局过滤器,用于统计每个请求的处理时间。之后重启项目访问各个URL即可在控制台看到接口调用时间。具体操作步骤如下。

  1. 编写 TimeCalculateGlobalFilter 类。

    gateway-demo 项目的 td.gateway.cloud.newbee.filter 包中新建 TimeCalculateGlobalFilter.java 文件,具体代码及注释如下:

    package ltd.gateway.cloud.newbee.filter;
    
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    //全局过滤器,统计接口调用时间
    @Component
    public class TimeCalculateGlobalFilter implements GlobalFilter, Ordered {
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 请求开始时间
            long startTime = System.currentTimeMillis();
            String requestURL = String.format("Host:%s Path:%s Params:%s",
                    exchange.getRequest().getURI().getHost(),
                    exchange.getRequest().getURI().getPath(),
                    exchange.getRequest().getQueryParams());
    
            System.out.println(requestURL);
    
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                // 请求结束时间
                long endTime = System.currentTimeMillis();
                // 打印调用时间
                long requestTime = endTime - startTime;
    
                System.out.println(exchange.getRequest().getURI().getPath() + "请求时间为" + requestTime + "毫秒");
            }));
        }
    
        @Override
        public int getOrder() {
            return Ordered.LOWEST_PRECEDENCE;
        }
    }

    该类中的逻辑比较简单,先获取请求开始的时间,然后等待具体微服务的响应,并记录请求结束时间,最后计算调用时间并输出到控制台。

  2. 功能验证

    此时,gateway-demo 项目的配置文件如下:

    server.port=8147
    # 应用名称
    spring.application.name=newbee-cloud-gateway-service
    # 注册中心 Nacos 的访问地址
    spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
    # 登录名(默认为 nacos, 可自行修改)
    spring.cloud.nacos.username=nacos
    # 密码(默认为 nacos, 可自行修改)
    spring.cloud.nacos.password=nacos
    
    spring.cloud.gateway.routes[0].id=goods-service-route
    spring.cloud.gateway.routes[0].uri=lb://newbee-cloud-goods-service
    spring.cloud.gateway.routes[0].order=1
    spring.cloud.gateway.routes[0].predicates[0]=Path=/goods/**
    
    spring.cloud.gateway.routes[1].id=shopcart-service-route
    spring.cloud.gateway.routes[1].uri=lb://newbee-cloud-shopcart-service
    spring.cloud.gateway.routes[1].order=1
    spring.cloud.gateway.routes[1].predicates[0]=Path=/shop-cart/**

    全局过滤器不用在配置文件中做额外的配置,所有请求都会使用它。

    编码完成后,需要启动 Nacos Server,之后依次启动 gateway-demogoods-service-demoshopcart-service-demo 这三个项目。如果未能成功启动,则开发人员需要查看控制台中的日志是否报错,并及时确认问题和修复。启动成功后进入 Nacos 控制台,单击 “服务管理” 中的服务列表,可以看到列表中已经存在三个服务的服务信息。

  3. 打开浏览器进行功能验证,依次使用不同的地址进行测试,结果整理见表9-6。

    image 2025 04 16 19 11 34 265
    Figure 6. 表9-6 使用不同的地址进行测试的结果

    所有请求都被自定义的 TimeCalculateGlobalFilter 过滤器处理,并且每个请求所花费的时间都被计算并打印出来。自定义全局过滤器功能验证成功!