1. 概述

本文将带你深入掌握如何编写自定义的 Spring Cloud Gateway 过滤器。

我们之前在《探索 Spring Cloud Gateway》一文中介绍了该框架及其内置过滤器。但实际项目中,仅靠内置功能远远不够。✅

本文目标是让你真正“榨干”API 网关的能力——通过自定义过滤器实现更灵活、更强大的控制。

我们将从以下几个层面展开:

  • ✅ 编写影响所有请求的 全局过滤器(Global Filter)
  • ✅ 实现可按需绑定到特定路由的 网关过滤器工厂(GatewayFilterFactory)
  • ✅ 进阶实战:修改请求/响应、链式调用其他服务(Reactive 风格)

踩坑提示:Spring Cloud Gateway 基于 WebFlux 和 Project Reactor,如果你对响应式编程不熟,建议先补一补基础,否则后面代码会看得一头雾水。


2. 项目搭建

先快速搭建一个基础的 API 网关应用。

2.1 Maven 依赖配置

使用 Spring Cloud 时,强烈建议通过 dependencyManagement 统一管理版本,避免依赖冲突。

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

然后引入核心依赖(无需指定版本):

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

📌 版本选择建议:


2.2 网关基础配置

假设本地 8081 端口运行着一个服务,访问 /resource 接口会返回一个字符串。

我们的目标是:当请求网关的 /service/resource 时,自动转发到该服务。

