Skip to content

Spring Cloud Gateway

1. 介绍

Gateway,即为网关,为用户提供了一个统一入口,并将请求路由到后端不同的微服务。

假设现在有多个微服务,包括订单服务、商品服务、支付服务等,并且这些微服务部署在多台服务器上,那么前端应用应该访问哪个请求路径呢?显然,后台微服务应该给出一个统一的入口,这就是网关的作用之一。

当请求到达网关后,例如https://gateway-ip:port/createOrder,网关怎么判断这个请求应该转发给哪个微服务呢?所以,我们应该在网关服务里设置路由规则,由路由规则决定将请求转发给哪个微服务。

为了获取微服务的地址,网关也需要到服务注册与发现中心(例如Nacos)上找到对应微服务的地址。

image-20250709103335909

2. 环境搭建

单独新建一个模块,引入如下依赖:

xml
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
</dependencies>

注意,Spring Cloud Gateway提供两套实现,spring-cloud-starter-gatewayspring-cloud-starter-gateway-mvc,前者基于Reactive模型,后者基于传统的Servlet模型,一般使用前者。

image-20250709113128054

网关主启动类:

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

然后在配置文件中修改:

yaml
server:
  port: 80

spring:
  application:
    name: gateway
  cloud:
    gateway:
      enabled: true

为了前端访问时不带端口号,一般将端口改为80。

3. 路由介绍

网关中最主要的功能就是请求路由,对应的是路由规则。

3.1 术语与流程介绍

路由规则涉及三个术语:

  • Route:一个Route就是一个路由规则,由ID、URI、一组Predicate(断言)和一组Filter(过滤器)组成。当请求到达网关时,如果该路由规则的断言全部匹配(返回true),表示该请求可以由该路由规则处理,会将该请求转发到URI对应的路径;
  • Predicate:断言,是一个函数式接口(Predicate),当请求到来时,被封装为ServerWebExchange,断言可以对该请求实行特定的匹配规则,返回boolean值;
  • Filter:过滤器,更容易理解的话应该称为拦截器,分为拦截请求和拦截响应,当一个请求要转发给某个微服务时,可以使用请求拦截对该请求做出修改,也可以使用响应拦截对微服务返回的响应做出修改;

网关工作图如下:

Spring Cloud Gateway Diagram

当请求到达网关时,如果Handler Mapping判读该请求全部符合某个Route的断言规则,那么就交给Web Handler进行请求转发,首先会经过拦截器链的前执行器(在请求到达微服务之前),之后请求到达微服务执行业务,之后响应从微服务回到网关,经过拦截器的后执行器(请求从微服务回来),最终返回给前端。

3.2 配置路由规则

我们可以在网关配置文件中配置路由规则,如下:

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/api/product/**
  • spring.cloud.gateway.routes是一个列表,其中的元素是RouteDefinition,包含如下内容:

    java
    public class RouteDefinition {
        private String id;
        private @NotEmpty @Valid List<PredicateDefinition> predicates = new ArrayList();
        private @Valid List<FilterDefinition> filters = new ArrayList();
        private @NotNull URI uri;
        private Map<String, Object> metadata = new HashMap();
        private int order = 0;
    }
    • id:路由规则标识符;
    • uri:请求匹配上该路由规则后,需要将该请求转发到哪个地址;可以配置的内容如下:
      • 具体的主机和端口,格式为:http://hostname:porthttps://hostname:port
      • 带负载均衡的服务发现名称,格式为:lb://service-namelb表示load balancer,负载均衡的意思,service-name为微服务名称,需要与Nacos中的微服务名称相同;
      • WebSocket 连接,格式为:ws://hostname:portwss://hostname:port
    • predicates:一组断言规则,具体后续再讲解;
    • filters:一组过滤器规则,具体后续再讲解;
    • metadata:元数据,具体后续再讲解;
    • order:该路由规则的优先级,数字越低优先级越高。路由规则是有顺序的,如果前面的路由规则匹配上该请求,那么就不再进行后续匹配了。

3.3 测试

通过上面的配置,我们就可以测试路由是否生效了,不过,还需要注意的是,我们匹配的路径是/api/order/**等,表示请求路径应该以/api/order/开头,转发后到达微服务的路径也是这样的,所以需要修改微服务的请求路径,统一以/api/order/开头,在配置文件中修改如下:

properties
server.servlet.context-path=/api/order
properties
server.servlet.context-path=/api/product

并且,我们之前的订单服务,使用OpenFeign远程调用了商品服务,也需要添加前缀:

java
@FeignClient(value = "product-service", path = "/api/product", fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {

    @GetMapping("/product/{id}")
    Product getProduct(@PathVariable("id") Long id);

}

@FeignClient中的path指定前缀路径。

启动如下微服务和网关:

image-20250709114055113

然后在浏览器中访问如下地址:

image-20250709114034364

测试成功。

4. Predicate

在之前的例子中,我们写的断言如下:

yaml
          predicates:
            - Path=/api/order/**

为什么要这么写呢?

4.1 什么是断言

断言就是一种匹配规则,根据HTTP请求返回truefalse

在Spring Cloud Gateway中,断言由RoutePredicateFactory定义,其实现类如下:

image-20250709114627691

也就是说,每个实现类就实现了某种断言,例如PathRoutePredicateFactory就是根据请求路径匹配请求,该断言的名称就是Path

我们就以PathRoutePredicateFactory为例,介绍断言是如何工作的,首先在PathRoutePredicateFactory中,存在以下内部类:

java
public static class Config {

  private List<String> patterns = new ArrayList<>();

  private boolean matchTrailingSlash = true;
}
  • patterns表示可以匹配的路径,可以有多个;
  • matchTrailingSlash表示是否可以匹配路径末尾的/,默认值为true,如果设置为false,假设可以匹配的路径是/red/{segment},/blue/{segment},那么路径为/red/1/的请求就无法通过该断言,最终会导致该路由规则无法匹配到该请求;

之后,在PathRoutePredicateFactory的方法apply中,就可以根据配置判断是否匹配路径:

java
@Override
public Predicate<ServerWebExchange> apply(Config config) {
  // 首先从config中解析可以匹配的路径规则
  final ArrayList<PathPattern> pathPatterns = new ArrayList<>();
  synchronized (this.pathPatternParser) {
    pathPatternParser.setMatchOptionalTrailingSeparator(config.isMatchTrailingSlash());
    config.getPatterns().forEach(pattern -> {
      PathPattern pathPattern = this.pathPatternParser.parse(pattern);
      pathPatterns.add(pathPattern);
    });
  }
  // 返回GatewayPredicate对象
  return new GatewayPredicate() {
    // 在test方法中判断是否匹配
    @Override
    public boolean test(ServerWebExchange exchange) {
      // 拿到请求的路径
      PathContainer path = (PathContainer) exchange.getAttributes().computeIfAbsent(
          GATEWAY_PREDICATE_PATH_CONTAINER_ATTR,
          s -> parsePath(exchange.getRequest().getURI().getRawPath()));

      // config中的路径逐一与请求路径判断是否匹配
      PathPattern match = null;
      for (int i = 0; i < pathPatterns.size(); i++) {
        PathPattern pathPattern = pathPatterns.get(i);
        if (pathPattern.matches(path)) {
          match = pathPattern;
          break;
        }
      }

      if (match != null) {
        // 匹配上了,返回true
        traceMatch("Pattern", match.getPatternString(), path, true);
        PathMatchInfo pathMatchInfo = match.matchAndExtract(path);
        putUriTemplateVariables(exchange, pathMatchInfo.getUriVariables());
        exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ATTR, match.getPatternString());
        String routeId = (String) exchange.getAttributes().get(GATEWAY_PREDICATE_ROUTE_ATTR);
        if (routeId != null) {
          // populated in RoutePredicateHandlerMapping
          exchange.getAttributes().put(GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR, routeId);
        }
        return true;
      }
      else {
        // 没有匹配上,返回false
        traceMatch("Pattern", config.getPatterns(), path, false);
        return false;
      }
    }

    @Override
    public Object getConfig() {
      return config;
    }

    @Override
    public String toString() {
      return String.format("Paths: %s, match trailing slash: %b", config.getPatterns(),
          config.isMatchTrailingSlash());
    }
  };
}

除了匹配方法,还存在以下方法需要关注shortcutFieldOrder,表示简写形式:

java
private static final String MATCH_TRAILING_SLASH = "matchTrailingSlash";

@Override
public List<String> shortcutFieldOrder() {
  return Arrays.asList("patterns", MATCH_TRAILING_SLASH);
}

4.2 断言的写法

断言有两种形式的写法:完整形式与简写形式。

完整形式需要指明断言的名称与参数,例如:

yaml
    predicates:
      - name:  Path
        args:
          patterns:
            - /api/order/**
          matchTrailingSlash: false
  • name:就是断言实现类的前缀名称,例如PathRoutePredicateFactory的名称就是PathMethodRoutePredicateFactory的名称就是Method
  • args:断言实现类内部类Config的配置参数,例如PathRoutePredicateFactory的内部配置类有patternsmatchTrailingSlash,我们就可以在args中写上对应的配置;

简写形式的形式为name=args,例如:

yaml
    predicates:
      - Path=/api/order/**

也就是说使用名称为Path的断言实现,并且参数为/api/order/**,表示匹配路径是/api/order/**,如果要配置matchTrailingSlash,可以这样写:

yaml
    predicates:
      - Path=/api/order/**,false

5. Filter

5.1 Filter怎么配

与断言相似,Filter也有两种形式:完整形式与简写形式。

完整形式

yaml
    filters:
      - name: AddRequestHeader
        args:
          name: X-Request-Id
          value: abc
  • name:即GatewayFilterFactory的前缀,例如AddRequestHeaderGatewayFilterFactory的名称即为AddRequestHeader

  • args:过滤器的参数,可以查看apply()方法的参数,例如NameValueConfig

    java
    public static class NameValueConfig {
    
      @NotEmpty
      protected String name;
    
      @NotEmpty
      protected String value;
    }

简写形式

就是以name=args-value的形式定义的Filter,例如:

yaml
    filters:
      - AddResponseHeader=X-Request-Id,abc

完整的Filter列表请参照:https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway-server-webflux/gatewayfilter-factories.html

5.2 Filter作用

例如,我们以如下配置演示Filter的作用:

yaml
spring:
  application:
    name: gateway
  cloud:
    gateway:
      enabled: true
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            - AddRequestHeader=X-Request-Id,abc
            - name: AddResponseHeader
              args:
                name: X-Gateway-name
                value: ${spring.application.name}

以上配置了两个Filter,其中:

  • AddRequestHeader在向为服务转发请求时,会添加一个请求头,名称为X-request-Id,值为abc
  • AddResponseHeader在向前端返回响应时,也会添加一个响应头,名称为X-Gateway-name,值为${spring.application.name},即gateway

image-20250709131023923

5.3 自定义Filter

可以实现GatewayFilterFactory接口来实现自己的Filter,实现如下方法:

  • apply():具体的Filter行为,如果是在响应中添加响应头,需要在then()方法中添加逻辑,函数式编程;
  • shortcutFieldOrder():定义了简写形式;
  • getConfigClass():获取配置类;
java
@Component
public class CustomResponseHeaderGatewayFilterFactory implements GatewayFilterFactory<AbstractNameValueGatewayFilterFactory.NameValueConfig> {
    @Override
    public GatewayFilter apply(AbstractNameValueGatewayFilterFactory.NameValueConfig config) {

        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                return chain.filter(exchange).then(Mono.fromRunnable(()->{
                    String headerName = config.getName();
                    String value = "";
                    String headerValue = config.getValue();
                    switch (headerValue){
                        case "uuid": value = UUID.randomUUID().toString();break;
                        default: value = headerValue;
                    }
                    exchange.getResponse().getHeaders().add(headerName, value);
                }));
            }
        };

    }

    @Override
    public List<String> shortcutFieldOrder() {
        return List.of("name", "value");
    }

    @Override
    public Class<AbstractNameValueGatewayFilterFactory.NameValueConfig> getConfigClass() {
        return AbstractNameValueGatewayFilterFactory.NameValueConfig.class;
    }
}

将自定义的Filter加入到容器中。

之后,就可以在配置文件中使用自定义的Filter:

yaml
  filters:
    - CustomResponseHeader=customer-header,uuid

测试结果如下:

image-20250709141327616

5.4 默认Filter

除了为每个路由规则配置Filter外,我们也可以配置默认的Filter:

yaml
spring:
  cloud:
    gateway:
      default-filters:
      - AddResponseHeader=X-Response-Default-Red, Default-Blue
      - PrefixPath=/httpbin

5.5 RewritePathGatewayFilterFactory

在前面的例子中,为了匹配微服务,我们在请求路径上加了/api/order/,并且微服务的路径(controller)也要加上相同的路径前缀,如果没有路径,则会提示404。

我们可以使用RewritePathGatewayFilterFactory可以改写路径,即请求到达网关后,匹配到对应的微服务后,网关会改写请求路径,使其匹配后端微服务的路径。

使用如下:

yaml
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}

具体解释如下:

  • /api/order/?(?<segment>.*):这是一个正则表达式,匹配 /api/order/ 后面的内容,并将其捕获到名为 segment 的组中。? 表示 /api/order/ 后面可以没有内容或有任意内容。
  • /$\{segment}:重写后的路径,将原路径中 /api/order/ 后面的内容(即 ${segment})拼接到根路径 / 后面。

例如,请求路径为 /api/order/test 会被重写为 /test

6. Metadata

6.1 基本使用

metadata中可以配置额外的信息,随意配置,例如:

yaml
  metadata:
    author: shuimuzi
    description: service of gateway

之后,可以按照如下方式获取metadata:

java
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
// get all metadata properties
route.getMetadata();
// get a single metadata property
route.getMetadata(someKey);

例如,我们在自定义的CustomResponseHeaderGatewayFilterFactory中,获取metadata并加入到响应头中:

java
@Override
public GatewayFilter apply(AbstractNameValueGatewayFilterFactory.NameValueConfig config) {

    return new GatewayFilter() {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            return chain.filter(exchange).then(Mono.fromRunnable(()->{
                String headerName = config.getName();
                String value = "";
                String headerValue = config.getValue();
                switch (headerValue){
                    case "uuid": value = UUID.randomUUID().toString();break;
                    default: value = headerValue;
                }
                exchange.getResponse().getHeaders().add(headerName, value);

                Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
                Map<String, Object> metadata = route.getMetadata();
                metadata.forEach((k,v)->{
                    exchange.getResponse().getHeaders().add(k, v.toString());
                });
            }));
        }
    };
}

image-20250709144220238

6.2 CORS

我们可以在metadata中配置跨域相关内容:

yaml
  metadata:
    cors:
      allowedOrigins: '*'
      allowedMethods:
        - GET
        - POST
      allowedHeaders: '*'
      maxAge: 30

image-20250709144415149

可以看到,响应头中多了一些关于跨域的内容。