【SpringCloud学习笔记】Gateway

/ 微服务 / 没有评论 / 3160浏览

SpringCloud Gateway

Gateway介绍

Gateway是SpringCloud的一个全新项目,基于Spring5.0、Springboot2.0和Project Reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API路由管理方式。其作为SpringCloud生态系统中的网关,目标是替代Zuul,在SpringCloud2.0以上版本中,没有对新版本的Zuul2.0以上最新高性能版本进行集成,仍然还是使用的Zuul1.x非Reator模式的老版本。而为了提升网关的性能,Gateway是基于WebFlux框架实现的,二WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

Gateway目标提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如:安全、监控指标、限流等。

特性:

为何用Gateway

Zuul 1.x 是阻塞式的高并发场景下效率低下。

Gateway基于WebFlux是非阻塞式的异步框架。

Gateway核心概念

Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一些列的断言和过滤器组成,如果断言为true则匹配改路由。

Predicate(断言):基于Java8的Predicate,可以匹配Http请求中所有内容(例如请求头或者请求参数),如果请求与断言相匹配则进行路由。

Filter(过滤器):指的是GatewayFilter的实例,使用过滤器可以在请求被路由前或者后对请求进行修改。

image-20210309195555532

说明:Web请求通过一些匹配条件,定位到正在的服务节点。并在转发过程的前后,进行一些精细化控制。Predicate就是我们的匹配条件;Filter可以理解为一个无所不在的拦截器,有了这两个元素再加上目标uri就可以实现一个具体的路由。

Gateway工作流程

Spring Cloud Gateway Diagram

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

Filter在之前可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在之后可以做响应内容&响应头的吸怪、日志的输出、流量监控等非常重要的作用。

案例实操

创建maven项目

新建名为:cloud-gateway-9527 的maven项目

修改pom文件

    <dependencies>
        <!-- 服务网关依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- 将gateway注册到eureka中 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 支持热部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
    </dependencies

时间过程中发现了很严重的版本依赖的问题;经过治疗查找最终发现是Springboot和Gateway版本不匹配的问题;最后根据其官方提供的对应匹配关系对父项目的版本依赖做了如下修改:

      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Hoxton.SR10</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <!-- 将Springboot的版本信息从 2.2.2.RELEASE 改成  2.3.0.RELEASE -->  
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.3.0.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

修改application.yml

server:
  port: 9527
