2020:0712--6--SpringCloud:服务网关gateway

1,127 阅读12分钟

Gateway新一代网关

由于zuul的不再维护,zuul2迟迟没有开发出来(半成品)。Spring社区开发了自己的服务网关gateway。

gateway的概述简介

1.  官网

上一代zuul 1.x

当前gateway

2.  背景

cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的zuul网关

但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代zuul:

即springcloud gateway一句话:gateway是原zuul1.x版的替代。

3.  概述

Gateway是在Spring生态系统上构建的API网关服务,基于Spring5,SpringBoot2和Project Reactor等技术。

Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤功能,
例如:熔断,限流,重试。

底层替代了zuul,基于WebFlux框架,WebFlux框架使用了Reactor模式通信框架Netty

一句话:SpringCoud Gateway使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。


4.  能干嘛

    反向代理
    鉴权
    流量控制
    熔断
    日志监控
    ......
    
5.  微服务架构中网关在哪里

6.  我们为什么选择gateway
    
    1.  SpringCloud Gateway具有如下特性

    2.  SpringCloud和Zuul的区别

    3.  Zuul 1.x 的模型

    4.  gateway 模型

7.  Gateway的三大核心概念

    1.  Route路由
        
        路由是构建网关的基本模块,它由ID,目标URI,一些列的断言和过滤器组成,
        如果断言为true则匹配该路由。
        
        路由转发:
            发送请求调用微服务时,负载均衡分派到微服务之前,会经过网关。
            具体分派到哪个微服务,要符合网关的路由转发规则,才能转发到指定微服务。

    2.  Predicate断言
    
        参考的是Java8的java.util.function.Predicate
        开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由。
    
    3.  Filter过滤
    
        指的是Spring框架中GatewayFliter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
        
        处理请求前和处理请求后都可能有Filter

    4.  总体流程
    
        核心逻辑:路由转发+执行过滤器链

gateway的入门配置

1. 新建module

    cloud-gateway-gateway9527

2. pom

    引入gateway依赖。
    网关作为微服务也要注册到我们的Eureka注册中心。
 <dependencies>
        <!--引入网关gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!--引入我们自定义的公共api jar包-->
        <dependency>
            <groupId>com.atguigu.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!--web/actuator这两个一般一起使用,写在一起-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--监控-->
        <dependency>
            <groupId>
                org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!--eureka client-->
        <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>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

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

3. yml

    server:
      port: 9527
    
    spring:
      application:
        name: cloud-gateway
    eureka:
      instance:
        hostname: cloud-gateway-service
        prefer-ip-address: true
        instance-id: cloud-gateway9527
      client:
        service-url: 
          register-with-eureka: true
          fetch-register: true
          defaultZone: http://eureka7001.com:7001/eureka

4 主启动类

    @SpringBootApplication
    @EnableEurekaClient
    public class GatewayMain9527 {
    
        public static void main(String[] args) {
            SpringApplication.run(GatewayMain9527.class, args);
        }
    }

5. 主业务类:没有

6. Gateaway9527网关如何做路由映射呢?

1.  我们以前访问cloud-provider-payment8001中的controller方法

    通过:
        localhost:8001/payment/get/id
        localhost:8001/payment/lb
        就能访问到响应的方法。

2.  我们目前不想暴露8001端口,希望在8001外面套一层9527

