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,则拒绝转发,

原文地址:https://www.cnblogs.com/xjwhaha/p/14049372.html