Spring Cloud系列—Gateway统一服务入口

上篇文章:

Spring Cloud系列---OpenFeign远程调用https://blog.csdn.net/sniper_fandc/article/details/149942580?fromshare=blogdetail&sharetype=blogdetail&sharerId=149942580&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link

目录

[1 API网关和Gateway](#1 API网关和Gateway)

[2 Predicate](#2 Predicate)

[3 Route Predicate Factories](#3 Route Predicate Factories)

[4 Gateway Filter Factories](#4 Gateway Filter Factories)

[4.1 GatewayFilter](#4.1 GatewayFilter)

[4.1.1 简单使用](#4.1.1 简单使用)

[4.1.2 常用配置项](#4.1.2 常用配置项)

[4.1.3 RequestRateLimiter限流](#4.1.3 RequestRateLimiter限流)

[4.2 GlobalFilter](#4.2 GlobalFilter)

[4.2.1 使用方法](#4.2.1 使用方法)

[4.2.2 过滤器执行顺序](#4.2.2 过滤器执行顺序)

[4.3 自定义Filter](#4.3 自定义Filter)

[4.3.1 自定义GatewayFilter](#4.3.1 自定义GatewayFilter)

[4.3.2 自定义GlobalFilter](#4.3.2 自定义GlobalFilter)


微服务如果没有一个统一的入口,就都会暴露在公共网络中,那么所有的请求都可以访问到这些微服务。因此微服务通常会进行权限设置来控制接口安全,但是每访问一个微服务都需要权限验证,就会很麻烦。因此如果为众多微服务设置一个统一的服务入口,请求到来时只经过该服务入口权限验证一次,后续访问不再验权,就会方便很多。

1 API网关和Gateway

API网关是一种服务,其作用就是提供一个后端服务的统一入口。

API网关主要有4种作用:

**1.权限控制:**对访问微服务的请求进行拦截,并进行权限验证。

**2.动态路由:**网关根据请求和规则动态地将请求路由到某个微服务。

**3.负载均衡:**如果路由的微服务实例有多个,就需要进行负载均衡。

**4.限流:**在流量高的场景中,网关可以根据配置来对流量进行限流,防止高流量使服务宕机。

Spring Cloud Gateway是基于Spring+SpringBoot开发的网关组件,用于替换掉已经停止维护的Zuul(Spring Cloud Netflix的网关组件),在性能方面,RPS(每秒请求数)大约高于Zuul的1.6倍。

2 Predicate

Predicate是Java 8提供的函数式编程接口,接收一个参数并返回布尔值,主要用来进行条件过滤、请求参数校验等。

要使用该接口就需要实现该接口的test()方法,比如实现判断字符串是否为空,为空则为true;否则为false:

java 复制代码
class StringPredicate implements Predicate<String> {

    @Override

    public boolean test(String str) {

        return str.isEmpty();

    }

}

public class PredictTest {

    @Test

    public static void main(String[] args) {

        Predicate<String> predicate = new StringPredicate();

        System.out.println(predicate.test(""));

        System.out.println(predicate.test("123456"));

    }

}

test()方法主要用来定义该Predicate的条件是什么,同时该接口还实现了and()、or()、negat()等等多种条件判断的方法(对应与、或、非):

java 复制代码
public class PredictTest {

    @Test

    public static void main(String[] args) {

        Predicate<String> predicate = new StringPredicate();

        System.out.println(predicate.test(""));

        System.out.println(predicate.test("123456"));

        //匿名内部类实现(是否非空)

        Predicate<String> predicate1 = new Predicate<String>() {

            @Override

            public boolean test(String s) {

                return !s.isEmpty();

            }

        };

        //lambda实现(是否包含字符a)

        Predicate<String> predicate2 = s -> s.contains("a");

        //lambda实现(是否是数字组成)

        Predicate<String> predicate3 = s -> s.chars().allMatch(Character::isDigit);

        System.out.println("是否非空&&是否包含字符a:" + predicate1.and(predicate2).test(""));

        System.out.println("是否非空&&是否包含字符a:" + predicate1.and(predicate2).test("cab"));

        System.out.println("是否包含字符a||是否由数字组成:" + predicate2.or(predicate3).test("12b"));

        System.out.println("是否包含字符a||是否由数字组成:" + predicate2.or(predicate3).test("123"));

        System.out.println("是否包含字符a||是否由数字组成:" + predicate2.or(predicate3).test("12a"));

        System.out.println("是否非空的非==是否为空:" + predicate1.negate().test(""));

    }

}

or和and的使用方法任意一个条件对象Predicate调用or(另一个条件对象Predicate)或and(另一个条件对象Predicate),然后在组合条件上调用test()。

3 Route Predicate Factories

Gateway的路由匹配基于Route Predicate Factories(路由断言工厂),在Gateway的配置文件中,通常会定义路由匹配规则,服务启动时Route Predicate Factories会识别路由匹配规则并转化为Predicate对象,然后由这些Predicate的条件组合来进行请求的路由。

由于Gateway本身是一个服务,因此创建项目:

引入依赖:

XML 复制代码
    <dependencies>

        <!--网关-->

        <dependency>

            <groupId>org.springframework.cloud</groupId>

            <artifactId>spring-cloud-starter-gateway</artifactId>

        </dependency>

        <!--基于nacos实现服务发现依赖-->

        <dependency>

            <groupId>com.alibaba.cloud</groupId>

            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>

        </dependency>

        <!--负载均衡-->

        <dependency>

            <groupId>org.springframework.cloud</groupId>

            <artifactId>spring-cloud-starter-loadbalancer</artifactId>

        </dependency>

        <dependency>

            <groupId>junit</groupId>

            <artifactId>junit</artifactId>

            <scope>test</scope>

        </dependency>

    </dependencies>

创建SpringBoot配置文件,输入如下内容:

bash 复制代码
server:

  port: 10030 # 网关端口

spring:

  application:

    name: gateway # 服务名称

  cloud:

    nacos:

      discovery:

        server-addr: 192.168.159.150:8848

    gateway:

      routes: # 网关路由配置

        - id: product-service #路由ID, 自定义, 唯一即可

          uri: lb://product-service #目标服务地址

          predicates: #路由条件

            - Path=/product/**

        - id: order-service

          uri: lb://order-service

          predicates:

            - Path=/order/**

            - After=2026-01-20T17:42:47.789-07:00[America/Denver]

其中predicates定义的就是路由条件,也是被路由断言工厂识别的内容。

Gateway定义了如下的路由断言工厂的路由条件:

bash 复制代码
predicates:

        - After=2017-01-20T17:42:47.789-07:00[America/Denver] #该时间以后请求允许通过(时间格式是Java的ZonedDateTime对象提供)

predicates:

        - Before=2017-01-20T17:42:47.789-07:00[America/Denver] #该时间之前请求允许通过(时间格式是Java的ZonedDateTime对象提供)

predicates:

        - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] #该时间段之间请求允许通过(时间格式是Java的ZonedDateTime对象提供)

predicates:

        - Cookie=chocolate, ch.p #请求中Cookie的key为chocolate,value匹配ch.p(正则表达式)允许通过

predicates:

        - Header=X-Request-Id, \d+ #请求Header中存在key为X-Request-Id,value匹配\d+(正则表达式)允许通过

predicates:

        - Host=**.somehost.org,**.anotherhost.org #请求Host匹配该值(正则)才允许通过,比如www.somehost.org允许通过

predicates:

        - Method=GET,POST #请求方法类型为GET或POST允许通过

predicates:

        - Path=/red/{segment},/blue/{segment} #请求这些指定的路径允许通过

predicates:

        - Query=green #请求包含参数green允许通过

predicates:

        - Query=red, gree. #请求包含参数red且值匹配gree.(正则)允许通过

predicates:

        - RemoteAddr=192.168.1.1/24 #请求的ip在指定范围内允许通过,比如192.168.1.10允许通过

注意:如果某个服务配置了多个路由规则,那这些规则之间的关系是与关系,即只有路由规则都满足了,请求才会通过。

当路由匹配规则如下时,当前时间在2026年前,不可以访问该路径:

bash 复制代码
- After=2026-01-20T17:42:47.789-07:00[America/Denver]

当路由匹配规则修改为如下格式,此时就可以访问该路径:

bash 复制代码
- After=2024-01-20T17:42:47.789-07:00[America/Denver]

4 Gateway Filter Factories

Gateway Filter Factories(网关过滤器工厂)是对请求处理前和处理后的一些逻辑处理。从Filter处理时机分为Pre类型和Post类型:

**Pre类型:**请求被后端服务器处理前(路由后,处理前),过滤器所做的一些事,比如鉴权和限流。

**Post类型:**请求被后端服务器处理后,返回客户端前所做的一些事。

从Filter作用范围分为GatewayFilter和GlobalFilter:

**GatewayFilter:**作用在单个路由或分组路由上。

**GlobalFilter:**作用在所有的路由上,对所有请求生效。

4.1 GatewayFilter

4.1.1 简单使用

添加配置项:

bash 复制代码
server:

  port: 10030 # 网关端口

spring:

  application:

    name: gateway # 服务名称

  cloud:

    nacos:

      discovery:

        server-addr: 192.168.159.150:8848

    gateway:

      routes: # 网关路由配置

        - id: product-service #路由ID, 自定义, 唯一即可

          uri: lb://product-service #目标服务地址

          predicates: #路由条件

            - Path=/product/**

        - id: order-service

          uri: lb://order-service

          predicates:

            - Path=/order/**

            - After=2024-01-20T17:42:47.789-07:00[America/Denver]

          filters:

            - AddRequestParameter=userName, zhangsan #Pre类型:在请求头添加参数userName=zhangsan 

在order-service中接收该参数:

java 复制代码
    @RequestMapping("/pre")

    public String pre(@RequestParam("userName") String userName) {

        return "接收到请求中pre类型过滤器添加的参数userName=" + userName;

    }

重启服务,访问接口,发现pre类型的过滤器在请求中添加的参数成功接收到:

4.1.2 常用配置项

filters配置项Gateway提供了很多,常见如下:

bash 复制代码
- AddRequestHeader=X-Request-red, blue #请求头添加key-value

- AddRequestParameter=red, blue #请求添加参数

- AddResponseHeader=X-Response-Red, Blue #响应头添加key-value

- name: RequestRateLimiter #对当前网关进行限流(Redis的令牌桶限流算法),被限流请求返回429-Too Many Requests的HTTP响应状态

  args:

    redis-rate-limiter.replenishRate: 10 #令牌填充速率(每秒)

    redis-rate-limiter.burstCapacity: 20 #令牌桶令牌最大容量

    redis-rate-limiter.requestedTokens: 1 #一个请求能取走的令牌

- RemoveRequestHeader=X-Request-Foo #移除请求头的X-Request-Foo键

- RemoveResponseHeader=X-Response-Foo #移除响应头的X-Request-Foo键

- RemoveRequestParameter=red #移除请求的red参数

- name: Retry #针对HTTP响应的状态码进行重试

  args:

    retries: 3 #首次响应失败后的请求重试次数

    statuses: BAD_GATEWAY #org.springframework.http.HttpStatus中的一种失败状态码

    methods: GET,POST #重试的请求方法

    backoff: #重试退避时间算法

      firstBackoff: 10ms #firstBackoff*factor^n为首次失败后重试的退避时间,n表示第几次失败

      maxBackoff: 50ms #配置最大退避时间间隔

      factor: 2 #退避时间间隔计算因子

      basedOnPreviousValue: false #是否使用prevBackoff * factor计算退避时间

- name: RequestSize #请求最大的大小(字节)

  args:

    maxSize: 5000000

# 默认过滤器,对全部路由的过滤都生效

spring:

  cloud:

    gateway:

      default-filters:

      - AddResponseHeader=X-Response-Default-Red, Default-Blue

      - PrefixPath=/httpbin

4.1.3 RequestRateLimiter 限流

其中,对于上述的限流机制,采用的RedisRateLimiter限流算法,常见的四种限流算法如下:

固定窗口: 假设每分钟窗口最多容纳1000个请求,每分钟一个固定的窗口进行限流。缺点: 假设1-58秒无请求,59-60秒有1000个请求,60-61秒有1000个请求,按照固定窗口均未达到上限。但是在2秒内共2000个请求,请求量激增容易会使服务宕机。适用于短信验证码等场景(每分钟内短信不得发送多次验证码)

滑动窗口: 假设窗口最多容纳1000个请求,窗口不再固定,而是定期滑动。解决了固定窗口的临界请求问题。****缺点:****定期滑动时间难以确定,时间长退化为固定窗口,时间短可能比较消耗资源。Sentinel(Spring Cloud Sentinel,熔断和限流的工具)使用的就是滑动窗口算法来限流。

漏桶算法: 假设漏桶最多容纳1000个请求,这类似MQ,生产者向漏桶(队列)放入请求,消费者以一定速率取走请求。****缺点:****如果取走请求的速率比较慢,而请求流量激增,漏桶满了就会丢失请求,消费者也无法及时处理请求。

****令牌桶算法:****假设令牌桶最多容纳1000个令牌,每个请求要先取走令牌桶一定数量的令牌后就可以离开然后被服务端处理,而令牌桶又会以一定速率填充令牌。令牌桶即可以及时处理请求,又可以处理短时间内流量激增的情况。

4.2 GlobalFilter

4.2.1 使用方法

GlobalFilter是Gateway的全局过滤器,也是用来条件过滤的,不同的是GlobalFilter应用于全局所有请求,而GatewayFilter只应用于对应设置的路由。GlobalFilter通常用于做安全工作、性能监控和日志记录等全局性工作。

Gateway提供了多种GlobalFilter,比如Gateway Metrics Filter(网关指标,提供监控指标)、LoadBalancer Client Filter(负载均衡)等等。

假设要使用Gateway Metrics Filter,使用方法首先要添加依赖:

XML 复制代码
<dependency>

    <groupId>org.springframework.boot</groupId>

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

</dependency>

添加配置信息:

bash 复制代码
spring:

  cloud:

    gateway:

      metrics:

        enabled: true #开启监控指标

management: #更详细信息

  endpoints:

    web:

      exposure:

        include: "*" #开启所有Web监控

  endpoint:

    health:

      show-details: always #显示信息更详细

    shutdown:

      enabled: true

访问http://127.0.0.1:10030/actuator后,就会显示一些JSON格式的URL信息,根据需求访问对应的URL就可以显示对应的信息。

4.2.2 过滤器执行顺序

请求路由后,网关会把所有类型的Filter合并到一个过滤器链中,并在该过滤器链进行排序。

排序的原则是order值(int型),每个Filter都有order值,默认为0。值越小越优先执行。

而order值通过Filter实现Order接口或添加@Order注解设置。Spring Cloud Gateway的Filter由Spring指定,用户还可以自定义Filter,由用户指定。

当order一样时,pre类型执行顺序是GatewayFilter(defaultFilter>其它的路由Filter)>GlobalFilter。post类型执行顺序是GlobalFilter>GatewayFilter。类似拦截器一样,是一个环状。

4.3 自定义Filter

4.3.1 自定义GatewayFilter

继承AbstractGatewayFilterFactory抽象类,实现Ordered接口:

java 复制代码
//类名必须以xxxGatewayFilterFactory

//过滤器名称则是xxx,即去掉GatewayFilterFactory

//泛型中填入接收配置文件中参数的对象(自定义的CustomConfig)

//同时要实现Order接口指定优先级

@Component

public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomConfig> implements Ordered {

    //构造方法

    public CustomGatewayFilterFactory() {

        super(CustomConfig.class);

    }



    @Override

    public GatewayFilter apply(CustomConfig config) {

        return new GatewayFilter() {

            /**

             * ServerWebExchange: HTTP请求-响应交互的契约, 提供对HTTP请求和响应的访问, 服务器端请求属性, 请求实例,响应实例等, 类似Context角色

             * GatewayFilterChain: 过滤器链

             * Mono: Reactor核心类, 数据流发布者, Mono最多只触发一个事件, 所以可以把Mono用于在异步任务完成时发出通知.

             * Mono.fromRunnable: 创建一个包含Runnable元素的数据流

             * chain.filter(exchange): 执行请求

             * then()执行请求后执行的逻辑

             */

            @Override

            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

                System.out.println("Pre类型自定义GatewayFilter执行:" + config);

                return chain.filter(exchange).then(Mono.fromRunnable(() -> {

                    System.out.println("Post类型自定义GatewayFilter执行:" + config);

                }));

            }

        };

    }

    //指定优先级最低

    @Override

    public int getOrder() {

        return Ordered.LOWEST_PRECEDENCE;

    }

}

在指定服务下面添加自定义的过滤器:

bash 复制代码
filters:

  - AddRequestParameter=userName, zhangsan

  - name: Custom

    args:

      name: GatewayFilterTest

这里args的name对应CustomConfig类的属性:

java 复制代码
//专门用于接收配置文件的filters:-name:args:name的参数

@Data

public class CustomConfig {

    private String name;

}

4.3.2 自定义GlobalFilter

实现GlobalFilter和Ordered接口:

java 复制代码
@Component

public class CustomGlobalFilter implements GlobalFilter, Ordered {


    @Override

    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        System.out.println("Pre类型自定义GlobalFilter执行");

        return chain.filter(exchange).then(Mono.fromRunnable(() -> {

            System.out.println("Post类型自定义GlobalFilter执行");

        }));

    }


    @Override

    public int getOrder() {

        return Ordered.LOWEST_PRECEDENCE;

    }

}

重新运行程序,通过网关端口访问order-service的接口http://127.0.0.1:10030/order/1,结果如下:

可以发现,在同样的order值下,pre类型的过滤器是先执行GatewayFilter再执行GlobalFilter;而post类型是先执行GlobalFilter再执行GatewayFilter。整个执行顺序构成类似拦截器的环状执行(从外到里再从里到外),并且自定义GatewayFilter也通过配置文件yml接收到参数。

下篇文章: