Spring Cloud - 05 (Gateway)

目录

服务网关-Gateway

服务网关是微服务的第一道关卡,目前Nginx是应用最广泛的反向代理技术,在各个大厂的核心业务系统中都有大量应用,不过Nginx可不是使用Java来配置的,使用和配置Nginx需要掌握它的语法树。Spring Cloud则为广大的Java技术人员提供了更加“编程友好”的方式来构建网关层,那就是Gateway网关层组件。我们可以通过Java代码或者是yml配置文件的方式编写自己的路由规则,并通过内置过滤器或自定义过滤器来实现复杂的业务需求(比如在网关层做令牌验证)。Gateway本身也集成了强大的限流功能,结合使用Redis+SpEL表达式,可以对业务系统进行精准限流。

Gateway可以做什么?
Spring Cloud - 05 (Gateway)
在Spring Cloud中,Gateway是借助Eureka的服务发现机制来实现服务寻址的,负载均衡则依靠Ribbon。

路由功能

Gateway中可以定义很多个Route,一个Route就是一套包含完整转发规则的路由,主要由三部分组成:
Spring Cloud - 05 (Gateway)

  • 断言(Predicate)集合:断言是路由处理的第一个环节,它是路由的匹配规则,它决定了一个网络请求是否可以匹配给当前路由来处理。之所以它是一个集合的原因是我们可以给一个路由添加多个断言,当每个断言都匹配成功以后才算过了路由的第一关。

  • 过滤器(filter)集合:如果请求通过了前面的断言匹配,那就表示它被当前路由正式接手了,接下来这个请求就要经过一系列的过滤器集合。过滤器的功能就是八仙过海各显神通了,可以对当前请求做一系列的操作,比如说权限验证,或者将其他非业务性校验的规则提到网关过滤器这一层。在过滤器这一层依然可以通过修改Response里的Status Code达到中断效果,比如对鉴权失败的访问请求设置Status Code为403之后中断操作。

  • URI:如果请求顺利通过过滤器的处理,接下来就到了最后一步,那就是转发请求。URI是统一资源标识符,它可以是一个具体的网址,也可以是IP+端口的组合,或者是Eureka中注册的服务名称。

关于负载均衡: 对最后一步寻址来说,如果采用基于Eureka的服务发现机制,那么在Gateway的转发过程中可以采用服务注册名的方式来调用,后台会借助Ribbon实现负载均衡(可以为某个服务指定具体的负载均衡策略),其配置方式如:lb://FEIGN-SERVICE-PROVIDER/,前面的lb就是指代Ribbon作为LoadBalancer。

常用断言(Predicate)介绍

一个请求在抵达网关层后,首先就要进行断言匹配,在满足所有断言之后才会进入Filter阶段。

1、路径匹配:
Path断言是最常用的一个断言请求,几乎所有路由都要使用到它,我们来看一下它的例子

.route(r -> r.path("/gateway/**")
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)
.route(r -> r.path("/baidu")
             .uri("http://baidu.com:80/")
)

Path断言的使用非常简单,就像我们在Controller中配置@RequestPath的方式一样,在Path断言中填上一段URL匹配规则,当实际请求的URL和断言中的规则相匹配的时候,就下发到该路由中URI指定的地址,这个地址可以是一个具体的HTTP地址,也可以是Eureka中注册的服务名称。在上面的例子中,如果我们访问“/gateway/test”,这个路径将匹配到第一个路由。

2、M:ethod断言:
这个断言是专门验证HTTP Method的,在下面的例子中,我们把Method断言和Path断言通过一个and连接符合并起来,共同作用于路由判断,当我们访问“/gateway/sample”并且HTTP Method是GET的时候,将适配下面的路由

