zoukankan      html  css  js  c++  java
  • SpringCloud 网关组件Gateway

    官网文档: https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/

    1. 概述

    1.1 什么是网关

    微服务架构里面还有一个非常重要的组件,就是网关,

    在Spring Cloud 全家桶里面也有这个角色, 在 1.x 版本中 采用的是 Zuul 网关,

    但是因为 zuul的升级一直跳票,一直放鸽子, Spring Cloud 在2.x 中研发了一个自己的网关 替代了 Zuul, 那就是 Gateway

    网关常见的功能有路由转发、权限校验、限流控制等作用。

    例如在微服务架构中, 如果可以使用网关对 请求进行转发, 前端只需访问一个地址,并携带需要调用的目标地址,由网关进行统一管理. 并且在请求过程中 对请求进行过滤,鉴权,使 微服务的 服务地址不直接暴露,保护了 微服务节点的安全

    微服务架构中网关的位置:

    1.2 Gateway 网关的基本特性

    1. Gateway 是在Spring 生态体系之上构建的 API 网关服务,由于底层使用 netty, 所以是基于 Spring5, Spring Boot2 和 Project Reactor等技术,Gateway旨在 提供一种简单而有效的方式来对 API 进行路由, 以及提供一些强大的过滤器功能,例如 熔断,限流,重试等
    2. Gateway作为 Spring Cloud 生态体系中的网关,目标是替代 Zuul, 在Spring Cloud 2.0 以上的版本中,没有对新版本的Zuul 2.0 以上最新高性能版本进行集成, 仍然使用老的 非 Reactor 模式的 ,
    3. 为了提高性能, Gateway 是基于 WebFlux框架实现的, 而WebFlux 框架底层使用了高性能的 Reactor 模式的通信框架 Netty
    4. Gateway 的目标提供统一的路由方式且基于 Filter 链的方式提供了网关基本的功能,例如 : 安全, 监控/指标, 和限流

    源码架构:

    什么是WebFlux

    这里做基本介绍,详细请自行官网学习

    1. 传统的 Web框架, 比如说 spring mvc 等 都是 Servlet API 与 Servlet 容器基础上运行的
    2. 但是, 在 Servlet 3.1 之后, 有了异步非阻塞的支持, 而WebFlux 是一个典型的非阻塞异步的框架,它的核心是基于Reactor模式的相关API 实现的,相对于传统的web 框架来说, 它可以 运行在诸如Netty,Undertow 等支持Servlet3.1 的容器上, 非阻塞+ 函数式编程(Spring5 必须使用java8)
    3. Spring WebFlux 是 Spring 5.0 引入的新的响应式框架, 区别于Springmvc, 它不需要依赖 Servlet API ,他是完全异步非阻塞的,并且基于Reactor模式来实现响应式流规范

    基本核心组件

    Gateway 三个组件

    • 路由: 网关的基本构建模块,它是由ID、目标URl、断言集合和过滤器集合定义, 如果集合断言为真,则匹配路由。
    • Predicate(断言):这是java 8的一个函数式接口predicate,可以用于lambda表 达式和方法引用,输入类型是:Spring Framework ServerWebExchange,允许 开发人员匹配来自HTTP请求的任何内容,例如请求头headers和参数paramers ,如果请求与断言相匹配,则进行对应的路由
    • Filter(过滤器):这些是使用特定工厂构建的Spring Framework GatewayFilter 实例,这里可以在发送请求之前或之后修改请求和响应

    2. 基本使用

    2.1 工程搭建及测试

    需要搭建 Gateway 网关的微服务, 并注册到注册中心.

    pom依赖:

        <dependencies>
            <!--   gateway     -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
            <!--        &lt;!&ndash;   web Gateway不需要web依赖     &ndash;&gt;-->
            <!--        <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>-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
           
        </dependencies>
    

    yml配置:

    server:
      port: 9527
    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          routes:
            - id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
              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
      client:
        service-url:
          register-with-eureka: true
          fetch-registry: true
          defaultZone: http://eureka7001.com:7001/eureka
     
    

    主启动类:

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

    上面的代码中, 将微服务启动, 并注册到7001 微服务,并在配置文件中, 对 路径/payment/get/**/payment/lb/** 进行拦截,这就是断言,若断言为true,则匹配该路由,并跳转到对应的uri属性下的 地址中

    测试结果:

    • 直接访问 8001 微服务的 接口 http://127.0.0.1:8001/payment/lb

      返回结果 : "8001" 此接口返回 8001 微服务的端口

    • 访问 9527 Gateway 微服务的 地址: http://127.0.0.1:9527/payment/lb

      断言成功, 跳转路由, 返回结果: "8001", 成功调用

    若访问的路径,没有任何路由匹配,则报错404:

    2.2 编码方式配置路由

    上面使用 yml 配置文件的方式进行 配置路由规则, 也可以使用编码的方式进行配置

    下面我们使用编码的方式配置路由,跳转到百度的国内新闻

    @Configuration
    public class GateWayConfig {
        @Bean
        public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
    		//路由构建器
            RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
            //配置路由规则,对比 yml 文件配置
            //  id: path_route , predicates: /guonei , uri: http://news.baidu.com/guonei
            routes.route("path_route"
                    , r->r.path("/guonei").uri("http://news.baidu.com/guonei"))
                    .build();
            return routes.build();
        }
    }
    

    下面进行测试:

    • 直接访问百度国内新闻 http://news.baidu.com/guonei,成功跳转
    • 通过Gateway 微服务访问 http://127.0.0.1:9527/guonei ,也可以跳转

    2.3 使用微服务名跳转

    上面的代码中,我们跳转到某个微服务,都是 直接写对方的ip 地址,

    Gateway 会自动 从注册中心中获取服务列表, 可以通过微服务的名称作为路由转发,那么上面的代码就不用写成

    http://localhost:8001 而是 lb://cloud-payment-service lb 为负载均衡,若该微服务有两个实现,则进行负载均衡

    代码演示:

    首先必须先开启注册中心路由功能: spring.cloud.gateway.discovery.locator.enabled=true

    pom修改:

    server:
      port: 9527
    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true  #开启从注册中心动态创建路由的功能,利用微服务名进行路由
          routes:
            - id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
              #uri: http://localhost:8001   #匹配后提供服务的路由地址
              uri: lb://cloud-payment-service
              predicates:
                - Path=/payment/get/**   #断言,路径相匹配的进行路由
    
            - id: payment_routh2
              #uri: http://localhost:8001   #匹配后提供服务的路由地址
              uri: lb://cloud-payment-service
              predicates:
                - Path=/payment/lb/**   #断言,路径相匹配的进行路由
    
    
    eureka:
      instance:
        hostname: cloud-gateway-service
      client:
        service-url:
          register-with-eureka: true
          fetch-registry: true
          defaultZone: http://eureka7001.com:7001/eureka
     
    

    开启 8001,8002 微服务,调用9527地址: http://127.0.0.1:9527/payment/lb,

    轮流返回 "8001","8002" 对应微服务的地址,调用成功,并负载均衡

    3. 断言工厂

    3.1 概述

    Gateway网关中 另一个非常重要的组件: 断言, 它决定一个请求是否由匹配此路由. 在前面的案例中使用的就是其中的Path 断言工厂生成的 断言类, 并匹配请求,跳转到指定路径,

    GateWay给我们提供了很多不同类型的断言工厂,都是抽象类AbstractRoutePredicateFactory 的子类

    详细使用,请查看官网文档 : https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gateway-request-predicates-factories

    分类:

    时间相关:

    • AfterRoutePredicateFactory: 匹配在指定日期时间之后发生的请求

      示例:

      # 表示在 2017年1月20日17:42:47 之后
      #此时间格式 可以使用 ZonedDateTime 类获取
      #ZonedDateTime.now(); // 默认时区
      #ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定时区获取当前时间
      predicates:
              - After=2017-01-20T17:42:47.789-07:00[America/Denver]
      
    • BeforeRoutePredicateFactory 匹配在指定日期时间之前发生的请求

    • BetweenRoutePredicateFactory 匹配在datetime1之后和在datetime2之前的请求。该datetime2参数必须datetime1之后

      示例:

      #表示在2017年1月20日17:42:47之后 并且 在2017年1月21日17:42:47之前
      predicates:
              - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
      

    Cookie相关:

    • CookieRoutePredicateFactory 匹配具有指定Cookie,并且值与指定的正则匹配的请求

      示例:

      # 表示Cookie 中携带 键为chocolate,值为可以匹配正则"ch.p" 的字符串
      predicates:
              - Cookie=chocolate, ch.p
      

    Header相关:

    • HeaderRoutePredicateFactory, 匹配 请求头中有指定的名,并且值匹配指定的正则表达式

      示例:

      predicates:
              - Header=X-Request-Id, d+
      
    • HostRoutePredicateFactory, 匹配Host 域名列表

      示例:

      # 匹配路径中host 为 *.baidu.com 的 和 *.souhu.com的
      predicates:
              - Host=**.baidu.com,**.sohu.com
      
    • RemoteAddrRoutePredicateFactory ,匹配请求的Remote(客户端i来源ip)

      示例:

      # 匹配Remote 为 192.168.1.1 至 192.168.1.254
      # 斜杠后面的24 表示最后一位的最大值 即254
      #16 表示最后两位 即 255.254
      #8 表示最后三位 即 255.255.254
      predicates:
              - RemoteAddr=192.168.1.1/24
      

    请求相关:

    • MethodRoutePredicateFactory 匹配指定的请求方式

      示例:

      #匹配 GET,POST 请求
      predicates:
              - Method=GET,POST
      
    • QueryRoutePredicateFactory 匹配请求有指定的参数key,并且值匹配指定的正则

      示例:

      # 请求键为 red 值匹配正则 "gree."
      predicates:
              - Query=red, gree.
      
    • PathRoutePredicateFactory 匹配url 路径,也就是我们上面案例中用到的

    3.2 断言工厂的工作原理

    下面使用MethodRoutePredicateFactory 来进行演示

    源码

    public class MethodRoutePredicateFactory extends AbstractRoutePredicateFactory<MethodRoutePredicateFactory.Config> {
        /** @deprecated */
        @Deprecated
        public static final String METHOD_KEY = "method";
        public static final String METHODS_KEY = "methods";
    
        public MethodRoutePredicateFactory() {
            super(MethodRoutePredicateFactory.Config.class);
        }
        /**
        *  封装是 config 类中使用哪个字段接受参数
        */
        public List<String> shortcutFieldOrder() {
            return Arrays.asList("methods");
        }
    
        public ShortcutType shortcutType() {
            return ShortcutType.GATHER_LIST;
        }
    
        /**
        *  实际生产 断言操作类的方法
        */
        public Predicate<ServerWebExchange> apply(MethodRoutePredicateFactory.Config config) {
            return new GatewayPredicate() {
                /**
      			 *  断言操作类的test方法,判断请求是否符合条件
       			 */
                public boolean test(ServerWebExchange exchange) {
                    HttpMethod requestMethod = exchange.getRequest().getMethod();
                    return Arrays.stream(config.getMethods()).anyMatch((httpMethod) -> {
                        return httpMethod == requestMethod;
                    });
                }
    
                public String toString() {
                    return String.format("Methods: %s", Arrays.toString(config.getMethods()));
                }
            };
        }
    
        /**
        *  配置类,接受配置文件中的配置的信息,
        */
        @Validated
        public static class Config {
            // 一个枚举数组, 接受请求方式,例如 [GET,POST]
            private HttpMethod[] methods;
    
            public Config() {
            }
    
            /** @deprecated */
            @Deprecated
            public HttpMethod getMethod() {
                return this.methods != null && this.methods.length > 0 ? this.methods[0] : null;
            }
    
            /** @deprecated */
            @Deprecated
            public void setMethod(HttpMethod method) {
                this.methods = new HttpMethod[]{method};
            }
    
            public HttpMethod[] getMethods() {
                return this.methods;
            }
    
            public void setMethods(HttpMethod... methods) {
                this.methods = methods;
            }
        }
    }
    
    

    上面的代码中, 可以看出其实MethodRoutePredicateFactory 的实现比较简单.生产一个GatewayPredicate进行断言.主要做了如下两个操作

    • 获取配置文件中配置的参数
    • 判断请求的方法是否匹配其中任意一个参数

    3.3 自定义断言工厂

    根据上面的规则,我们可以实现自己的自定义断言工厂

    接收参数的Config 类:

    public class TulingTimeBetweenConfig {
    
        private LocalTime startTime;
    
        private LocalTime endTime;
    
        public LocalTime getStartTime() {
            return startTime;
        }
    
        public void setStartTime(LocalTime startTime) {
            this.startTime = startTime;
        }
    
        public LocalTime getEndTime() {
            return endTime;
        }
    
        public void setEndTime(LocalTime endTime) {
            this.endTime = endTime;
        }
    }
    

    断言工厂类,注意工厂类的类名必须以"RoutePredicateFactory"开头, "RoutePredicateFactory" 之前的一部分则作为配置文件中的键

    @Component
    public class TulingTimeBetweenRoutePredicateFactory extends AbstractRoutePredicateFactory<TulingTimeBetweenConfig> {
    
        public TulingTimeBetweenRoutePredicateFactory() {
            super(TulingTimeBetweenConfig.class);
        }
        /**
         * 真正的业务判断逻辑
         */
        @Override
        public Predicate<ServerWebExchange> apply(TulingTimeBetweenConfig config) {
    
            LocalTime startTime = config.getStartTime();
    
            LocalTime endTime = config.getEndTime();
    
            return new Predicate<ServerWebExchange>() {
                @Override
                public boolean test(ServerWebExchange serverWebExchange) {
                    LocalTime now = LocalTime.now();
                    //判断当前时间是否在在配置的时间范围类
                    return now.isAfter(startTime) && now.isBefore(endTime);
                }
            };
    
        }
    
        /**
         * 用于接受yml中的配置 ‐ TulingTimeBetween=上午7:00,下午11:00
         */
        @Override
        public List<String> shortcutFieldOrder() {
            return Arrays.asList("startTime", "endTime");
        }
    
    }
    
    

    yaml配置文件,使用逗号分隔

    predicates:
        - TulingTimeBetween=上午7:00,下午11:00
    

    测试,当请求时间为上午七点到下午十一点前的所有请求,都会被拦截

    4. 过滤器工厂

    上面的操作中,我们仅仅只是将 请求拦截,并跳转到某个地址,貌似没做什么操作,作用很小,下面介绍过滤器的使用,将在拦截过程中做一些操作

    ,SpringCloudGateway 也提供了很多的过滤器工厂,我们通过一些过滤器工厂可以进行一些业务逻辑处理器,比如添加剔除响应头,添加去除参数等.

    官网文档: https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gatewayfilter-factories

    4.1 常用过滤器简单介绍

    下面简单介绍几种常用的:

    添加请求头:

    给拦截到请求中加入指定的请求头和指定的值

    predicates:
      ‐ TulingTimeBetween=上午7:00,下午11:00
    filters:
      ‐ AddRequestHeader=X‐Request‐Company,tuling
    
    

    添加请求参数

    给请求加上指定的 Parameter 参数,和指定的值

    predicates:
     ‐ TulingTimeBetween=上午7:00,下午11:00
    filters:
     ‐ AddRequestParameter=company,tuling
    

    为匹配的路由统一添加前缀

    给请求加上指定的前缀,比如下面的配置中,请求http://localhost:8888/selectProductInfoById/1会转发到

    http://localhost:8888/product‐api/selectProductInfoById/1

    predicates:
     ‐ TulingTimeBetween=上午7:00,下午11:00
    filters:
     ‐ PrefixPath=/product‐api
    

    更多详细用户请参考官网,提供了丰富的过滤器工厂

    4.1 自定义过滤器工厂

    过滤器工厂的实现思路和断言工厂类似,也可以参考着自定义自己的过滤器工厂,下面我们来实现一个记录整个过滤链执行时间的过滤器工厂类

    过滤器操作类:

    在查看源码过程中,发现其过滤器工厂返回过滤器操作类代码中,都是使用匿名内部类的方式,但是这样过滤器的执行顺序无法保证,只能按照加载顺序执行,所以这里我们将操作类单独定义,实现Ordered 接口,保证加载顺序优先

    public class TimeMonitorGatewayFilter implements GatewayFilter, Ordered {
    
        private static final String COUNT_START_TIME = "countStartTime";
        private AbstractNameValueGatewayFilterFactory.NameValueConfig config;
    
        public TimeMonitorGatewayFilter(AbstractNameValueGatewayFilterFactory.NameValueConfig config) {
            this.config = config;
        }
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
                chain) {
            //获取配置文件yml中的
            // filters:
            // ‐ TimeMonitor=enabled,true
            String name = config.getName();
            String value = config.getValue();
    
            if (value.equals("false")) {
                return null;
            }
            //在请求中记录开始时间
            exchange.getAttributes().put(COUNT_START_TIME,
                    System.currentTimeMillis());
    
            //then方法相当于aop的后置通知一样,当整个过滤链执行完毕时 ,将调用此方法,
            return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
                @Override
                public void run() {
                    //结束时间
                    Long startTime = exchange.getAttribute(COUNT_START_TIME);
                    //获取开始时间 并计算差值
                    if (startTime != null) {
                        StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())
                                .append(": ")
                                .append(System.currentTimeMillis() - startTime)
                                .append("ms");
                        sb.append(" params:").append(exchange.getRequest().getQueryParams());
                        //打印执行时间
                        System.out.println(sb.toString());
                    }
                }
            }));
        }
    
        /**
         *  数字越小 Spring 加载此类越优先
         * @return
         */
        @Override
        public int getOrder() {
            return -100;
        }
    }
    

    此类在执行链开始时执行,并记录开始时间,并定义了结束过滤链结束时,计算差值

    过滤器工厂类:

    和断言工厂一样, 也是使用指定的后缀,来确定配置文件中的配置方式,必须为"GatewayFilterFactory" 结尾

    并继承了 AbstractNameValueGatewayFilterFactory 类, 可以接受配置文件中的 name,value 形式的参数

    但是本例中只使用 value来定义

    @Component
    public class TimeMonitorGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
    
        @Override
        public GatewayFilter apply(NameValueConfig config) {
            return new TimeMonitorGatewayFilter(config) ;
        }
    }
    

    yaml配置文件:

    接受到的参数 : name为enabled ,value为true,但是上面的代码中 只用到了 true参数,含义为开启此功能

    predicates:
     ‐ Query=company,product
    filters:
     ‐ TimeMonitor=enabled,true
    

    测试: 调用本网关,[127.0.0.1:9527/payment/lb?name=10](http://127.0.0.1:9527/payment/lb?name=10)

    打印日志信息: /payment/lb: 8ms params:{name=[10]}

    4.3 自定义全局过滤器

    GateWay 框架中,还有一种特殊的过滤器, 为全局过滤器,只要是被拦截的请求,都会被执行,上面的负载均衡功能就是

    LoadBalancerClientFilter 全局过滤器起的作用

    其他全局过滤器使用,请查看官网: https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#global-filters

    同样,我们也可以自定义全局过滤器:

    @Component
    public class MyLogGateWayFilter implements GlobalFilter,Ordered {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            System.out.println("进来");
            //获取 url上第一个 uname param
            String uname = exchange.getRequest().getQueryParams().getFirst("uname");
            if (uname==null){
                exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
                return exchange.getResponse().setComplete();
            }
    
            return chain.filter(exchange);
        }
    
        /**
         * 过滤链的排序
         * @return
         */
        @Override
        public int getOrder() {
            return 0;
        }
    }
    
    

    上面的过滤器中,进行了简单的鉴权操作,若请求参数中没有username,则拒绝转发,

  • 相关阅读:
    select在各个浏览器中的兼容性问题
    pc打开手机站提示切换为手机屏幕
    图片预加载
    div盒子水平垂直居中的方法
    如何检测该浏览器为微信浏览器
    小箭头的写法,z-index在ie7显示混乱问题
    微信a标签不跳转
    css font简写规则
    windows环境cmd下执行jar
    docker制作镜像步骤
  • 原文地址:https://www.cnblogs.com/xjwhaha/p/14049372.html
Copyright © 2011-2022 走看看