3.  yml新增网关配置

    server:
      port: 9527
    
    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          routes:   # 可以为controller中的所有rest接口做路由
            - id: payment_routh           # 路由id:payment_route,没有固定规则,建议配合服务名
              uri: http://localhost:8001  # 匹配后提供服务的路由地址
              predicates:
                - Path=/payment/get/**    # 断言:路径相匹配的进行路由
            
            - id: payment_routh2
              uri: http://localhost:8001
              predicates:
                - Path=/payment/lb/**
    
    eureka:
      instance:
        hostname: cloud-gateway-service
        prefer-ip-address: true
        instance-id: cloud-gateway9527
      client:
        service-url:
          register-with-eureka: true
          fetch-register: true
          defaultZone: http://eureka7001.com:7001/eureka
分析一下:
      cloud:
        gateway:
          routes:   # 可以为controller中的所有rest接口做路由
            - id: payment_routh           # 路由id:payment_route,没有固定规则,建议配合服务名
              uri: http://localhost:8001  # 匹配后提供服务的路由地址
              predicates:
                - Path=/payment/get/**    # 断言:路径相匹配的进行路由
            
            - id: payment_routh2
              uri: http://localhost:8001
              predicates:
                - Path=/payment/lb/**
    routes:    
        
        可以指定多个路由,为controller中的所有rest接口做路由
    
    routes:  
        - id: payment_routh           
          uri: http://localhost:8001  # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**    # 断言:路径相匹配的进行路由
            
        uri和predicates拼接://localhost:8001/payment/get/**就是具体的接口请求路径。
        
        predicates:我断言http://localhost:8001下面有一个/payment/get/**这样的地址。
            找到了这个地址就返回true,可以用9527端口访问,进行端口的适配。
            找不到就返回false,不能用9527这个端口适配。
            
            慢慢的就不在暴露微服务本来的接口8001.转而使用统一网关9527.
            
        为什么是/get/**:因为后面会传参数:/get/{id}
        

4.  这样就为这两个方法做了一下路由匹配

    9527网关就挡在了这两个方法前面,那么当你想要访问里面的这个
    
    localhost:8001/payment/get/1,就要套一个网关。
    

5.  测试:
    启动7001,8001,9527

启动9527时报错。

    原因是:gateway底层使用的是webflux会与web冲突,我在pom中导入了这个依赖;
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    移除重新启动
    
6.  启动成功,开始测试
    localhost:8001/payment/get/1

    真实地址:可以访问 

    结合网关端口的地址:也可以访问
    localhost:9527/payment/get/1

    在以后我们就会慢慢淡化这个真实的地址。 

7 Gateway网关路由的另一种配置

7.1 代码配置

    代码中注入RouteLocator的Bean
    
    业务需求:
        通过9527网关访问到百度新闻的网址;http://news.baidu.com/guonei
        
        
    在config包下创建一个配置类
    路由规则是:我现在访问/guonei,将会转发到http://news.baidu.com/guonei
    @Configuration
    public class GatewayConfig {
    
        @Bean
        public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
    
            //构建一个路由器
            RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
    
            //路由器的id是:path_route_atguigu,规则是我现在访问/guonei,将会转发到http://news.baidu.com/guonei
            routes.route("path_route_atguigu",
                    r -> r.path("/guonei").uri("http://news.baidu.com/guonei")).build();
    
            return routes.build();
        }
    }
    测试成功:

    为什么可以这样:
        因为我们做了新的路由规则,localhost:9527/guonei可以直接跳转到
        http://news.baidu.com/guonei这个网站

7.2 对应的yml配置

    测试:

gateway的进阶配置

1. 通过微服务名实现动态路由

    分析一下我们目前面临的问题:
        
        一个路由规则仅仅只对应一个接口方法,即我们将请求地址写死了。
        试想一下:在分布式集群的情况下,会有多少个主机,多少个端口,多少个接口?
        难道我们要为每一个接口都定义一个路由规则吗?
        
    解决思路:
        
        我们前面用80调用8001和8002中的接口时,只认微服务名。访问接口时没有指定哪个端口。
        
        那么我们在定义路由规则时也可以通过微服务名实现动态路由和负载均衡。
        
    
    1.  默认情况
        
        默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由
        进行转发,从而实现动态路由的功能
    
    2.  演示
        
        启动7001和两个服务提供者8001/8002
        
    3.  9527的pom要有
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

    4. yml
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用为服务名进行路由。
      routes:   # 可以为controller中的所有rest接口做路由
        - id: payment_routh           # 路由id:payment_route,没有固定规则,建议配合服务名
          #uri: http://localhost:8001  # 匹配后提供服务的路由地址
          uri: lb://cloud-payment-service  # lb://开头代表从注册中心中获取服务,后面接的就是你需要转发到的服务名称
          predicates:
            - Path=/payment/get/**    # 断言:路径相匹配的进行路由

        - id: payment_routh2
          #uri: http://localhost:8001
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/lb/**
    注意:
        uri: lb://cloud-payment-service
        lb://开头代表从注册中心中获取服务,后面接的就是你需要转发到的服务名称
        而且找到的服务我还要实现负载均衡。
        
        
    配置好这个yml后,就会去注册中心找cloud-payment-service微服务,然后根据默认的负载均衡算法.
    选择一个微服务去调用里面的接口。
    
    SpringCloudNetFlixRibbon会在定义lb前缀的目标URL上实现负载均衡。
    
    我们之前是通过80访问8001/8002,在80中实现了负载均衡。
    
    配置好后的路由规则:
        发送localhost:9527/xx/xxx请求,会去找cloud-payment-service服务名中对应微服务实例。
        再根据具体的路径,找的具体的方法接口,并且开启了负载均衡。

2. 测试

    启动9527
    
    发送请求:http://localhost:9527/payment/lb

    发送请求:http://localhost:9527/payment/get/1

3. Predicate的使用

    1.  我们注意:Predicates
        这是一个复数,其实有多种Predicate。
        我们这里用的Predicate的是[Path],它是路由规则的其中一个。作用是,如果cloud-payment-service
        的微服务实例中有/payment/get**的接口,就会返回true,路由规则生效。

    2.  但是实际中不止一个Path断言Predicate。

        每一个断言Predicate都有它独特的规则,多个Predicate断言是一个与&组合。

3.1 看一下其他的Predicate

    1  After Route Predicate Factory
    
    1.1 测试一下如何得到当前时区的时间。
        public class T2 {
        
            public static void main(String[] args) {
                ZonedDateTime zbj = ZonedDateTime.now(); //默认时区
                System.out.println(zbj);
            }
        }

        配置yml

        意思是说在这个时间后的访问请求才有效果。
        
    1.2 测试:http://localhost:9527/payment/get/1
    
        成功

        将配置改成1h之后: 目前还没到该时间

        再次测试:http://localhost:9527/payment/get/1
        报错:

    2. 那么Before Route Predicate和Between Route Predicate效果和写法类似。

    3.  Cookie Route Predicate Factory
    
        例如:
        predicates:
        - Cookie=chocolate, ch.p
        
        Cookie Route Predicate需要两个参数,一个是Cookie name,一个是正则表达式。
        路由规则会通过获取对应的Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,
        如果没有匹配上则不执行。
        
    3.1 测试
    
        - Cookie=username,txl

        表示只有发送的请求有cookie,而且里面有username=txl这个数据才能访问。

    3.2 利用curl命令发送POST/GET请求:作为服务请求。
        
        curl http://localhost:9527/payment/get/1 : 只发了一个GET请求,没有带Cookie
        
        报错:

        curl http://localhost:9527/payment/get/1 --cookie "username=txl"
        成功:

    4.  Header Router Predicate Factory
    
        - Header=X-Request-Id, \d+
        
        带着这样的请求头才执行:请求头要有X-Request-Id属性,并且值为整数的正则表达式
        
        测试:
        1.curl http://localhost:9527/payment/get/1 -H "X-Request-Id:-123"

        2.curl http://localhost:9527/payment/get/1 -H "X-Request-Id:123"

        发现这样也是错误的,因为没有满足Cookie

        2.1. curl http://localhost:9527/payment/get/1 -H "X-Request-Id:123" 

            成功
        
        3.  curl http://localhost:9527/payment/get/1 --cookie "username=txl"

    5.  Host Router Predicate Factory
    
        - Host=**.atguigu.com
        
        为了测试,我将cookie和header暂时注释掉

        含义:
            只有指定主机可以访问,可以指定多个用“,”分隔开。
            
        测试:
            curl http:9527/payment/get/1 -H "Host: www.atguigu.com"

            curl http://localhost:9527/payment/get/1

    6.  Method Router Predicate Factory
        
        - Method=GET  #只有get请求才能访问
        
    7.  Query Router Predicate Factory
    
        - Query=username, \d+ #要有参数username=整数,才能路由
        
        http://localhost:9527/payment/get/1?username=123

4 Gateway的Filter

    指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
    
    Filter链:同时满足一系列的过滤链。
    
    路由过滤器可用于修改进去的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。
    SpringCloudGateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类产生。
    
    1.  生命周期
    
        pre
        post
        
    2.  种类
    
        单一的:GatewayFilter
        全局的:GlobalFilter

    3.  自定义过滤器
    
        两个主要接口介绍:implements GlobalFilter, Ordered
        
        定义一个全局过滤器    
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
    //参数exchange可以理解为request,带了一个过滤连chain
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        log.info("*********************come in MyLogGateWayFilter:  "+ new Date());

        //取出请求参数的uname对应的值
        String uname = exchange.getRequest().getQueryParams().getFirst("uname");

        //如果uanme为空,就直接过滤掉,不走路由
        if(uname == null){
            log.info("************* 用户名为Null 非法用户 o(╥﹏╥)o");
            
            //判断该请求不通过时:给一个回应,返回
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }

        //反之,调用下一个过滤器,也就是放行:在该环节判断通过的exchange放行,交给下一个filter判断
        return chain.filter(exchange);
    }

    /**
     * 这个过滤器的加载顺序,数字越小,优先级越高
     * 设置这个过滤器在Filter链中的加载顺序。
     * @return
     */
    public int getOrder() {
        return 0;
    }
}
    测试:
        http://localhost:9527/payment/get/1?uname=55

        http://localhost:9527/payment/get/1