spring:
  application:
    name: cloud-gateway-9527
  cloud:
    gateway:
      routes:
        - id: payment_get_info
          uri: http://localhost:8001
          predicates:
            - Path=/payment/get/**
        - id: payment_discovery
          uri: http://localhost:8002
          predicates:
            - Path=/payment/discovery
eureka:
  instance:
    instance-id: cloud-gateway-service
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 1 #eureka客户端向服务端发送心跳的间隔,单位秒,默认30
    lease-expiration-duration-in-seconds: 2  #eureka服务端在收到最后一次心跳后等待时间上限,单位为秒,默认90,,超时将剔除服务
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
     defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

测试

  1. 访问http://localhost:8001/payment/get/1 能正常访问;
  2. 访问http://localhost:9527/payment/get/1 能正常访问

配置的两种方式

gateway的服务网关有两种方式,上面我们介绍的是基于application.yml的配置文件形式;下面来介绍一下java编码的方式来配置服务网关的跳转。

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        routes.route("path_route_2_baidu", r->r.path("/guonei").uri("http://news.baidu.com/guonei"));
        routes.route("path_route_2_baidu2", r->r.path("/guoji").uri("http://news.baidu.com/guoji"));
        return routes.build();
    }
}

配置完成后重启,访问:http://localhost:9527 可以顺利的跳转到百度。

image-20210312100708817

结合Ribbon实现负载均衡

只需要修改映射的uri即可;具体配置如下;

spring:
  application:
    name: cloud-gateway-9527
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态获取路由的功能,利用微服务名进行路由 实测这里配不配自都可以实现负载均衡
      routes:
        - id: payment_get_info
#          uri: http://localhost:8001
          uri: lb://CLOUD-PROVIDER-PAYMENT
          predicates:
            - Path=/payment/get/**

        - id: payment_discovery
          uri: lb://CLOUD-PROVIDER-PAYMENT
          predicates:
            - Path=/payment/discovery

测试方法:

在浏览器地址栏中访问:http://localhost:9527/payment/get/1 不断刷新,返回信息的端口会在8001和8002中不断切换。

image-20210312101126209

Predicate

在Gateway启动时会加载断言:

image-20210313150823345

SpringCloud Gateway 将路由器匹配作为Spring WebFlux HandlerMapping基础架构的一部分。SpringCloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与Http请求的不同属性匹配。多个Route Predicate工厂可以进行组合。Gateway创建Route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route。Gateway包括许多内置的Route Predicate Factories.所有这些谓词都匹配HTTP请求的不同是属性,多个谓词工厂可以组合,并通过逻辑and。

断言SpringCloud Gateway默认提供了如下种类:

After

配置说明

after后面需要跟一个时间;时间的类型是ZonedDateTime

          predicates:
            - Path=/payment/get/**
            - After=2021-03-12T10:35:38.193+08:00[Asia/Shanghai]
测试结果

时间没有到之前访问:

image-20210312103416361

时间到了后访问:

image-20210312103553048

Before

配置说明

Before和After类似也是跟一个ZonedDateTime类型的时间;表名再次之前是无法匹配断言的。

Between

配置说明

Between和After类似也是跟一个ZonedDateTime类型的时间;表名再次之前是无法匹配断言的。

     predicates:
        - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

Cookie

配置说明

Cookie要求请求的信息中必须包含cookie相关信息;否则就会拒绝访问。实例如下:

          predicates:
            - Path=/payment/get/**
            - After=2021-03-12T10:35:38.193+08:00[Asia/Shanghai]
            - Cookie=username,huzd
测试结果

不带Cookie访问:

image-20210312104203576

带Cookie访问:

image-20210312104422732

Header

配置说明

两个参数:一个是属性名称,一个是正则表达式,这个属性值和正则表达式匹配则执行。

          predicates:
            - Path=/payment/get/**
            - After=2021-03-12T10:35:38.193+08:00[Asia/Shanghai]
#            - Cookie=username,huzd
            - Header=X-Request-Id, \d+
测试结果

测试:如果正常匹配返回接口数据,如果不是整数则无法匹配,返回错误。

image-20210312104902388

Host

配置说明

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

          predicates:
            - Path=/payment/get/**
            - After=2021-03-12T10:35:38.193+08:00[Asia/Shanghai]
#            - Cookie=username,huzd
#            - Header=X-Request-Id, \d+
            - Host=**.huzd.info
测试结果

测试过程及结果:

#测试成功命令
curl http://localhost:9527/payment/get/1 -H "Host:www.huzd.fun"
#测试失败命令
curl http://localhost:9527/payment/get/1 -H "Host:www.sina.info"

测试截图:

image-20210313135021141

Method

配置说明

限制请求方法为GET 或者 POST

          predicates:
            - Path=/payment/get/**
            - After=2021-03-12T10:35:38.193+08:00[Asia/Shanghai]
#            - Cookie=username,huzd
#            - Header=X-Request-Id, \d+
#            - Host=**.huzd.info
            - Method=GET #指定只支持get类型的请求

测试过程及测试结果:

huzd@huzd-MacBook-Pro ~ % curl -X GET http://localhost:9527/payment/get/1  # 正确测试
{"code":200,"message":"查询成功(服务端口:8002)","object":{"id":1,"serial":"101010101010101"}}%    

huzd@huzd-MacBook-Pro ~ % curl -X POST http://localhost:9527/payment/get/1 #使用POST测试,报错
{"timestamp":"2021-03-13T05:55:47.828+00:00","path":"/payment/get/1","status":404,"error":"Not Found","message":null,"requestId":"852dea2f-6","trace":"org.springframework.web.server.ResponseStatusException: 404 NOT_FOUND\n\tat org.springframework.web.reactive.resource.ResourceWebHandler.lambda$handle$0(ResourceWebHandler.java:325)\n\tSuppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: \nError has been observed at the following site(s):\n\t|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]\n\t|_ checkpoint ⇢ HTTP POST \"/payment/get/1\" …………

Query

配置说明

限定http请求中必须携带某个参数;-Query 可以跟两个参数值第一个为参数的名称;第二个为正则表达式用来约束参数值。

          predicates:
            - Path=/payment/get/**
            - After=2021-03-12T10:35:38.193+08:00[Asia/Shanghai]
            - Query=username, \d+
测试结果

测试过程及结果:

huzd@huzd-MacBook-Pro ~ % curl http://localhost:9527/payment/get/1?username=11           # 带username参数并且值为整数,正常访问   
{"code":200,"message":"查询成功(服务端口:8001)","object":{"id":1,"serial":"101010101010101"}}%    

huzd@huzd-MacBook-Pro ~ % curl http://localhost:9527/payment/get/1?username=11f #带username参数并且值为非整数,访问异常
{"timestamp":"2021-03-13T06:10:41.741+00:00","path":"/payment/get/1","status":404,"error":"Not Found","message":null,"requestId":"8f2f9a4c-20","trace":"org.springframework.web.server.ResponseStatusException: 404 NOT_FOUND\n\tat org.springframework.web.reactive.resource.ResourceWebHandler.lambda$handle$0(ResourceWebHandler.java:325)\n\tSuppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: \nError has been observed at the following site(s):\n\t|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]\n\t|_ checkpoint ⇢ HTTP GET \"/payment/get/1?username=11f\"

Path

配置说明

用于匹配需要转发的路径

      routes:
        - id: payment_get_info
          uri: lb://CLOUD-PROVIDER-PAYMENT
          predicates:
            - Path=/payment/get/**

RemoteAddr

配置说明
      routes:
        - id: payment_get_info
          uri: lb://CLOUD-PROVIDER-PAYMENT
          predicates:
            - Path=/payment/get/**
            - After=2021-03-12T10:35:38.193+08:00[Asia/Shanghai]
            - RemoteAddr=127.0.0.1
测试结果

测试过程及结果

huzd@huzd-MacBook-Pro ~ % curl http://127.0.0.1:9527/payment/get/1   #使用配置的127.0.0.1 请求成功
{"code":200,"message":"查询成功(服务端口:8001)","object":{"id":1,"serial":"101010101010101"}}%            

huzd@huzd-MacBook-Pro ~ % curl http://localhost:9527/payment/get/1 #使用配置的localhost 请求失败
{"timestamp":"2021-03-13T06:21:44.562+00:00","path":"/payment/get/1","status":404,"error":"Not Found","message":null,"requestId":"bb03ff12-22","trace":"org.springframework.web.server.ResponseStatusException: 404 NOT_FOUND\n\tat org.springframework.web.reactive.resource.ResourceWebHandler.lambda$handle$0(ResourceWebHandler.java:325)\n\tSuppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: \nError has been observed at the following site(s):\n\t|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]\n\t|_ checkpoint ⇢ HTTP GET \"/payment/get/1\" 

Weight

配置说明

设置访问权重;通过权重来转发请求

        - id: payment_weight_high
          uri: http://localhost:8001
          predicates:
            - Path=/payment/weight/**
            - Weight=group1,7
        - id: payment_weight_low
          uri: http://localhost:8002
          predicates:
            - Path=/payment/weight/**
            - Weight=group1,3

上图中70%的请求会被分发到payment_weight_high中,30%请求会被分发到payment_weight_low中

为了配合测试我们在服务提供8001和8002中添加了新的方法:

    @GetMapping("/weight/{id}")
    @ResponseBody
    public CommonResult weight(@PathVariable("id") Long id) {
        Payment payment = paymentService.getById(id);
        CommonResult cr = null;
        if(payment!=null){
            cr = new CommonResult(200,"invoke weight(服务端口:"+port+")",payment);
        }else{
            cr = new CommonResult(400,"invoke weight(服务端口:"+port+")",null);
        }
        return cr;
    }
测试结果

测试方法及结果:

访问:http://localhost:9527/payment/weight/1 频繁刷新根据返回的信息可以看到;大部分请求落到8001机器上。

官网示例如下:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories

Filter

路由过滤器可用于修改进入的HTTP请求和返回的HTTP请求,路由过滤器智能指定旅游进行使用。Gateway内置了多种路由器,他们都有GatewayFilter工厂类来生成。

Filter,在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出、流量监控等,有非常重要的作用。

Filter除了分为“pre”和“post”两种方式的Filter外,在Spring Cloud Gateway中,Filter从作用范围可分为另外两种,一种是针对于单个路由的Gateway Filter,它在配置文件中的写法同predict类似;另外一种是针对于所有路由的Global Gateway Filer。现在从作用范围划分的维度来讲解这两种Filer。

AddRequestHeader &AddRequestParameter

配置说明
      routes:
        - id: payment_get_info_h
#          uri: http://localhost:8001
          uri: lb://CLOUD-PROVIDER-PAYMENT
          predicates:
            - Path=/payment/get/**
            - After=2021-03-12T10:35:38.193+08:00[Asia/Shanghai]
          filters:
            - AddRequestHeader=X-Request-appid, SCCS_PRODUCT
            - AddRequestParameter=huzd, XDI2JSS11SL1

修改服务提供者的PaymentController来查看参数结果:

    @GetMapping("/get/{id}")
    @ResponseBody
    public CommonResult getById(@PathVariable("id") Long id, HttpServletRequest request) {
        String headerParam = request.getHeader("X-Request-appid");
        String otherReqParam = request.getParameter("huzd");
        System.out.println("---->otherReqParam:"+otherReqParam);
        Payment payment = paymentService.getById(id);
        CommonResult cr = null;
        if(payment!=null){
            cr = new CommonResult(200,"查询成功(服务端口:"+port+")-appid:"+headerParam,payment);
        }else{
            cr = new CommonResult(400,"无对应记录(服务端口:"+port+")-appid:"+headerParam,null);
        }
        return cr;
    }
测试结果

已经拿到appid及额外添加的参数。

image-20210315141144850

image-20210315141219963

自定义全局过滤器

Java编写全局拦截器

@Component
@Slf4j
public class CustomGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono <Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("访问CustomGlobalFilter,filter方法");
        String username = exchange.getRequest().getQueryParams().getFirst("username");
        log.info("检测传参第一个为username的值:"+username);
        if(StringUtils.isEmpty(username)){
            log.info("传参为null,非法");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

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

测试日志:

image-20210315164036116