SpringCloud 11 —— 网关之Gateway (上) 谓词

Gateway简介

Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关,Spring Cloud Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2和 Project Reactor等技术。Spring Cloud Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能, 例如:熔断、限流、重试等。

注意:Spring Cloud Gateway需要Spring Boot和Spring Webflux提供的Netty下运行,不能在传统的Servlet容器中或作为WAR构建时使用

Spring Cloud Gateway 的特性:

  • 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建;
  • 动态路由:能够匹配任何请求属性;
  • 可以对路由指定 Predicate(断言)和 Filter(过滤器);
  • 集成Hystrix的断路器功能;
  • 集成 Spring Cloud 服务发现功能;
  • 易于编写的 Predicate(断言)和 Filter(过滤器);
  • 请求限流功能;
  • 支持路径重写。

工作原理

客户端向 Spring Cloud Gateway 发出请求。如果 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑

在没有端口的路由中定义的URI,HTTP和HTTPS URI的默认端口值分别为80和443。

与Zuul 1.*的对比

Zuul构建于 Servlet 2.5,兼容 3.x,使用的是阻塞式API不支持长连接

Spring Cloud Gateway构建于 Spring 5+,基于 Spring Boot 2.x 响应式的、非阻塞式的 API。同时,它支持 websockets,在使用方面也比较简单

毕竟Spring Cloud Gateway是Spring Cloud 现在主力推广的,从最新的Spring Cloud 版本可以看到并没有集成Zuul 2.*

最后在满足公司业务的情况下,用Zuul 1.* 、Spring Cloud Gateway 都是可以的。毕竟很少有公司达到Netflix 日几千亿的访问量。 Zuul 2.* 目前使用的比较少,遇到问题文档比较少,并且Spring Cloud 目前并没有集成,暂时还是先观望吧。

Gateway 的基本使用

谓词

内置的路由谓词工厂
在简单使用这一节我们主要来说一下内置的路由,并介绍如何使用。官方文档

Spring Cloud Gateway内置了一系列的路由谓词工厂,以便我们可以在开发中灵活的使用Gateway进行请求转发。Gateway内置的路由谓词工厂如下表:

路由谓词工厂作用参数
After当且仅当请求时的时间After配置的时间时,才转发该请求一个带有时区的具体时间
Before当且仅当请求时的时间Before配置的时间时,才转发该请求一个带有时区的具体时间
Between当且仅当请求时的时间Between配置的时间段时,才转发该请求一个带有时区的具体时间段
Cookie当且仅当请求时携带的Cookie名称及值与配置的名称及值相符时,才转发该请求Cookie的名称及值,支持使用正则表达式来匹配值
Header当且仅当请求时携带的Header名称及值与配置的名称及值相符时,才转发该请求Header的名称及值,支持使用正则表达式来匹配值
Host当且仅当请求时名为Host的Header的值与配置的值相符时,才转发该请求Host的值,支持配置多个且支持使用通配符
Method当且仅当请求时所使用的HTTP方法与配置的请求方法相符时,才转发该请求HTTP请求方法,例如GET、POST等
Path当且仅当请求时所访问的路径与配置的路径相匹配时,才转发该请求通配符、占位符或具体的接口路径,可以配置多个
Query当且仅当请求时所带有的参数名称与配置的参数名称相符时,才转发该请求参数名称和参数值(非必须),支持使用正则表达式对参数值进行匹配
RemoteAddr当且仅当请求时的IP地址与配置的IP地址相符时,才转发该请求IP地址或IP段
Weight当且仅当配置了权重,并且多余一个的情况时,根据配置的权重才可以调用具体的服务,一般调用的服务是同一个且负载部署的应用。数字,数据越大,被调用的次数越多。

注意:当predicates配置项只配置了一个Predicate且没有配置Path时,Path的默认值为/**

使用以下代码可以打印带有时区的当前时间,然后再自行修改成特定时间即可:

System.out.println(ZonedDateTime.now());

下面我们针对每个谓词进行分析
所有的谓词和过滤器是可以根据需求整合在一起使用的。

谓词 After

下面我们开始第一个谓词After ,该谓词后面需要拼接一个参数,即日期时间。该谓词匹配在指定日期时间之后发生的请求。匹配不到会返回404

为了演示方便,我们新建一个项目模块cloud-gateways-gateway-simple,并且为了使后面其他问题也使用这个项目,我们使用多profile不同配置构建项目,每个profile 是一种谓词类型。

导入依赖(Spring Cloud Gateway 是使用 netty+webflux 实现因此不需要再引入 spring-boot-starter-web包,这里我们引入Eureka,是为了直接通过Eureka 获取注册服务并发送请求。)

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

启动类

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

应用配置

为了方便,我们使用多profile的形式来配置应用,对于多profile不了解的话,可以去看一下spring和springboot基础。这里我们配置了端口和服务名以及当前使用的profile和注册中心。对于Spring Cloud 网关我们配置了在2020年5月5号14:20:14.455之后才可以访问的路由,请求路由的时候回拿当前时间和配置的时间做对比。成功的话我们会路由到ribbon-server服务上去。

使用以下代码可以打印带有时区的当前时间,然后再自行修改成特定时间即可:

System.out.println(ZonedDateTime.now());
server:
  port: 8699
spring:
  application:
    name: gateways-gateway
  profiles:
    active: after_route
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka/
---
spring:
  cloud:
    gateway:
      routes:
        - id: after_route #我们自定义的路由 ID,保持唯一
          uri: lb://ribbon-server #目标服务地址
          predicates: #路由条件,支持多个参数。
            - After=2020-05-05T14:20:14.455+08:00[Asia/Shanghai]
  profiles: after_route #过滤规则,支持多个参数,可以和predicates共用。

启动项目,访问127.0.0.1:8699/user/1

然后我们修改配置把时间配置加大,设置到2021年:

          predicates: #路由条件,支持多个参数。
            - After=2021-05-05T14:20:14.455+08:00[Asia/Shanghai]

再次启动访问。发现返回404,因为网关没有转发请求:

AfterRoutePredicateFactory 源码分析

@Override
public Predicate<ServerWebExchange> apply(Config config) {
   return new GatewayPredicate() {
      @Override
      public boolean test(ServerWebExchange serverWebExchange) {
         final ZonedDateTime now = ZonedDateTime.now();
         return now.isAfter(config.getDatetime());
      }

      @Override
      public String toString() {
         return String.format("After: %s", config.getDatetime());
      }
   };
}

可以看到仅仅是获取当前时间,然后和配置的时间作比较,之后交给NettyRoutingFilter执行

NettyRoutingFilter 源码

@Override
@SuppressWarnings("Duplicates")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
   URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);

   String scheme = requestUrl.getScheme();
   if (isAlreadyRouted(exchange)
         || (!"http&q