.route(r -> r.path("/gateway/**")
             .and().method(HttpMethod.GET)
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

3、RequestParam匹配:
请求断言也是在业务中经常使用的,它会从ServerHttpRequest中的Parameters列表中查询指定的属性,有如下两种不同的使用方式

.route(r -> r.path("/gateway/**")
             .and().method(HttpMethod.GET)
             .and().query("name", "test")
             .and().query("age")
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)
  • 属性名验证:query("age"),此时断言只会验证QueryPrameters列表中是否包含了一个叫age的属性,并不会验证它的值;
  • 属性值验证:query("name", "test"),它不仅会验证name属性是否存在,还会验证它的值是不是和断言相匹配,比如当前的断言会验证请求参数中的name属性值是不是test,第二个参数实际上是一个用作模式匹配的正则表达式。

4、Header断言:
这个断言会检查Header中是否包含了响应的属性,通常可以用来验证请求是否携带了访问令牌,比如如下设置:

.route(r -> r.path("/gateway/**")
             .and().header("Authorization")
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

上面的断言指定了Header中必须包含一个Authorization属性,Header断言和Query断言一样,也可以通过传入两个参数的形式对属性值进行检查。

5、Cookie断言:
顾名思义,Cookie验证的是Cookie中保存的信息,Cookie断言和上面介绍的两种断言使用方式大同小异,唯一的不同是它必须连同属性值一同验证,不能单独只验证属性是否存在,示例如下:

.route(r -> r.path("/gateway/**")
             .and().cookie("name", "test")
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

6、时间片匹配:
时间匹配有三种模式,分别是Before、After和Between,这些断言指定了在什么时间范围内路由才会生效

.route(r -> r.path("/gateway/**")
             .and().before(ZonedDateTime.now().plusMinutes(1))
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

以Before断言为例,它接受的是一个ZonedDateTime参数,用来表示生效的时间。比如上面的例子中我们使用了ZonedDateTime.now().plusMinutes(1)表示当前时间的后一分钟,由于路由的规则是在项目启动时加载的,那么这里的当前时间也就是项目加载完成的时间,因此该路由的有效时间就是服务启动后的一分钟以内。

过滤器(filter)的实现方式

在Gateway中实现一个过滤器非常简单,只要实现GatewayFilter接口的默认方法就好了

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // 随意发挥,例如此处可以写鉴权的逻辑
    return chain.filter(exchange);
}
  • ServerWebExchange:这是Spring封装的HTTP request-response交互协议,从中我们可以获取request和response中的各种请求参数,也可以向其中添加内容。
  • GatewayFilterChain:它是过滤器的调用链,在方法结束的时候我们需要将exchange对象传入调用链中的下一个对象。

过滤器排序:
在Gateway中我们可以通过实现org.springframework.core.Ordered接口,来给过滤器指定执行顺序,比如下面的代码实现了Ordered接口方法,将过滤器执行顺序设置为0:

@Override
public int getOrder() {
    return 0;
}

过滤器示例

Gateway的组件库相当丰富,有二十多个过滤器,下面随便展示几个:

1、Header过滤器:
这个系列有很多组过滤器,AddRequestHeader和AddResponseHeader,分别向Request和Response里加入指定Header。相应的RemoveRequestHeader和RemoveResponseHeader分别做移除操作,用法也很简单:

.filters(f -> f.addResponseHeader("who", "gateway-header"))

上面的例子会向header中添加一个who的属性,对应的值是gateway-header。

2、StripPrefix过滤器:
这是个比较常用的过滤器,它的作用是去掉部分URL路径。比如我们的过滤器配置如下:

.route(r -> r.path("/gateway-test/**")
             .filters(f -> f.stripPrefix(1))
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

假如HTTP请求访问的是/gateway-test/sample/update,如果没有StripPrefix过滤器,那么转发到FEIGN-SERVICE-PROVIDER服务的访问路径也是一样的。当我们添加了这个过滤器之后,Gateway就会根据“stripPrefix(1)”中的值截取URL中的路径,比如这里我们设置的是1,那么就去掉一个前缀,最终发送给后台服务的路径变成了“/sample/update”。

3、PrefixPath过滤器:
它和StripPrefix的作用是完全相反的,会在请求路径的前面加入前缀

.route(r -> r.path("/gateway-test/**")
             .filters(f -> f.prefixPath("go"))
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

比如说我们访问“/gateway-test/sample”的时候,上面例子中配置的过滤器就会把请求发送到“/go/gateway-test/sample”。

4、RedirectTo过滤器:
它可以把收到特定状态码的请求重定向到一个指定网址:

.filters(f -> f.redirect(302, "https://www.imooc.com/"))

上面的例子接收HTTP status code和URL两个参数,如果请求结果是404,则重定向到第二个参数指定的页面,这个功能也可以做统一异常处理,将Unauthorized或Forbidden请求重定向到登录页面。

5、SaveSession过滤器:
我们知道微服务是无状态的会话,所以大多都不依赖session机制,但是如果你有分布式session的需求,比如说某些功能是基于spring-session和spring-security来实现的,那么这个过滤器或许对你有用,它在调用服务之前都会强制保存session

.filters(f -> f.saveSession())

代码示例

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-cloud-demo</artifactId>
        <groupId>com.jinsh</groupId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>gateway-sample</artifactId>

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

启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {

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

配置文件

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 这个设置为true,它就会去注册中心拉取所有服务的路由规则
          lower-case-service-id: true # 路径改为小写
      # yml配置路由规则
      routes:
        - id: feigncClient
          uri: lb://FEIGN-CLIENT  # lb负载均衡,FEIGN-CLIENT服务
          predicates:
            - Path=/yml/** # 断言规则,/yml/**的请求将匹配到FEIGN-CLIENT
          filters:
            - StripPrefix=1 # 去除了第一个路径 /yml/sayHi -> /sayHi

eureka:
  client:
    service-url:
      defaultZone: http://localhost:20000/eureka/

server:
  port: 63001

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

自定义过滤器实现接口计时功能

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 自定义过滤器实现接口计时功能
 */
@Slf4j
@Component
// 实现GlobalFilter接口,就可以成为全局过滤器
public class TimerFilter implements GatewayFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        StopWatch timer = new StopWatch();
        timer.start(exchange.getRequest().getURI().getRawPath());
        return chain.filter(exchange).then(
                Mono.fromRunnable(() -> {
                    timer.stop();
                    log.info(timer.prettyPrint());
                })
        );
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

java代码方式配置路由规则

=import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;

@Configuration
public class GatewayConfiguration {

    @Autowired
    private TimerFilter timerFilter;

    /**
     * java代码配置路由规则
     * @param builder
     * @return
     */
    @Bean
    @Order
    public RouteLocator customizedRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/java/**")
                        .and().method(HttpMethod.GET)
                        .and().header("name") // header里面需要包含name属性
                        .filters(f -> f.stripPrefix(1)
                                .addRequestParameter("java-param", "gateway-config") // 在Request中添加参数
                                .filter(timerFilter)
                        )
                        .uri("lb://FEIGN-CLIENT")
                ).build();
    }
}

GateWay 限流

这里采用令牌桶计数的方式做限流,下面直接上操作

准备工作:
我们的Best Practice是基于Redis来实现限流,因此要保证本地启动了Redis服务。同时将下列配置加入到Gateway的配置文件中:

spring:
  application:
    name: gateway-service
  redis:
      host: localhost
      port: 6379
      database: 0

这里是配置Redis连接信息的,假如你不配置的话,Gateway也会尝试用默认配置项来连接Redis。但如果你在Redis配置信息中提供了错误的IP或者Port的话,调用方法时依然会成功,不过限流功能就失效了,因为底层的Netty服务无法连接到Redis,也就无法提供限流支持。但Gateway为了保证服务可用性,限流功能的异常并不会阻碍正常的方法调用。

Key Resolver:
Gateway的限流组件要求定义一个Key Resolver用来对每次路由请求生成一个Key,这个Key就是一个限流分组的标识,每个Key相当于一个令牌桶。假如我们限定了一个服务每秒只能被调用3次,这个限制会对不同的Key单独计数,我们把调用方机器的Host Name作为限流Key,那么来自同一台机器的调用将落到同一个Key下面,也就是说在这个场景下,每台机器都独立计算单位时间调用量。
创建Key Resolver的方式很简单:

@Bean
public KeyResolver remoteAddrKeyResolver() {
    return exchange -> Mono.just(
        exchange.getRequest().getRemoteAddress().getHostName());
}

上面的例子创建了基于Host Name的令牌生成器,我们可以根据自己的业务来选择合适的Key,比如说可以在接口层面做限流(使用接口的Path作为Key),还可以从Request中提取业务字段作为Key(比如用户ID等)。

配置文件方式配置过滤器:

spring:
  cloud:
    gateway:
      routes:
      - id:  feignapi
        uri: lb://FEIGN-SERVICE-PROVIDER
        predicates:
        - Path=/feign-api/**
        filters:
        - StripPrefix=1
        - name: RequestRateLimiter
          args:
            key-resolver: '#{@remoteAddrKeyResolver}'  # 这里注入的就是在上一步中我们定义的Key Resolver,它使用SpEL表达式从Spring上下文中获取指定Bean
            redis-rate-limiter.replenishRate: 10  # 令牌桶每秒的平均填充速度
            redis-rate-limiter.burstCapacity: 20  # 令牌桶总量

java代码方式配置过滤器:

package com.jinsh;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;

/**
 * 限流配置
 */
@Configuration
public class RedisLimitConfiguration {

    // HostAddressKey
    @Bean
    @Primary
    public KeyResolver remoteAddressKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest()
                .getRemoteAddress()
                .getAddress()
                .getHostAddress()
        );
    }

    @Bean("redisLimiter")
    @Primary
    public RedisRateLimiter redisLimiter() {
        // 10:令牌桶每秒的平均填充速度,20:令牌桶总量
        return new RedisRateLimiter(10, 20);
    }
}

路由规则

@Configuration
public class GatewayConfiguration {

    @Autowired
    private KeyResolver hostNameResolver;

    @Autowired
    @Qualifier("redisLimiter")
    private RateLimiter rateLimiter;

    /**
     * java代码配置路由规则
     * @param builder
     * @return
     */
    @Bean
    @Order
    public RouteLocator customizedRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/java/**")
                        .filters(f -> f.requestRateLimiter(c -> {
                            c.setKeyResolver(hostNameResolver);
                            c.setRateLimiter(rateLimiter);
                        }))
                        .uri("lb://FEIGN-CLIENT")
                ).build();
    }
}
上一篇:《微服务架构设计模式》之外部API模式


下一篇:开发者社区精选直播合集(二十)| Python入门及大数据应用