微服务网关Spring Cloud Gateway

509 阅读13分钟

1. Spring Cloud Gateway简介

1.1 简介

  SpringCloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

  SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 2.0之前的非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

  Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

注意:Spring Cloud Gateway 底层使用了高性能的通信框架Netty

1.2 特征

  SpringCloud官方,对SpringCloud Gateway 特征介绍如下:

  • 基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
  • 集成 Spring Cloud DiscoveryClient
  • Predicates 和 Filters 作用于特定路由,易于编写的 Predicates 和 Filters
  • 具备一些网关的高级功能:动态路由、限流、路径重写
  • 集成Spring Cloud DiscoveryClient
  • 集成熔断器CircuitBreaker

  从以上的特征来说,和Zuul的特征差别不大。SpringCloud Gateway和Zuul主要的区别,还是在底层的通信框架上。

  简单说明一下上文中的三个术语:

(1)Filter(过滤器):

和Zuul的过滤器在概念上类似,可以使用它拦截和修改请求,并且对下游的响应,进行二次处理。过滤器为org.springframework.cloud.gateway.filter.GatewayFilter类的实例。

(2)Route(路由):

网关配置的基本组成模块,和Zuul的路由配置模块类似。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。

(3)Predicate(断言):

  这是一个 Java 8 的 Predicate,可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。断言的输入类型是一个 ServerWebExchange。

1.3 入门案例

1.3.1 创建微服务网关工程05_cloud_gateway

image.png

1.3.2 添加依赖

image.png

1.3.3 启动器

package com.lxs.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {

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

}

1.3.4 配置文件

  application.yml代码如下。

server:
  port: 9005
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: url-proxy-1
          uri: https://blog.csdn.net
          predicates:
            - Path=/csdn      
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:9004/eureka

1.3.5 启动并测试

  启动Eureka Server和网关微服务访问http://localhost:9005/csdn,发现路由到了blog.csdn.net

1.4 处理流程

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

image.png

2. 路由配置方式

  路由是网关配置的基本组成模块,和Zuul的路由配置模块类似。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。

2.1 基础路由配置方式

  如果请求的目标地址,是单个的URI资源路径,配置文件实例如下。

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: service1
          uri: https://blog.csdn.net
          predicates:
            - Path=/csdn

各字段含义如下。

  • id:我们自定义的路由 ID,保持唯一
  • uri:目标服务地址
  • predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。

上面这段配置的意思是,配置了一个 id 为service1的URI代理规则,路由的规则为,当访问地址http://localhost:8080/csdn/1.jsp时,会路由到上游地址https://blog.csdn.net/1.jsp。

2.2 基于代码的路由配置方式

  转发功能同样可以通过代码来实现,我们可以在启动类 GateWayApplication 中添加方法 customRouteLocator() 来定制转发规则。

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {

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

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("path_route", r -> r.path("/csdn")
                        .uri("https://blog.csdn.net"))
                .build();
    }
}

一般不用这种方式,用配置文件的方式。

2.3 和注册中心相结合的路由配置方式

  在uri的schema协议部分为自定义的lb:类型,表示从微服务注册中心(如Eureka)订阅服务,并且通过负载均衡进行服务的路由。代码如下。

server:
  port: 9005
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: service1
          uri: https://blog.csdn.net
          predicates:
            - Path=/csdn
        - id: service2
