Spring Cloud Gateway 网关路由与过滤器使用记录

作者:old wang 发布时间: 2022-04-12 阅读量:3 评论数:0

在微服务系统中,网关通常是外部流量进入系统的统一入口。

客户端请求不会直接访问每个后端服务,而是先进入网关,再由网关根据路由规则转发到对应的服务。

网关通常会承担这些职责:

  • 请求路由;

  • 服务转发;

  • 负载均衡;

  • 权限校验;

  • 请求过滤;

  • 统一日志;

  • 限流熔断;

  • 请求头、响应头处理。

在 Spring Cloud 体系中,常用的网关组件是 Spring Cloud Gateway。

本文记录 Spring Cloud Gateway 的基础使用方式,包括:

  • Gateway 基本依赖;

  • 代码方式配置路由;

  • 配置文件方式配置路由;

  • 路由断言;

  • 过滤器工厂;

  • 自定义局部过滤器;

  • 自定义全局过滤器。

一、Gateway 简介

早期 Spring Cloud 项目中,经常使用 Netflix Zuul 作为网关。

后来 Spring Cloud 推出了自己的网关组件:Spring Cloud Gateway。

Spring Cloud Gateway 和传统 Servlet 组件不太一样,它基于:

Spring Boot
Spring WebFlux
Reactor Netty

也就是说,它采用的是响应式编程模型。

所以在使用 Gateway 时,需要注意几个点。

1. 不要同时引入 spring-boot-starter-web

Gateway 基于 Spring WebFlux。

如果项目中同时引入:

<artifactId>spring-boot-starter-web</artifactId>

可能会和 WebFlux 产生冲突。

Gateway 项目中一般只保留 Gateway 相关依赖,不再引入传统 Spring MVC Web 依赖。

2. Gateway 默认使用 Netty

Gateway 默认运行在 Netty 容器上。

如果项目中引入 Tomcat、Jetty 等 Servlet 容器相关依赖,运行时可能出现异常。

3. 建议使用 jar 包方式启动

如果新建项目时选择了 war 模式,需要注意删除 IDE 自动生成的 ServletInitializer.java,并将打包方式改成 jar

Gateway 更适合作为独立网关服务运行。

二、引入依赖

新建一个 Gateway 模块,引入 Gateway 依赖。

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

如果需要通过注册中心进行服务发现,可以再引入 Eureka Client。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

实际项目中,如果使用的是 Nacos、Consul 等注册中心,对应依赖需要替换成对应的服务发现组件。

三、通过代码配置路由

Spring Cloud Gateway 支持通过 Java 代码配置路由。

示例:

package com.scd.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

    /**
     * 创建路由规则
     */
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("customer", r -> r.path("/customer-api/**")
                        .filters(f -> f.stripPrefix(1))
                        .uri("http://localhost:3001"))

                .route("goods", r -> r.path("/goods-api/**")
                        .filters(f -> f.stripPrefix(1))
                        .uri("lb://goods"))

                .build();
    }
}

这里定义了两个路由。

第一个路由:

.route("customer", r -> r.path("/customer-api/**")
        .filters(f -> f.stripPrefix(1))
        .uri("http://localhost:3001"))

含义是:

  • 路由 ID 为 customer

  • 匹配路径 /customer-api/**

  • 转发前去掉路径中的第一层;

  • 最终转发到 http://localhost:3001

例如请求:

http://localhost:6001/customer-api/customer/name/1

经过:

stripPrefix(1)

处理后,请求路径会变成:

/customer/name/1

最终转发到:

http://localhost:3001/customer/name/1

第二个路由:

.route("goods", r -> r.path("/goods-api/**")
        .filters(f -> f.stripPrefix(1))
        .uri("lb://goods"))

这里的 URI 使用的是:

lb://goods

这是基于服务发现的路由方式。

其中:

goods

表示注册中心中的服务名。

Gateway 会通过注册中心找到 goods 服务的可用实例,并进行负载均衡转发。

四、通过配置文件配置路由

除了代码方式,也可以通过 application.yml 配置路由。

示例:

spring:
  application:
    name: gateway

  cloud:
    gateway:
      routes:
        - id: customer
          uri: http://localhost:3001
          predicates:
            - Path=/customer-api/**
          filters:
            - StripPrefix=1

        - id: goods
          uri: lb://goods
          predicates:
            - Path=/goods-api/**
          filters:
            - StripPrefix=1

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:1001/eureka,http://localhost:1002/eureka

server:
  port: 6001

logging:
  level:
    root: info

这段配置和前面的 Java 代码配置基本等价。

其中:

predicates:
  - Path=/customer-api/**

表示路径断言。

filters:
  - StripPrefix=1

表示过滤器配置,作用是去掉请求路径中的第一层。

uri: lb://goods

表示通过服务发现转发到 goods 服务。

五、Gateway 中的几个核心概念

Spring Cloud Gateway 中有几个核心概念。

1. Route

Route 表示一条路由规则。

一条路由通常由下面几部分组成:

  • 路由 ID;

  • 目标 URI;

  • 断言集合;

  • 过滤器集合。

只有当断言匹配成功后,请求才会进入对应路由。

2. Predicate

Predicate 表示路由断言。

它用于判断当前请求是否匹配某条路由。

例如:

- Path=/customer-api/**

就是一个路径断言。

只有请求路径匹配 /customer-api/** 时,当前路由才会生效。

3. Filter

Filter 表示过滤器。

过滤器可以在请求转发前后执行一些自定义逻辑。

常见用途包括:

  • 修改请求路径;

  • 添加请求头;

  • 添加响应头;

  • 权限校验;

  • 日志记录;

  • 参数处理;

  • 灰度标识传递。

Gateway 中的过滤器分为两类:

  • 局部过滤器;

  • 全局过滤器。

局部过滤器只对指定路由生效。

全局过滤器对所有路由生效。

六、路由断言工厂

Gateway 的断言由路由断言工厂生成。

相关接口是:

RoutePredicateFactory<C>

它的核心方法是:

Predicate<ServerWebExchange> apply(C config);

Gateway 内置了很多断言工厂。

这些类的命名通常是:

XXXRoutePredicateFactory

例如:

PathRoutePredicateFactory
QueryRoutePredicateFactory

配置文件中的:

- Path=/customer-api/**

对应的就是 PathRoutePredicateFactory

七、使用 Query 断言

除了路径断言,也可以使用请求参数断言。

例如要求请求中必须存在参数 id

代码方式:

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route("customer", r -> r.path("/customer-api/**")
                    .and().query("id")
                    .filters(f -> f.stripPrefix(1))
                    .uri("http://localhost:3001"))
            .build();
}

如果要求参数 id 必须是数字,可以继续添加正则匹配:

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route("customer", r -> r.path("/customer-api/**")
                    .and().query("id")
                    .and().query("id", "^[0-9]*$")
                    .filters(f -> f.stripPrefix(1))
                    .uri("http://localhost:3001"))
            .build();
}

配置文件方式:

spring:
  cloud:
    gateway:
      routes:
        - id: customer
          uri: http://localhost:3001
          predicates:
            - Path=/customer-api/**
            - Query=id
            - Query=id,^[0-9]*$
          filters:
            - StripPrefix=1

这段配置表示:

  1. 请求路径必须匹配 /customer-api/**

  2. 请求参数中必须包含 id

  3. id 参数必须匹配数字格式。

例如可以匹配:

/customer-api/customer/name/1?id=100

但不能匹配:

/customer-api/customer/name/1

也不能匹配:

/customer-api/customer/name/1?id=abc

八、过滤器工厂

Gateway 也内置了很多过滤器工厂。

相关接口是:

GatewayFilterFactory<C>

它的核心方法是:

GatewayFilter apply(C config);

过滤器工厂的命名规则通常是:

XXXGatewayFilterFactory

例如:

AddResponseHeaderGatewayFilterFactory
StripPrefixGatewayFilterFactory

前面使用的:

- StripPrefix=1

对应的就是 StripPrefixGatewayFilterFactory

如果想给响应添加一个响应头,可以使用 AddResponseHeader

代码方式:

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route("goods", r -> r.path("/goods-api/**")
                    .filters(f -> f.stripPrefix(1)
                            .addResponseHeader("response-header", "response-value"))
                    .uri("lb://goods"))
            .build();
}

配置文件方式:

spring:
  cloud:
    gateway:
      routes:
        - id: goods
          uri: lb://goods
          predicates:
            - Path=/goods-api/**
          filters:
            - StripPrefix=1
            - AddResponseHeader=response-header,response-value

这样 Gateway 在响应客户端时,会添加响应头:

response-header: response-value

九、自定义局部过滤器

局部过滤器只对某个路由生效。

可以通过实现 GatewayFilter 的方式定义局部过滤器。

示例:

private GatewayFilter myGatewayFilter() {
    return (exchange, chain) -> {
        System.out.println("我是局部过滤器逻辑");

        ServerHttpRequest request = exchange.getRequest();

        ServerHttpRequest newRequest = request.mutate()
                .header("request-header", "my-request-header")
                .build();

        String id = request.getQueryParams().getFirst("id");

        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        response.getHeaders().add("response-header", "my-response-header");

        return chain.filter(exchange.mutate().request(newRequest).build());
    };
}

然后在指定路由中使用:

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route("goods", r -> r.path("/goods-api/**")
                    .filters(f -> f.stripPrefix(1)
                            .filter(myGatewayFilter()))
                    .uri("lb://goods"))
            .build();
}

这里需要注意一个点。

不能直接这样修改请求头:

request.getHeaders().add("header", "myheader");

因为:

request.getHeaders()

返回的是只读对象。

正确做法是使用:

request.mutate()

构建一个新的请求对象。

例如:

ServerHttpRequest newRequest = request.mutate()
        .header("request-header", "my-request-header")
        .build();

然后通过:

exchange.mutate().request(newRequest).build()

把新的 request 放回 exchange。

十、自定义全局过滤器

全局过滤器对所有路由生效。

只需要定义一个 GlobalFilter Bean。

@Bean
public GlobalFilter globalFilter() {
    return (exchange, chain) -> {
        System.out.println("我是全局过滤器");

        return chain.filter(exchange);
    };
}

因为加了:

@Bean

Spring 会把这个 GlobalFilter 注册到 IoC 容器中。

Gateway 会自动识别它,并加入过滤器链。

全局过滤器适合处理所有请求都需要经过的逻辑,例如:

  • 统一日志;

  • traceId 生成;

  • 鉴权;

  • 黑白名单;

  • 统一请求头处理;

  • 全局异常信息记录。

十一、完整示例

下面是一个整合了路由、局部过滤器和全局过滤器的示例。

package com.scd.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;

@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

    /**
     * 创建路由规则
     */
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("customer", r -> r.path("/customer-api/**")
                        .filters(f -> f.stripPrefix(1))
                        .uri("http://localhost:3001"))

                .route("goods", r -> r.path("/goods-api/**")
                        .filters(f -> f.stripPrefix(1)
                                .filter(myGatewayFilter()))
                        .uri("lb://goods"))

                .build();
    }

    /**
     * 自定义局部过滤器
     */
    private GatewayFilter myGatewayFilter() {
        return (exchange, chain) -> {
            System.out.println("我是局部过滤器逻辑");

            ServerHttpRequest request = exchange.getRequest();

            ServerHttpRequest newRequest = request.mutate()
                    .header("request-header", "my-request-header")
                    .build();

            String id = request.getQueryParams().getFirst("id");

            ServerHttpResponse response = exchange.getResponse();
            response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
            response.getHeaders().add("response-header", "my-response-header");

            return chain.filter(exchange.mutate().request(newRequest).build());
        };
    }

    /**
     * 自定义全局过滤器
     */
    @Bean
    public GlobalFilter globalFilter() {
        return (exchange, chain) -> {
            System.out.println("我是全局过滤器");

            return chain.filter(exchange);
        };
    }
}

启动 Gateway 模块后,访问:

http://localhost:6001/customer-api/customer/name/1

请求会转发到:

http://localhost:3001/customer/name/1

访问:

http://localhost:6001/goods-api/goods/customer/name/1

请求会通过服务发现转发到 goods 服务。

十二、使用 Gateway 时的注意点

1. Gateway 项目不要混用 Spring MVC

Gateway 基于 WebFlux。

不要在 Gateway 模块中同时引入传统 Web MVC 依赖。

否则容易出现启动异常或运行时冲突。

2. 路由路径和 StripPrefix 要配套

如果路由路径是:

/customer-api/**

而后端服务实际路径是:

/customer/**

就需要配置:

- StripPrefix=1

否则转发后的路径可能不匹配后端接口。

3. lb:// 依赖服务发现

如果使用:

lb://goods

需要保证:

  1. 项目已经接入注册中心;

  2. goods 服务已经成功注册;

  3. Gateway 能从注册中心拉取到服务实例。

否则请求无法正确转发。

4. 修改请求对象要使用 mutate

Gateway 中的请求对象是不可变风格。

如果要修改请求头,需要使用:

request.mutate()

不要直接修改:

request.getHeaders()

5. 局部过滤器和全局过滤器职责要分清

局部过滤器适合某个路由专属逻辑。

全局过滤器适合所有请求都需要处理的逻辑。

不要把所有逻辑都堆到全局过滤器里,否则后期维护会比较困难。

结论

Spring Cloud Gateway 是微服务系统中的统一流量入口。

它通过:

  • Route 定义路由;

  • Predicate 判断请求是否匹配;

  • Filter 在请求前后增加处理逻辑;

  • lb://service-id 支持服务发现和负载均衡;

  • 局部过滤器处理单个路由逻辑;

  • 全局过滤器处理所有请求通用逻辑。

常见配置方式有两种:

  1. 通过 Java 代码配置 RouteLocator

  2. 通过 application.yml 配置路由规则。

实际项目中,简单路由更推荐放在配置文件中,便于维护和调整;需要复杂逻辑时,可以使用代码方式或自定义过滤器。

评论