配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: service_route
        uri: http://localhost:8081
        predicates:
        - Path=/service/**
        filters:
        - RewritePath=/service(?<segment>/?.*), $\{segment}

其中 RewritePath 是内置过滤器,用于重写路径。比如 /service/foo 会被重写为 /foo 再转发。

为了便于调试,开启网关和 Netty 客户端日志:

logging:
  level:
    org.springframework.cloud.gateway: DEBUG
    reactor.netty.http.client: DEBUG

3. 编写全局过滤器(Global Filter)

当请求匹配到某个路由后,会进入一个过滤器链。这些过滤器可分为两类:

  • “pre” 过滤器:请求转发前执行
  • “post” 过滤器:收到响应后执行

全局过滤器作用于所有请求,实现方式非常简单:✅

实现 GlobalFilter 接口并注册为 Spring Bean

3.1 编写“pre”全局过滤器

@Component
public class LoggingGlobalPreFilter implements GlobalFilter {

    final Logger logger = LoggerFactory.getLogger(LoggingGlobalPreFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        logger.info("Global Pre Filter executed");
        return chain.filter(exchange);
    }
}

逻辑清晰:打印日志 → 继续执行过滤器链。


3.2 编写“post”全局过滤器

“post”阶段稍微复杂,因为必须等整个链执行完才能执行后续逻辑。⚠️

利用 Reactor 的 then() 操作符即可实现:

@Configuration
public class LoggingGlobalFiltersConfigurations {

    final Logger logger = LoggerFactory.getLogger(LoggingGlobalFiltersConfigurations.class);

    @Bean
    public GlobalFilter postGlobalFilter() {
        return (exchange, chain) -> {
            return chain.filter(exchange)
                .then(Mono.fromRunnable(() -> {
                    logger.info("Global Post Filter executed");
                }));
        };
    }
}

📌 核心点:

  • chain.filter(exchange) 返回 Mono<Void>,表示异步执行链
  • .then(...) 在链执行完成后触发回调

调用 /service/resource 后,日志输出如下:

DEBUG --- o.s.c.g.h.RoutePredicateHandlerMapping: Route matched: service_route
INFO  --- c.b.s.c.f.global.LoggingGlobalPreFilter: Global Pre Filter executed
DEBUG --- r.netty.http.client.HttpClientConnect: [id: 0x58f7e075, L:/127.0.0.1:57215 - R:localhost/127.0.0.1:8081] Handler is being applied
INFO  --- c.f.g.LoggingGlobalFiltersConfigurations: Global Post Filter executed

顺序完全符合预期。


3.3 合并“pre”和“post”逻辑

一个过滤器完全可以同时处理前后逻辑:

@Component
public class FirstPreLastPostGlobalFilter implements GlobalFilter, Ordered {

    final Logger logger = LoggerFactory.getLogger(FirstPreLastPostGlobalFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        logger.info("First Pre Global Filter");
        return chain.filter(exchange)
            .then(Mono.fromRunnable(() -> {
                logger.info("Last Post Global Filter");
            }));
    }

    @Override
    public int getOrder() {
        return -1; // 越小越早执行
    }
}

⚠️ 注意 Ordered 接口的作用:

  • 数值越小,优先级越高
  • “pre” 阶段:按 order 升序执行
  • “post” 阶段:按 order 降序执行(即后进先出)

SpringCloudGateway


4. 编写 GatewayFilter(局部过滤器)

全局过滤器太“粗”,很多时候我们需要只对特定路由生效的过滤器。

4.1 定义 GatewayFilterFactory

实现自定义 GatewayFilter 的标准姿势是继承 AbstractGatewayFilterFactory

@Component
public class LoggingGatewayFilterFactory extends 
    AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {

    final Logger logger = LoggerFactory.getLogger(LoggingGatewayFilterFactory.class);

    public LoggingGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        // ...
    }

    public static class Config {
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;

        // 构造函数、getter/setter...
    }
}

📌 关键点:

  • Config 类用于接收外部配置参数
  • 必须提供无参构造 + setter,否则属性注入会失败 ❌

过滤器逻辑:

@Override
public GatewayFilter apply(Config config) {
    return (exchange, chain) -> {
        if (config.isPreLogger()) {
            logger.info("Pre GatewayFilter logging: " + config.getBaseMessage());
        }
        return chain.filter(exchange)
            .then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    logger.info("Post GatewayFilter logging: " + config.getBaseMessage());
                }
            }));
    };
}

4.2 通过配置文件注册过滤器

application.yml 中使用:

spring:
  cloud:
    gateway:
      routes:
      - id: service_route
        uri: http://localhost:8081
        predicates:
        - Path=/service/**
        filters:
        - RewritePath=/service(?<segment>/?.*), $\{segment}
        - name: Logging
          args:
            baseMessage: My Custom Message
            preLogger: true
            postLogger: true

或使用简洁语法:

filters:
- RewritePath=/service(?<segment>/?.*), $\{segment}
- Logging=My Custom Message, true, true

要支持简洁语法,需重写 shortcutFieldOrder()

@Override
public List<String> shortcutFieldOrder() {
    return Arrays.asList("baseMessage", "preLogger", "postLogger");
}

4.3 控制过滤器执行顺序

如果想指定该过滤器在链中的位置,返回 OrderedGatewayFilter

@Override
public GatewayFilter apply(Config config) {
    return new OrderedGatewayFilter((exchange, chain) -> {
        // 过滤逻辑
    }, 1); // order = 1
}

数值越小,越早执行。


4.4 编程式注册过滤器

也可以通过 Java 配置方式定义路由:

@Bean
public RouteLocator routes(RouteLocatorBuilder builder, LoggingGatewayFilterFactory loggingFactory) {
    return builder.routes()
        .route("service_route_java_config", r -> r.path("/service/**")
            .filters(f -> 
                f.rewritePath("/service(?<segment>/?.*)", "$\\{segment}")
                 .filter(loggingFactory.apply(new Config("My Custom Message", true, true))))
            .uri("http://localhost:8081"))
        .build();
}

这种方式更灵活,适合复杂逻辑或动态路由场景。


5. 进阶实战

光打印日志太小儿科了。真正的网关过滤器应该能:

  • ✅ 修改请求头/参数
  • ✅ 修改响应内容
  • ✅ 链式调用其他服务(非阻塞)

5.1 检查并修改请求

场景:旧版 API 通过 locale 参数传语言,新版改用 Accept-Language 头。我们希望网关做兼容处理:

  1. 若有 Accept-Language,保留
  2. 否则,取 locale 参数值作为语言
  3. 都没有则用默认值
  4. 最后删除 locale 参数

实现如下:

(exchange, chain) -> {
    ServerHttpRequest request = exchange.getRequest();
    List<Locale.LanguageRange> acceptLanguage = request.getHeaders().getAcceptLanguage();

    if (!acceptLanguage.isEmpty()) {
        return chain.filter(exchange); // 已有语言头,直接放行
    }

    // 获取 locale 参数
    String queryParamLocale = request.getQueryParams().getFirst("locale");
    Locale requestLocale = Optional.ofNullable(queryParamLocale)
        .map(Locale::forLanguageTag)
        .orElse(config.getDefaultLocale());

    // 修改请求头
    ServerHttpRequest modifiedRequest = request.mutate()
        .headers(h -> h.setAcceptLanguageAsLocales(Collections.singletonList(requestLocale)))
        .build();

    // 重新构建 exchange 并移除 query param
    URI newUri = UriComponentsBuilder.fromUri(request.getURI())
        .replaceQueryParams(new LinkedMultiValueMap<>())
        .build()
        .toUri();

    ServerWebExchange modifiedExchange = exchange.mutate()
        .request(modifiedRequest)
        .request(request -> request.uri(newUri))
        .build();

    return chain.filter(modifiedExchange);
}

📌 踩坑点:

  • 请求是不可变对象,必须通过 mutate() 创建副本
  • 修改 URI 需要重新构建整个 ServerWebExchange

5.2 修改响应

场景:后端服务返回自定义语言头 Bael-Custom-Language-Header,我们想将其标准化为 Content-Language

(exchange, chain) -> {
    return chain.filter(exchange)
        .then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();
            Optional.ofNullable(exchange.getRequest().getQueryParams().getFirst("locale"))
                .ifPresent(qp -> {
                    String contentLanguage = response.getHeaders().getContentLanguage().getLanguage();
                    response.getHeaders().add("Bael-Custom-Language-Header", contentLanguage);
                });
        }));
}

✅ 注意:响应对象可直接修改,无需 mutate

⚠️ 顺序很重要!该过滤器必须在“pre”阶段修改请求的过滤器之后执行,否则拿不到原始 locale 参数。


5.3 链式调用其他服务(Reactive)

场景:语言选择依赖第三方服务,需先调用语言服务获取推荐语言。

关键:必须非阻塞调用,否则会破坏响应式流。

(exchange, chain) -> {
    return WebClient.create()
        .get()
        .uri(config.getLanguageEndpoint())
        .exchange()
        .flatMap(response -> {
            return response.statusCode().is2xxSuccessful() ?
                response.bodyToMono(String.class) :
                Mono.just(config.getDefaultLanguage());
        })
        .map(LanguageRange::parse)
        .map(range -> {
            // 修改请求头
            ServerHttpRequest newRequest = exchange.getRequest().mutate()
                .headers(h -> h.setAcceptLanguage(range))
                .build();
            return exchange.mutate().request(newRequest).build();
        })
        .flatMap(chain::filter); // 继续执行链
}

✅ 核心思路:

  • 使用 WebClient 发起异步调用
  • 通过 flatMap 将外部服务响应与网关过滤链串联
  • 最终仍返回 Mono<Void>,保持响应式流完整性

6. 总结

本文系统讲解了 Spring Cloud Gateway 自定义过滤器的开发方式:

类型 适用场景 注册方式
GlobalFilter 全局通用逻辑(如日志、鉴权) 实现接口 + @Component
GatewayFilterFactory 可配置、可复用的局部过滤器 继承 AbstractGatewayFilterFactory

✅ 关键要点回顾:

  • “pre” 过滤器直接返回 chain.filter()
  • “post” 过滤器使用 .then() 回调
  • 修改请求必须 mutate(),响应可直接改
  • 链式调用外部服务要用 WebClient + flatMap
  • 执行顺序通过 Ordered 接口控制

所有完整示例代码已上传至 GitHub:

👉 https://github.com/eugenp/tutorials/tree/master/spring-cloud-modules/spring-cloud-gateway

测试时请确保目标服务(8081)已启动,并通过 Maven 执行集成测试。


原始标题:Writing Custom Spring Cloud Gateway Filters | Baeldung