#          uri: http://127.0.0.1:9001
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/**
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:9004/eureka

  注册中心相结合的路由配置方式,与单个URI的路由配置,区别其实很小,仅仅在于URI的schema协议不同。单个URI的地址的schema协议,一般为http或者https协议。启动多个支付微服务,会发现端口9000,9001轮流出现。

image.png

image.png

3. 路由匹配规则

  Spring Cloud Gateway的主要功能之一是转发请求,转发规则的定义主要包含三个部分,如表所示。

Route(路由)路由是网关的基本单元,由ID、URI、一组Predicate、一组Filter组成,根据Predicate进行匹配转发。
Predicate(谓语、断言)路由转发的判断条件,目前SpringCloud Gateway支持多种方式,常见如:Path、Query、Method、Header等,写法必须遵循 key=vlue的形式
Filter(过滤器)过滤器是路由转发请求时所经过的过滤逻辑,可用于修改请求、响应内容

  Spring Cloud Gateway 的功能很强大,我们仅仅通过 Predicates 的设计就可以看出来,前面我们只是使用了 predicates 进行了简单的条件匹配,其实 Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。

  Spring Cloud Gateway 是通过 Spring WebFlux 的 HandlerMapping 做为底层支持来匹配到转发路由,Spring Cloud Gateway 内置了很多 Predicates 工厂,这些 Predicates 工厂通过不同的 HTTP 请求参数来匹配,多个 Predicates 工厂可以组合使用,如下图所示。 image.png

3.1 Predicate断言条件

  Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。

  在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。网上有一张图总结了 Spring Cloud 内置的几种 Predicate 的实现。如图所示

image.png

  说白了 Predicate 就是为了实现一组匹配规则,方便让请求过来找到对应的 Route 进行处理,接下来我们接下 Spring Cloud GateWay 内置几种 Predicate 的使用。转发规则(predicates),假设 转发uri都设定为http://localhost:9001,常见Predicate,如表所示

规则实例说明
Path- Path=/gate/ , /rule/当请求的路径为gate、rule开头的时,转发到http://localhost:9001服务器上
Before- Before=2017-01-20T17:42:47.789-07:00[America/Denver]在某个时间之前的请求才会被转发到 http://localhost:9001服务器上
After- After=2017-01-20T17:42:47.789-07:00[America/Denver]在某个时间之后的请求才会被转发
Between- Between=2017-01-20T17:42:47.789-07:00[America/Denver],2017-01-21T17:42:47.789-07:00[America/Denver]在某个时间段之间的才会被转发
Cookie- Cookie=chocolate, ch.p名为chocolate的表单或者满足正则ch.p的表单才会被匹配到进行请求转发
Header- Header=X-Request-Id, \d+携带参数X-Request-Id或者满足\d+的请求头才会匹配
Host- Host=www.hd123.com当主机名为www.hd123.com的时候直接转发到http://localhost:9001服务器上
Method- Method=GET只有GET方法才会匹配转发请求,还可以限定POST、PUT等请求方式

3.1.1 通过请求参数匹配

  Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。

spring:
  cloud:
    gateway:
      routes:
        - id: service3
          uri: https://www.baidu.com
          order: 0
          predicates:
            - Query=smile

  这样配置,只要请求中包含 smile 属性的参数即可匹配路由。使用 curl 测试,命令行输入:curl localhost:9005?smile=x&id=2,经过测试发现只要请求汇总带有 smile 参数即会匹配路由,不带 smile 参数则不会匹配。

  还可以将 Query 的值以键值对的方式进行配置,这样在请求过来时会对属性值和正则进行匹配,匹配上才会走路由。

spring:
  cloud:
    gateway:
      routes:
        - id: service3
          uri: https://www.baidu.com
          order: 0
          predicates:
            - Query=keep, pu.

  这样只要当请求中包含 keep 属性并且参数值是以 pu 开头的长度为三位的字符串才会进行匹配和路由。 使用 curl 测试,命令行输入:curl localhost:8080?keep=pub,测试可以返回页面代码,将 keep 的属性值改为 pubx 再次访问就会报 404,证明路由需要匹配正则表达式才会进行路由。

3.1.2 通过Header匹配

  Header Route Predicate 和 Query Route Predicate 一样,也是接收 2 个参数,一个 header 中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。

spring:
  cloud:
    gateway:
      routes:        
        - id: service4
          uri: https://www.baidu.com
          order: 0
          predicates:
            - Header=X-Request-Id, \d+

  使用 curl 测试,命令行输入:curl http://localhost:9005 -H "X-Request-Id:88",则返回页面代码证明匹配成功。将参数-H "X-Request-Id:88"改为-H "X-Request-Id:spring",再次执行时返回404证明没有匹配。

3.1.3 通过Cookie匹配

  Cookie Route Predicate 可以接收两个参数,一个是 Cookie name ,一个是正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。

spring:
  cloud:
    gateway:
      routes:        
        - id: service5
          uri: https://www.baidu.com
          predicates:
            - Cookie=sessionId, test

  使用 curl 测试,命令行输入,curl http://localhost:9005 --cookie "sessionId=test",则会返回页面代码。

image.png

image.png

如果去掉--cookie "sessionId=test",后台汇报 404 错误。 image.png

3.1.4 通过Host匹配

  Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则。

spring:
  cloud:
    gateway:
      routes:    
        - id: service6
          uri: https://www.baidu.com
          predicates:
            - Host=**.baidu.com     

  使用 curl 测试,命令行输入,curl http://localhost:9005 -H "Host: www.baidu.com"或者curl http://localhost:8080 -H "Host: md.baidu.com",经测试以上两种 host 均可匹配到 host_route 路由,去掉 host 参数则会报 404 错误。

3.1.5 通过请求方式匹配

  可以通过是 POST、GET、PUT、DELETE 等不同的请求方式来进行路由。

spring:
  cloud:
    gateway:
      routes:    
        - id: service7
          uri: https://www.baidu.com
          predicates:
            - Method=PUT

  使用 curl 测试,命令行输入,curl -X PUT http://localhost:9005,测试返回页面代码,证明匹配到路由,以其他方式,返回 404 没有找到,证明没有匹配上路由。

3.1.6 通过请求路径匹配

  Path RoutePredicate 接收一个匹配路径的参数来判断是否路由。

spring:
  cloud:
    gateway:
      routes:    
        - id: service8
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/payment/{segment}

  如果请求路径符合要求,则此路由将匹配,curl 测试,命令行输入,curl http://localhost:9005/payment/1,可以正常获取到页面返回值,curl http://localhost:9005/payment2/1,报404,证明路由是通过指定路由来匹配。

3.1.7 组合匹配

spring:
  cloud:
    gateway:
      routes:    
        - id: service9
          uri: https://www.baidu.com
          order: 0
          predicates:
            - Host=**.foo.org
            - Path=/headers
            - Method=GET
            - Header=X-Request-Id, \d+
            - Query=foo, ba.
            - Query=baz
            - Cookie=chocolate, ch.p

  各种 Predicates 同时存在于同一个路由时,请求必须同时满足所有的条件才被这个路由匹配。 一个请求满足多个路由的断言条件时,请求只会被首个成功匹配的路由转发。

3.2 过滤器规则

  列举几个过滤器,如表所示。

过滤规则实例说明
PrefixPath- PrefixPath=/app在请求路径前加上app
RewritePath- RewritePath=/test, /app/test访问localhost:9022/test,请求会转发到localhost:8001/app/test
SetPathSetPath=/app/{path}通过模板设置路径,转发的规则时会在路径前增加app,{path}表示原请求路径
RedirectTo重定向
RemoveRequestHeader去掉某个请求头信息

3.2.1 PrefixPath

  对所有的请求路径添加前缀。

spring:
  cloud:
    gateway:
      routes:    
        - id: service10
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/{segment}
          filters:
            - PrefixPath=/payment

  访问/123请求被发送到http://127.0.0.1:9001/payment/123。

3.2.2 StripPrefix

  跳过指定的路径

spring:
  cloud:
    gateway:
      routes:    
        - id: service11
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/api/{segment}
          filters:
            - StripPrefix=1
            - PrefixPath=/payment

  此时访问http://localhost:9005/api/123,首先StripPrefix过滤器去掉一个/api,然后PrefixPath过滤器加上一个/payment,能够正确访问到支付微服务。

3.2.3 RewritePath

spring:
  cloud:
    gateway:
      routes:        
        - id: service12
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/api/payment/**
          filters:
            - RewritePath=/api/(?<segment>.*), /$\{segment}

  对于请求路径 /api/payment/**,当前的配置在请求到到达前会被重写为 /payment/**,由于YAML的语法问题,$符号后面应该加上\在解释正则表达式前,我们需要学习一下java正则表达式分组的两个概念:

  命名分组:(?<name>capturing text) 与普通分组一样的功能,并且将匹配的子字符串捕获到一个组名称或编号名称中。在获得匹配结果时,可通过分组名进行获取。

  引用捕获文本:${name} 将名称为name的命名分组所匹配到的文本内容替换到此处

  那么就很好解释官网的这个例子了: (?<segment>/?.*):匹配 /任意字符,此处/出现0次或1次。将匹配到的结果捕获到名称为segment的组中 $\{segment}:将 segment所捕获到的文本置换到此处,注意,\的出现是由于避免yaml认为这是一个变量,在gateway进行解析时,会被替换为${segment} 请求http://localhost:9005/api/payment/123路径,RewritePath过滤器将路径重写为http://localhost:9005/payment/123,能够正确访问支付微服务。

3.2.4 SetPath

  SetPath和Rewrite类似,代码如下。

spring:
  cloud:
    gateway:
      routes:           
        - id: service13
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/api/payment/{segment}
          filters:
            - SetPath=/payment/{segment}

  请求http://localhost:9005/api/payment/123路径,SetPath过滤器将路径设置为http://localhost:9005/payment/123,能够正确访问支付微服务。

3.2.5 RemoveRequestHeader

  去掉某个请求头信息。

spring:
  cloud:
    gateway:
      routes:
        - id: removerequestheader_route
          uri: https://example.org
          filters:
          - RemoveRequestHeader=X-Request-Foo

  去掉请求头X-Request-Foo

3.2.6 RemoveResponseHeader

  去掉某个回执头信息

spring:
  cloud:
    gateway:
      routes:
        - id: removerequestheader_route
          uri: https://example.org
          filters:
          - RemoveResponseHeader=X-Request-Foo

3.2.7 RemoveRequestParameter

  去掉某个请求参数信息

spring:
  cloud:
    gateway:
      routes:
        - id: removerequestparameter_route
          uri: https://example.org
          filters:
          - RemoveRequestParameter=red

3.2.8 SetRequestHeader

  设置请求头信息

spring:
  cloud:
    gateway:
      routes:
        - id: setrequestheader_route
          uri: https://example.org
          filters:
          - SetRequestHeader=X-Request-Red, Blue

3.2.9 default-filters

  对所有的请求添加过滤器。

spring:
  cloud:
    gateway:
      routes:        
        - id: service14
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/9001/{segment}
        - id: service15
          uri: http://127.0.0.1:9000
          predicates:
            - Path=/9000/{segment}
      default-filters:
      	- StripPrefix=1
        - PrefixPath=/payment

4. 自定义过滤器

4.1 过滤器执行次序

  Spring-Cloud-Gateway 基于过滤器实现,同 zuul 类似,有pre和post两种方式的 filter,分别处理前置逻辑和后置逻辑。客户端的请求先经过pre类型的 filter,然后将请求转发到具体的业务服务,收到业务服务的响应之后,再经过post类型的 filter 处理,最后返回响应到客户端。

  过滤器执行流程如下,order 越大,优先级越低,如图所示。

image.png

  过滤器分为全局过滤器和局部过滤器。

  • 全局过滤器:对所有路由生效。
  • 局部过滤器:对指定的路由生效。

4.2 全局过滤器

  实现 GlobalFilter 和 Ordered,重写相关方法,加入到spring容器管理即可,无需配置,全局过滤器对所有的路由都有效。代码如下。

//@Configuration
public class FilterConfig
{

    @Bean
    public GlobalFilter a()
    {
        return new AFilter();
    }

    @Bean
    public GlobalFilter b()
    {
        return new BFilter();
    }

    @Bean
    public GlobalFilter c()
    {
        return new CFilter();
    }

    @Bean
    public GlobalFilter myAuthFilter()
    {
        return new MyAuthFilter();
    }


    @Slf4j
    static class AFilter implements GlobalFilter, Ordered
    {

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
        {
            log.info("AFilter前置逻辑");
            return chain.filter(exchange).then(Mono.fromRunnable(() ->
            {
                log.info("AFilter后置逻辑");
            }));
        }

        //   值越小,优先级越高
        @Override
        public int getOrder()
        {
            return HIGHEST_PRECEDENCE + 100;
        }
    }

    @Slf4j
    static class BFilter implements GlobalFilter, Ordered
    {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
        {
            log.info("BFilter前置逻辑");
            return chain.filter(exchange).then(Mono.fromRunnable(() ->
            {
                log.info("BFilter后置逻辑");
            }));
        }

        @Override
        public int getOrder()
        {
            return HIGHEST_PRECEDENCE + 200;
        }
    }

    @Slf4j
    static class CFilter implements GlobalFilter, Ordered
    {

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
        {
            log.info("CFilter前置逻辑");
            return chain.filter(exchange).then(Mono.fromRunnable(() ->
            {
                log.info("CFilter后置逻辑");
            }));
        }

        @Override
        public int getOrder()
        {
            return HIGHEST_PRECEDENCE + 300;
        }
    }


    @Slf4j
    static class MyAuthFilter implements GlobalFilter, Ordered {

        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            log.info("MyAuthFilter权限过滤器");
            String token = exchange.getRequest().getHeaders().getFirst("token");
            if (StringUtils.isBlank(token)) {
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }

            return chain.filter(exchange);
        }

        @Override
        public int getOrder() {
            return HIGHEST_PRECEDENCE + 400;
        }
    }


}

  测试结果 image.png

执行顺序为A>B>C

  当添加了MyAuthFilter权限过滤器的时候,没有token执行的话没有授权是会被拦截。

image.png   添加之后可以成功访问

image.png

定义了4个全局过滤器,顺序为A>B>C>MyAuthFilter,其中全局过滤器MyAuthFilter中判断令牌是否存在,如果令牌不存在,则返回401状态码,表示没有权限访问。

4.3 局部过滤器

  定义局部过滤器步骤如下。

(1)需要实现GatewayFilter, Ordered,实现相关的方法

(2)加入到过滤器工厂,并且注册到spring容器中。

(3)在配置文件中进行配置,如果不配置则不启用此过滤器规则。

  接下来定义局部过滤器,对于请求头user-id校验,如果不存在user-id请求头,直接返回状态码406。代码如下。

@Component
public class UserIdCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<Object>
{
    @Override
    public GatewayFilter apply(Object config)
    {
        return new UserIdCheckGateWayFilter();
    }

    @Slf4j
    static class UserIdCheckGateWayFilter implements GatewayFilter, Ordered
    {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
        {
            String url = exchange.getRequest().getPath().pathWithinApplication().value();
            log.info("请求URL:" + url);
            log.info("method:" + exchange.getRequest().getMethod());
            //获取header
            String userId = exchange.getRequest().getHeaders().getFirst("user-id");
            log.info("userId:" + userId);

            if (StringUtils.isBlank(userId))
            {
                log.info("*****头部验证不通过,请在头部输入  user-id");
                //终止请求,直接回应
                exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
                return exchange.getResponse().setComplete();
            }
            return chain.filter(exchange);
        }

        //   值越小,优先级越高
        @Override
        public int getOrder()
        {
            return HIGHEST_PRECEDENCE;
        }
    }


}

  配置文件application.yml代码如下。

spring:
  cloud:
    gateway:
      routes:        
        - id: service2
          uri: lb://cloud-payment-service
          predicates:
            - Path=/{segment}
          filters:
            - PrefixPath=/payment
            - UserIdCheck

  测试结果如下:

image.png

如果不存在user-id请求头,直接返回状态码406。

image.png

成功执行,返回状态码200.

5. 高级特性

5.1 熔断降级

  在分布式系统中,网关作为流量的入口,因此会有大量的请求进入网关,向其他服务发起调用,其他服务不可避免的会出现调用失败(超时、异常),失败时不能让请求堆积在网关上,需要快速失败并返回给客户端,想要实现这个要求,就必须在网关上做熔断、降级操作。

  为什么在网关上请求失败需要快速返回给客户端?因为当一个客户端请求发生故障的时候,这个请求会一直堆积在网关上,当然只有一个这种请求,网关肯定没有问题(如果一个请求就能造成整个系统瘫痪,那这个系统可以下架了),但是网关上堆积多了就会给网关乃至整个服务都造成巨大的压力,甚至整个服务宕掉。因此要对一些服务和页面进行有策略的降级,以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应,所以需要网关上请求失败需要快速返回给客户端。

  CircuitBreaker过滤器使用Spring Cloud CircuitBreaker API 将网关路由包装在断路器中。Spring Cloud CircuitBreaker 支持多个可与 Spring Cloud Gateway 一起使用熔断器库。比如,Spring Cloud 支持开箱即用的 Resilience4J。

  要启用Spring Cloud CircuitBreaker过滤器,步骤如下。

5.1.1 添加依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
        </dependency>

5.1.2 配置文件

server:
  port: 9005
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: service14
          uri: http://127.0.0.1:9001
          predicates:
            - Path=/payment/{segment}
          filters:
            - name: CircuitBreaker
              args:
                name: backendA
                fallbackUri: forward:/fallbackA

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:9004/eureka

resilience4j:
  circuitbreaker:
    configs:
      default:
        failureRateThreshold: 30 #失败请求百分比,超过这个比例,CircuitBreaker变为OPEN状态
        slidingWindowSize: 10 #滑动窗口的大小,配置COUNT_BASED,表示10个请求,配置TIME_BASED表示10秒
        minimumNumberOfCalls: 5 #最小请求个数,只有在滑动窗口内,请求个数达到这个个数,才会触发CircuitBreader对于断路器的判断
        slidingWindowType: TIME_BASED #滑动窗口的类型
        permittedNumberOfCallsInHalfOpenState: 3 #当CircuitBreaker处于HALF_OPEN状态的时候,允许通过的请求个数
        automaticTransitionFromOpenToHalfOpenEnabled: true #设置true,表示自动从OPEN变成HALF_OPEN,即使没有请求过来
        waitDurationInOpenState: 2s #从OPEN到HALF_OPEN状态需要等待的时间
        recordExceptions: #异常名单
          - java.lang.Exception
    instances:
      backendA:
        baseConfig: default
      backendB:
        failureRateThreshold: 50
        slowCallDurationThreshold: 2s #慢调用时间阈值,高于这个阈值的呼叫视为慢调用,并增加慢调用比例。
        slowCallRateThreshold: 30 #慢调用百分比阈值,断路器把调用时间大于slowCallDurationThreshold,视为慢调用,当慢调用比例大于阈值,断路器打开,并进行服务降级
        slidingWindowSize: 10
        slidingWindowType: TIME_BASED
        minimumNumberOfCalls: 2
        permittedNumberOfCallsInHalfOpenState: 2
        waitDurationInOpenState: 120s #从OPEN到HALF_OPEN状态需要等待的时间

5.1.3 配置全局过滤器

  创建一个全局过滤器,打印熔断器状态,代码如下

@Component
@Slf4j
public class CircuitBreakerLogFilter implements GlobalFilter, Ordered {

    @Autowired
    private CircuitBreakerRegistry circuitBreakerRegistry;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String status = circuitBreakerRegistry.circuitBreaker("backendA").getState().toString();
        String url = exchange.getRequest().getPath().pathWithinApplication().value();
        log.info("url : {}, status : {}", url, status);
        return chain.filter(exchange);
    }

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

5.1.4 降级方法

@RestController
@Slf4j
public class FallbackController {

    @GetMapping("/fallbackA")
    public ResponseEntity fallbackA() {
        return ResponseEntity.ok("服务不可用,降级");
    }
}

  使用JMeter工具同时1秒内并发发送20次请求,正常允许下如图所示。

image.png

image.png

熔断器是关闭的状态。

  关闭支付服务后,无法正常执行,再次使用JMeter工具同时1秒内并发发送20次请求,如图所示返回降级方法,此时熔断器还是colse状态。

image.png

image.png

  再次使用JMeter工具同时1秒内并发发送20次请求,熔断器到HALF_OPEN状态。

image.png

接下来启动支付微服务,服务能够正常访问熔断器关闭。

5.2 统一跨域请求

5.2.1 跨域简介

  跨域请求就是指:当前发起请求的域与该请求指向的资源所在的域不一样。这里的域指的是这样的一个概念:我们认为若协议 + 域名 + 端口号均相同,那么就是同域。

  举个例子:假如一个域名为aaa.cn的网站,它发起一个资源路径为aaa.cn/books/getBookInfo的 Ajax 请求,那么这个请求是同域的,因为资源路径的协议、域名以及端口号与当前域一致(例子中协议名默认为http,端口号默认为80)。但是,如果发起一个资源路径为bbb.com/pay/purchase的 Ajax 请求,那么这个请求就是跨域请求,因为域不一致,与此同时由于安全问题,这种请求会受到同源策略限制。

  演示一下,创建index.html,发送ajax测试跨域,代码如下所示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="./js/jquery/jquery-1.10.2.min.js"></script>
    <script>
        function sendAjax() {
            $.ajax({
                method: 'GET',
                url: "http://127.0.0.1:9001/payment/123",
                contentType: 'application/json; charset=UTF-8',
                success: function(o) {
                    alert(o.id);
                    alert(o.message);
                }
            });
        }
    </script>
</head>
<body>
    <button onclick="sendAjax();" >send ajax</button>
</body>
</html>

  通过上述index.html,发送请求,因为浏览器同源策略,就会出现跨域访问问题。

  虽然在安全层面上同源限制是必要的,但有时同源策略会对我们的合理用途造成影响,为了避免开发的应用受到限制,有多种方式可以绕开同源策略,常用的做法JSONP, CORS。可以使用@CrossOrigin,代码如下。

@RestController
@RequestMapping("/payment")
@CrossOrigin
public class PaymentController {

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/{id}")
    public ResponseEntity<Payment> payment(@PathVariable("id") Integer id) {
        Payment payment = new Payment(id, "支付成功,服务端口=" + serverPort);
        return ResponseEntity.ok(payment);
    }

}

5.2.2 跨域配置

  现在请求经过gatway网关是,可以通过网关统一配置跨域访问,代码如下。

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origin-patterns: "*" # spring boot2.4配置
#            allowed-origins: "*"
            allowed-headers: "*"
            allow-credentials: true
            allowed-methods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTION