Spring Cloud框架学习-Spring Cloud Gateway

1. 简介

一个完整的微服务架构,由一系列独立运行的微服务组成,每个微服务负责完成不同的业务功能,这就需要API网关统一管理微服务提供的接口了。API网关的主要作用如下:

  • 请求接入:管理所有接入请求,作为所有微服务API接口的请求入口
  • 限流:流量控制,错峰流控,可以定义多种限流规则
  • 安全管理:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用)检验,黑名单限制(非法调用的限制)
  • 统一管理:提供统一的监控工具(记录请求响应数据,api耗时分析,性能监控),统一的日志记录,统一配置管理工具,Swagger工具等等

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

Spring Cloud Gateway 具有如下特性:

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

Spring Cloud Gateway的重要的三大概念:

  • Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由;
  • Predicate(断言):指的是Java 8 的 Function Predicate。输入类型是Spring框架中的ServerWebExchange。这使开发人员可以匹配HTTP请求中的所有内容,例如请求头或请求参数。如果请求与断言相匹配,则进行路由;
  • Filter(过滤器):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前后对请求进行修改。

Spring Cloud Gateway工作原理示例图:
在这里插入图片描述
客户端向 Spring Cloud Gateway 发出请求,如果请求与网关程序定义的路由匹配,则该请求就会被发送到网关 Web 处理程序,此时处理程序运行特定的请求过滤器链。

过滤器之间用虚线分开的原因是过滤器可能会在发送代理请求的之前和之后执行逻辑。所有 pre 过滤器逻辑先执行,然后执行代理请求;代理请求完成后,再执行 post 过滤器逻辑。

和Zuul的对比:

  1. Zuul 是 Netflix 公司的开源项目,Spring Cloud Gateway 是 Spring 家族中的产品,可以和Spring 家族中的其他组件更好的融合。
  2. Zuul 1.x基于Servlet 2. 5使用阻塞架构,使用的是阻塞式的 API,不支持长连接,例如Websocket。
  3. Spring Cloud Gateway 支持限流。
  4. Spring Cloud Gateway 基于 Netty 来开发,实现了异步和非阻塞,占用资源更小,性能强于Zuul。

2. 基本用法

Spring Cloud Gateway 支持两种不同的配置路由的方式:编码式和yml文件配置

2.1 编码式配置路由

创建Spring Boot项目,添加Spring Cloud Gateway依赖:

	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-gateway</artifactId>
			<exclusions>
				<exclusion>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-starter-hateoas</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

项目创建后,通过配置RouteLocator实现请求转发

@SpringBootApplication
public class DemoApplication {
    
    

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

	@Bean
    RouteLocator routeLocator(RouteLocatorBuilder builder){
    
    
        return  builder.routes().route("test_route", r -> r.path("/get").uri("http://httpbin.org")).build();
    }
}

配置完成后,重启项目,请求http://localhost:8080/get

在这里插入图片描述

2.2 配置文件配置路由

对应properties配置示例

spring.cloud.gateway.routes[0].id=test_route
spring.cloud.gateway.routes[0].uri=http://httpbin.org
spring.cloud.gateway.routes[0].predicates[0]=Path=/get

对应YML配置示例

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
        uri: http://httpbin.org
        predicates:
          - Path=/get

2.3 服务化配置

在Gateway项目上添加依赖,注册Gateway到Eureka上。

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

配置文件中添加注册中心配置,并开启自动代理

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: http://httpbin.org
          predicates:
            - Path=/get
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能
#         lower-case-service-id:true#使用小写服务名,默认是大写
  application:
    name: gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1111/eureka
logging:
  level:
    org.springframework.cloud.gateway: debug
在这里插入代码片

启动Eureka服务,启动Gateway和provider(服务提供者)服务,注册Gateway和provider服务到Eureka注册中心上。
在这里插入图片描述
provider服务的端口号为1113,Gateway的端口为8080.下面通过Gateway访问provider服务的hello接口

在这里插入图片描述

3. Predicate断言

Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。Spring Cloud Gateway包括许多内置的Route Predicate工厂( 包括时间匹配,请求方式匹配,请求路径匹配,参数匹配等)。所有这些Predicate都与HTTP请求的不同属性匹配。多个Route Predicate工厂可以进行组合,下面我们来介绍一些常用的Route Predicate。

3.1 时间匹配

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: http://httpbin.org
          predicates:
            - After=2021-01-01T01:01:01+08:00[Asia/Shanghai]

表示请求时间在 2021-01-01T01:01:01+08:00[Asia/Shanghai] 时间之后才会被路由。

除了 After 之外,还有两个关键字:

  • Before,表示在某个时间点之前的请求匹配到该路由
  • Between,表示在两个时间点之间的请求匹配到该路由,两个时间点用 , 隔开

3.2 请求方式匹配

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: http://httpbin.org
          predicates:
            - Method=GET

配置表示只给GET请求路由

3.3 请求路径匹配

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: http://httpbin.org
          predicates:
            - Path=/2019/0612/{
    
    segment}

表示路径满足 /2019/0612/ 这个规则,都会被进行转发

3.4 参数匹配

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: http://httpbin.org
          predicates:
            - Query=name

表示请求中一定要有 name 参数才会进行转发,否则不会进行转发,也可以指定参数和参数的值。

例如参数的key为name,value必须要以java开始:

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: http://httpbin.org
          predicates:
            - Query=name,java.*

上面多种匹配方式也可以组合使用

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: http://httpbin.org
          predicates:
            - Query=name,java.*
            - Path=/2019/0612/{
    
    segment}
            - Method=GET

3.5 Cookie匹配

带有指定Cookie的请求会匹配该路由。

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: http://httpbin.org
          predicates:
            - Cookie=username

3.6 请求头Header匹配

带有指定Cookie的请求会匹配该路由。如下带有请求头为"X-Request-Id:test123"的请求可以匹配该路由。

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: http://httpbin.org
          predicates:
            - Header=X-Request-Id

3.7 Host主机匹配

带有指定Host的请求会匹配该路由。如下带有Host:www.baidu.com的请求会匹配到该路由

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: http://httpbin.org
          predicates:
            - Host=www.**.com

3.8 远程地址RemoteAddr匹配

从指定远程地址发起的请求可以匹配该路由。

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: http://httpbin.org
          predicates:
            - RemoteAddr=192.168.1.1/24

3.8 权重匹配

使用权重来路由相应请求,以下表示有80%的请求会被路由到localhost:8201,20%会被路由到localhost:8202。

spring:
  cloud:
    gateway:
      routes:
      - id:weight_high
        uri:http://localhost:8201
        predicates:
        - Weight=group1,8
      - id: weight_low
        uri: http://localhost:8202
        predicates:
        - Weight=group1,2

4. Filter过滤器

Spring Cloud Gateway中的过滤器分为两大类:全局过滤器GlobalFilter和网关过滤器GatewayFilter。

全局过滤器GlobalFilter,实现GlobalFilter接口,Spring Cloud Gateway默认提供了以下实现类。
在这里插入图片描述

在这里插入图片描述

全局过滤器GlobalFilter的详细介绍:spring-cloud-gateway之GlobalFilter

网关过滤器GatewayFilter,实现GatewayFilter接口,Spring Cloud Gateway默认提供了以下实现类。
在这里插入图片描述

在这里插入图片描述
下面介绍常用的几个网关过滤器GatewayFilter。

4.1 AddRequestParameter GatewayFilter

使用AddRequestParameter过滤器,给/test_get请求自动额外添加参数

spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: lb://provider
          filters:
            - AddRequestParameter=name,robot
          predicates:
            - Path=/test_get

uri: lb://provider的作用是开启负载均衡功能,provider服务提供接口/test_get

在这里插入图片描述
浏览器请求Gateway,实现转发到provider的test_get接口,并自动添加上参数。
在这里插入图片描述
请求test_get接口,已有参数的话会额外加上过滤器配置的参数。
在这里插入图片描述

4.2 StripPrefix GatewayFilter

StripPrefix GatewayFilter是对指定数量的路径前缀进行去除的过滤器。

spring:
  cloud:
    gateway:
      routes:
      - id: test_route
        uri: http://localhost:8201
        predicates:
        - Path=/user-service/**
        filters:
        - StripPrefix=2

StripPrefix=2配置会把以/user-service/开头的请求的路径去除两位,即请求

http://localhost:9201/user-service/a/user/1

相当于请求该地址:

http://localhost:8201/user/1

4.3 PrefixPath GatewayFilter

PrefixPath GatewayFilter会对原有路径进行增加操作的过滤器。

spring:
  cloud:
    gateway:
      routes:
      - id: test_route
        uri: http://localhost:8201
        predicates:
        - Path=/user-service/**
        filters:
        - PrefixPath=/user

配置请求前缀为/user-service/的请求添加/user路径前缀,即请求

http://localhost:9201/user-service/1

相当于请求该地址:

http://localhost:8201/user-service/user/1

4.4 Hystrix GatewayFilter

Hystrix 过滤器允许我们将断路器功能添加到网关路由中,使服务免受级联故障的影响,并提供服务降级处理。

首先在pom.xml中添加Hystrix的相关依赖,开启断路器功能

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

添加相关服务降级的处理类

@RestController
public class FallbackController {
    
    


    @GetMapping("/fallback")
    public Object fallback() {
    
    
        Map<String,Object> result = new HashMap<>();
        result.put("data",null);
        result.put("message","get request fallback!");
        result.put("code",500);
        return result;
    }
}

在配置文件中添加相关配置:当路由出错时会转发到服务降级处理的控制器上。

spring:
  cloud:
    gateway:
      routes:
        - id: hystrix_route
          uri: http://localhost:8201
          predicates:
            - Method=GET
          filters:
            - name: Hystrix
              args:
                name: fallback
                fallbackUri: forward:/fallback

请求http://localhost:8080/1发现已经返回了服务降级的处理信息,因为http://localhost:8201/没有提供该接口。
在这里插入图片描述

4.5 RequestRateLimiter GatewayFilter

RequestRateLimiter 过滤器可以用于限流,使用RateLimiter实现来确定是否允许当前请求继续进行,如果请求太频繁默认会返回HTTP 429 - Too Many Requests响应。

在pom.xml中添加相关依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

添加限流策略的配置类,提供策略ipKeyResolver根据访问IP进行限流。

@Configuration
public class RedisRateLimiterConfig {
    
    

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

在yml配置文件中添加Redis和RequestRateLimiter的配置,这里配置对所有的GET请求都进行了按IP来限流的操作。

spring:
  redis:
    host: localhost
    password: 123
    port: 6379
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: lb://provider
          filters:
            - AddRequestParameter=name,robot
          predicates:
            - Path=/test_get
        - id: hystrix_route
          uri: http://localhost:8201
          predicates:
            - Method=GET
          filters:
            - name: Hystrix
              args:
                name: fallbackcmd
                fallbackUri: forward:/fallback
        - id: requestratelimiter_route
          uri: http://localhost:1113
          predicates:
            - Method=GET
          filters:
            - name: RequestRateLimiter
              args:
                #每秒允许处理的请求数量
                redis-rate-limiter.replenishRate: 1
                #每秒最大处理的请求数量
                redis-rate-limiter.burstCapacity: 2
                #限流策略,对应策略的Bean
                key-resolver: "#{@ipKeyResolver}"

      discovery:
        locator:
          enabled: true # 开启自动代理
  application:
    name: gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1111/eureka
logging:
  level:
    org.springframework.cloud.gateway: debug

RequestRateLimiter使用的算法是令牌桶算法,令牌桶算法介绍

令牌桶算法

RequestRateLimiter的配置说明:

  • filter名称必须是“RequestRateLimiter”
  • redis-rate-limiter.replenishRate:允许用户每秒处理多少个请求。这是令牌桶的填充速率。
  • redis-rate-limiter.burstCapacity:令牌桶的容量,允许在一秒钟内完成的最大请求数。这是令牌桶可以保存的令牌数。
  • redis-rate-limiter.requestedTokens: 是每个请求消耗多少个令牌,默认是1。
  • key-resolver:使用SpEL表达式按名称引用bean。

下面进行测试,启动provider服务,注册到注册中心http://localhost:1111/eureka上,提供hello接口。频繁多次浏览器访问:http://localhost:8080/hello ,会返回状态码为429的错误;
在这里插入图片描述
查看Redis中的Key:
在这里插入图片描述

  • 大括号中就是限流Key,这里我们测试的是ip限流,所以key是IP,本地访问的就是0:0:0:0:0:0:0:1/

  • timestamp:存储的是当前时间的秒数,也就是System.currentTimeMillis() / 1000或者Instant.now().getEpochSecond()
    在这里插入图片描述

  • tokens:存储的是当前这秒钟的对应的可用的令牌数量
    在这里插入图片描述

我们不仅可以通过ip进行限流,还可以根据ServerHttpRequest的其他参数进行限流
在这里插入图片描述

例如根据请求参数中的username进行限流

@Bean
KeyResolver userKeyResolver() {
    
    
    return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}

4.6 Retry GatewayFilter

Retry GatewayFilter Factory 为请求重试过滤器,当后端服务不可用时,网关会根据配置参数来发起重试请求。Retry GatewayFilter支持如下参数的配置:

  • retries: 重试的次数,默认值是 3
  • statuses: 应被重试的 HTTP Status Codes,参考 org.springframework.http.HttpStatus,此处可以配置多个状态码。
  • methods: 应被重试的 HTTP Methods,参考org.springframework.http.HttpMethod,默认是 GET。
  • series: 应被重试的 Status Codes 系列,参考 org.springframework.http.HttpStatus.Series,默认值是 SERVER_ERROR(5),表示5xx 段的状态码都会发起重试。如果 series 配置了错误码段,但是 status 没有配置,则仍然会匹配 series 进行重试。
  • exceptions: 应被重试的异常列表,默认值是 IOException and TimeoutException
  • backoff: 为重试配置指数级的 backoff。重试时间间隔的计算公式为 firstBackoff * (factor ^ n),n 是重试的次数;如果设置了 maxBackoff,最大的 backoff 限制为 maxBackoff. 如果 basedOnPreviousValue 设置为 true, backoff 计算公式为 prevBackoff * factor.。默认值是disabled不开启。

修改配置文件,实现当Get请求返回500响应码时进行重试1次。

spring:
  redis:
    host: localhost
    password: 123
    port: 6379
  cloud:
    gateway:
      routes:
        - id: retry_route
          uri: http://localhost:1113
          predicates:
          - Method=GET
          filters:
            - name: Retry
              args:
                #需要进行重试的次数
                retries: 1
                #返回状态码为500进行重试
                statuses: INTERNAL_SERVER_ERROR
                backoff:
                  firstBackoff: 10ms
                  maxBackoff: 50ms
                  factor: 2
                  basedOnPreviousValue: false      
      discovery:
        locator:
          enabled: true # 开启自动代理
  application:
    name: gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1111/eureka
logging:
  level:
    org.springframework.cloud.gateway: debug

浏览器访问http://localhost:8080/hello。
在这里插入图片描述
查看后台被被调用的http://localhost:1113的服务的日志打印,可以看到报了两次错。说明进行了一次重试。
在这里插入图片描述

4.7 自定义全局过滤器

实现自定义GlobalFilter和Ordered接口定义全局过滤器。官方文档的DEMO示例:

@Bean
public GlobalFilter customFilter() {
    
    
    return new CustomGlobalFilter();
}

public class CustomGlobalFilter implements GlobalFilter, Ordered {
    
    

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        log.info("custom global filter");
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
    
    
        return -1;
    }
}

实现全局的鉴权过滤器的代码示例:

@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    
    

    private static final String AUTH_CHECK_PATH="/api/**/auth/**";
    private static final String LOGIN_REQUEST="请您先登录再访问!";
    private static final String SESSION_EXPIRED="当前会话已过期,请重新登录!";


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();

        AntPathMatcher antPathMatcher = new AntPathMatcher();
        if (antPathMatcher.match(AUTH_CHECK_PATH,path)){
    
    
            List<String> tokenList = request.getHeaders().get("token");
            //没有token,直接响应失败
            if (tokenList==null){
    
    
                ServerHttpResponse response = exchange.getResponse();
                return out(response,LOGIN_REQUEST);
            }
            //token校验失败,直接响应失败
            boolean isCorrectToken = JwtHelper.checkJwtTToken(tokenList.get(0));
            if (!isCorrectToken){
    
    
                ServerHttpResponse response = exchange.getResponse();
                return out(response,SESSION_EXPIRED);
            }

        }
        //放行
        return chain.filter(exchange);
    }

    /**
     * 定义过滤器的优先级,值越小,优先级越高
     * @return
     */
    @Override
    public int getOrder() {
    
    
        return 0;
    }


    private Mono<Void> out(ServerHttpResponse response,String prompt) {
    
    
        JsonObject message = new JsonObject();
        message.addProperty("success", false);
        message.addProperty("code", 28004);
        message.addProperty("data", "");
        message.addProperty("message", prompt);
        byte[] bytes = message.toString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bytes);
        //指定编码,否则在浏览器中会中文乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        //输出http响应
        return response.writeWith(Mono.just(buffer));
    }
}

参考:
1.Spring Cloud Gateway官方文档
2.Spring Cloud Gateway:新一代API网关服务

猜你喜欢

转载自blog.csdn.net/huangjhai/article/details/124544629