• spring cloud项目06:网关(Gateway)(1)


    JAVA 8

    spring boot 2.5.2

    spring cloud 2020.0.3

    ---

    授人以渔:

    1、Spring Cloud PDF版本

    最新版本,下载下来,以便查阅。

    更多版本的官方文档:

    https://docs.spring.io/spring-cloud/docs/

    2、Spring Cloud Gateway

    没有PDF版本,把网页保存下来。

    本文使用的项目:

    主要路径:前端请求经过 external.gateway 转发到 adapter.web。在此过程中,会做一些试验。

    external.gateway  网关服务 端口 25001
    adapter.web web适配层应用 端口 21001
    data.user user数据层应用 端口 20001
    eureka.server Eureka注册中心 端口 10001

    目录

    0、序章

    Spring Cloud Gateway简介

    1、通过网关服务访问其它应用

    更多断言试验

    2、过滤器使用

    试验:自定义过滤器工厂

    试验:全局过滤器

    参考文档

    0、序章

    建立项目,引入 spring-cloud-starter-gateway 包:

    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    检查包依赖结构:

    其中,依赖了 spring-cloud-gateway-server、spring-boot-starter-webflux(使用Netty服务器)。

    启动项目,发现加载了很多 RoutePredicateFactory:

    检查项目启动后Spring容器中的Bean,可以发现很多和Gateway相关的,比如:

    部分Gateway相关Bean
    org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration
    org.springframework.cloud.gateway.config.GatewayAutoConfiguration$NettyConfiguration
    gatewayHttpClient
    org.springframework.cloud.gateway.config.GatewayAutoConfiguration
    gatewayConfigurationService
    routePredicateHandlerMapping
    gatewayProperties
    
    # 多个
    **RoutePredicateFactory
    
    # 多个
    **GatewayFilterFactory
    
    org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration
    spring.cloud.gateway.loadbalancer-org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties
    ...

    启动后访问 网关服务——http://localhost:25001/ ,但没有找到页面:里面有一个requestId,和之前的Web项目不一样

    Postman访问结果
    {
        "timestamp": "2021-09-11T03:06:33.044+00:00",
        "path": "/",
        "status": 404,
        "error": "Not Found",
        "message": null,
        "requestId": "18974297-1"
    }

    那么,有哪些端口可以访问呢

    添加actuator检查,也没有发现有可用的端口:

    使用actuator
    # pom.xml文件
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
    # application.properties文件
    management.endpoints.web.exposure.include=health, info, mappings

    查看Spring Cloud文档,其Spring Cloud Gateway下有一章“15. Actuator API”,原来,还需要添加以下配置才可以看到:

    # 多了一个 gateway
    management.endpoints.web.exposure.include=health, info, mappings,gateway

    再次启动 网关服务,此时,多了一个 /actuator/gateway 端点,下面是访问结果:

    访问/actuator/gateway及其子端点
    # 居然访问不到!
    # http://localhost:25001/actuator/gateway
    {
        "timestamp": "2021-09-11T03:41:43.253+00:00",
        "path": "/actuator/gateway",
        "status": 404,
        "error": "Not Found",
        "message": null,
        "requestId": "aba1dfd7-4"
    }
    
    # 子端点 routes,,返回结果为 [],因为什么路由也没有配置
    # http://localhost:25001/actuator/gateway/routes
    []
    
    # 子端点 globalfilters
    http://localhost:25001/actuator/gateway/globalfilters
    {
        "org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@6b00ad9": -2147482648,
        "org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@3ce53f6a": -1,
        "org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@59d77850": 2147483646,
        "org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration$NoLoadBalancerClientFilter@60859f5a": 10150,
        "org.springframework.cloud.gateway.filter.ForwardPathFilter@1a6cf771": 0,
        "org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@3ee69ad8": 10000,
        "org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@2d82408": -2147483648,
        "org.springframework.cloud.gateway.filter.GatewayMetricsFilter@53ed09e8": 0,
        "org.springframework.cloud.gateway.filter.NettyRoutingFilter@19650aa6": 2147483647,
        "org.springframework.cloud.gateway.filter.ForwardRoutingFilter@f679798": 2147483647
    }
    

    /actuator/gateway 端点还有一些子端点,S.C.的官文中会有详情(下图来自官网)。

    Spring Cloud Gateway简介

    S.C.的 第二代网关框架(第一代为 Netflix Zuul),不仅提供1)统一的路由方式,并且基于Filter链的方式提供了网关的基本功能。

    使用 非阻塞模式(WebFlux、Netty),支持长连接WebSocket。

    可用作为 整个分布式系统的流量入口,也可以作为 系统内部若干应用的网关服务,再统一其它应用提供服务。

    功能关键词:

    协议转换、路由转发、流量聚合、流量监控、限流、权限判断、缓存

    核心组件:

    路由、过滤器、断言(Predicate)

    请求处理流程:

    请求》Gateway Handler Mapping》路由匹配(断言)》Gateway Web Handler》过滤器链》代理服务(可以是 应用)

    启动后,存在3个Handler Mapping——不一定是 Gateway H.M.:

    routePredicateHandlerMapping
    requestMappingHandlerMapping
    resourceHandlerMapping

    Web Handler则有以下Bean:至于包含 Handler 字符的Bean 则有更多

    webHandler
    filteringWebHandler

    过滤器链:

    pre过滤器逻辑,处理请求后,交给代理服务;

    post过滤器逻辑,收到代理服务的响应后执行并返回请求方。

    pre过滤器逻辑可以:鉴权、限流、更改请求头、转换协议等;

    post过滤器逻辑可以:对响应数据进行修改,比如更改响应头、转换协议等。

    1、通过网关服务访问其它应用

    访问 web适配层应用 的接口:http://localhost:21001/user/get?id=1

    注,由于路由的配置特性,将配置文件转为YAML文件会更方便

    对于上面的接口,路由配置如下:

    # 路由配置
    #spring: # 前面有,这里不需要
      cloud:
        gateway:
          routes:
          # 访问 adapter.web
          - id: route1
            uri: http://localhost:21001
            predicates:
            # 严格按照下面的格式来,小于10的话,前加0
            - After=2021-09-11T13:13:13.000+08:00[Asia/Shanghai]

    路由通过 spring.cloud.gateway.routes.* 来配置,routes下每一个都是一个路由规则。

    每一个路由规则,都需要有 id、uri、predicates 三个属性,其中的 predicates为断言。

    上面使用了 After路由断言工厂(Bean名称 afterRoutePredicateFactory),格式要正确,否则无法启动。After的意思是:在这个时间之后的请求都可以使用这条路由——做转发。

    配置后,查看 /actuator/gateway/routes 端点:存在一条路由了。route_id为前面配置的 id。除了上面的3个参数,还可以配置 order、filters 参数。order在存在多个路由规则的时候指定顺序。

    [
        {
            "predicate": "After: 2021-09-11T13:13:13+08:00[Asia/Shanghai]",
            "route_id": "route1",
            "filters": [],
            "uri": "http://localhost:21001",
            "order": 0
        }
    ]

    测试通过网关服务访问路由中指定的服务:访问成功。

    和直接访问 web适配层应用 相比,这里的返回结果 少了2个Header:Keep-Alive、Conection,有什么影响呢?TODO

    在上面的访问中,网关服务 是没有日志输出的。开启调试模式(debug: true),可以看到下面的日志:

    当然,网关打印太多日志会影响服务器性能。

    Spring容器中有哪些断言工厂Bean呢?

    使用的时候,去掉 RoutePredicateFactory,再把首字母大写即可。每一个Bean名称都对应一个工厂类,可以去看源码。

    name=afterRoutePredicateFactory
    name=beforeRoutePredicateFactory
    name=betweenRoutePredicateFactory
    name=cookieRoutePredicateFactory
    name=headerRoutePredicateFactory
    name=hostRoutePredicateFactory
    name=methodRoutePredicateFactory
    name=pathRoutePredicateFactory
    name=queryRoutePredicateFactory
    name=readBodyPredicateFactory
    name=remoteAddrRoutePredicateFactory
    name=weightRoutePredicateFactory
    name=cloudFoundryRouteServiceRoutePredicateFactory

    更多断言试验

    配置After断言后的时间未到,测试结果如下:相比于正常的 路由生效时的 未找到路径,多了 messge、requestId 两个字段。

    点击查看代码
    {
        "timestamp": "2021-09-11T07:27:45.411+00:00",
        "path": "/user/get",
        "status": 404,
        "error": "Not Found",
        "message": null,
        "requestId": "6e46a6fa-8, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:61499"
    }
    正常的路径没找到结果
    {
        "timestamp": "2021-09-11 07:30:51",
        "status": 404,
        "error": "Not Found",
        "path": "/user2/get"
    }

    路由中配置的主机不存在(也可能是服务器故障、重启中等情况,注意,去掉前面配置的路由再做测试):

    错误信息
    响应结果:
    {
        "timestamp": "2021-09-11T07:37:17.437+00:00",
        "path": "/user/get",
        "status": 500,
        "error": "Internal Server Error",
        "requestId": "0d01c5bb-1, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:56362"
    }
    
    异常日志:
    2021-09-11 15:37:17.449 ERROR 26812 --- [ctor-http-nio-5] a.w.r.e.AbstractErrorWebExceptionHandler : [0d01c5bb-1, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:56362]  500 Server Error for HTTP GET "/user/get?id=1"
    io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: no further information: localhost/127.0.0.1:9999
    	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
    Caused by: java.net.ConnectException: Connection refused: no further information

    正常路由、错误路由并存(1):正常的先配置、错误的后配置(下面的配置中,Order注释掉

    正常错误2路由
        #
        # 路由配置
        gateway:
          routes:
          # 访问 adapter.web
          - id: route1
            # 1)服务
            uri: http://localhost:21001
    #        order: 2
            # 2)端口后添加部分路径:无用,和上面效果相同
    #        uri: http://localhost:21001/user
            predicates:
            # 严格按照下面的格式来,小于10的话,前加0
            - After=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]
    
          # 不存在的主机
          - id: routeErr
            uri: http://localhost:9999
    #        order: 1
            predicates:
            # 严格按照下面的格式来,小于10的话,前加0
            - After=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]

    /actuator/gateway/routes 结果:order都是默认值0,且,正常的在前

    响应结果
    [
        {
            "predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
            "route_id": "route1",
            "filters": [],
            "uri": "http://localhost:21001",
            "order": 0
        },
        {
            "predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
            "route_id": "routeErr",
            "filters": [],
            "uri": "http://localhost:9999",
            "order": 0
        }
    ]

    访问请求,成功——。

    正常路由、错误路由并存(2)——使用Order:错误的Order值为1、正常的Order值为2

    将上面配置中的 Order 配置 取消注释

    /actuator/gateway/routes 结果:routeErr 变为在前了

    响应结果
    [
        {
            "predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
            "route_id": "routeErr",
            "filters": [],
            "uri": "http://localhost:9999",
            "order": 1
        },
        {
            "predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
            "route_id": "route1",
            "filters": [],
            "uri": "http://localhost:21001",
            "order": 2
        }
    ]

    访问请求,失败,且发生连接错误。从效果来看,请求都被 错误路由接管了。

    Before断言试验

    时间格式同After断言。

    在时间参数到达前,此路由有效。

    - Before=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]

    /actuator/gateway/routes 结果:predicate变成Before了

    [
        {
            "predicate": "Before: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
            "route_id": "route1",
            "filters": [],
            "uri": "http://localhost:21001",
            "order": 0
        }
    ]

    访问请求,符合预期。

    Between断言试验

    # Between断言: 2个参数
    - Between=2021-09-11T16:13:13.000+08:00[Asia/Shanghai],2021-09-11T17:13:13.000+08:00[Asia/Shanghai]

    /actuator/gateway/routes 结果:

    响应结果
    [
        {
            "predicate": "Between: 2021-09-11T16:13:13+08:00[Asia/Shanghai] and 2021-09-11T17:13:13+08:00[Asia/Shanghai]",
            "route_id": "route1",
            "filters": [],
            "uri": "http://localhost:21001",
            "order": 0
        }
    ]

    访问请求,符合预期。

    配置2个正常路由:

    两个使用After断言的路由,uri相同,但是,route1要到 2221年才生效,而route2才是正常可用的——预期会使用route2来转发请求。

    响应结果
    [
        {
            "predicate": "After: 2221-09-11T14:13:13+08:00[Asia/Shanghai]",
            "route_id": "route1",
            "filters": [],
            "uri": "http://localhost:21001",
            "order": 0
        },
        {
            "predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
            "route_id": "route2",
            "filters": [],
            "uri": "http://localhost:21001",
            "order": 0
        }
    ]

    访问请求,符合预期。

    一个路由配置多个断言

    参数 predicates 是个复数形式,这就意味着,一个路由可以配置多个断言。

    gateway:
          routes:
          # 访问 adapter.web
          - id: route1
            # 1)服务
            uri: http://localhost:21001
            predicates:
            # After断言
            # 严格按照下面的格式来,小于10的话,前加0
            - After=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]
            # Header断言:2个参数,键、值
            - Header=headerParam, 123

    访问 /actuator/gateway/routes:

    [
        {
            "predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Header: headerParam regexp=123)",
            "route_id": "route1",
            "filters": [],
            "uri": "http://localhost:21001",
            "order": 0
        }
    ]

    访问请求:http://localhost:25001/user/get?id=1

    1)不带请求头headerParam=123

    请求失败

    响应结果-失败
    {
        "timestamp": "2021-09-11T08:43:01.274+00:00",
        "path": "/user/get",
        "status": 404,
        "error": "Not Found",
        "message": null,
        "requestId": "d8c568f7-3, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:59011"
    }

    2)带请求头headerParam=123

     请求成功。

    补充:RouteDefinition类部分源码

    @Validated
    public class RouteDefinition {
    
    	// 路由ID
    	private String id;
    
    	// 断言列表
    	@NotEmpty
    	@Valid
    	private List<PredicateDefinition> predicates = new ArrayList<>();
    	
        // 路由过滤器
    	@Valid
    	private List<FilterDefinition> filters = new ArrayList<>();
    
    	// 代理服务地址
    	@NotNull
    	private URI uri;
    
    	// 元数据?
    	private Map<String, Object> metadata = new HashMap<>();
    
    	// 路由顺序:值越小越优先
    	private int order = 0;
        
        // ...省略
    }

    小结,

    本节初步体验了Gateway的路由转发功能,试验了几个断言的用法,还有更多断言等待解锁(Cookie、Header、Host、Method、Path、Query、RemoteAddr等)。

    正如前文所言,断言(和Order)决定了使用哪个路由去处理请求,在路由处理前,要经过路由配置的过滤器处理——这里特指pre过滤器,之后才到达  服务代理(或应用),下一节将介绍过滤器的使用。来自博客园

    2、过滤器使用

    过滤器,从处理对象分为两种:1)pre——过滤请求,2)post——处理响应;从作用范围分为两种:1)针对单个路由的过滤器(GatewayFilter接口)、2)针对所有路由的全局过滤器(GlobalFilter接口)。

    GatewayFilter接口 的实现对象 主要是在 各种GatewayFilterFactory类中实现:

    而 GlobalFilter接口 则有很多直接实现类:来自博客园

    前文配置路由时,提到一个filters参数,便是用来配置 针对单个路由的过滤器的。

    在前面的RouteDefinition类中,filters参数是FilterDefinition列表,而FilterDefinition类只有name、args两个参数:

    @Validated
    public class FilterDefinition {
    
    	// 过滤器名称
    	@NotNull
    	private String name;
    	
        // 过滤器参数
    	private Map<String, String> args = new LinkedHashMap<>();
        
        // ...省略...
    }

    而且,FilterDefinition没有子类。那么,怎么创建FilterDefinition对象的呢FilterDefinition真的有用到?TODO

    在S.C.Gateway中,使用的是各种**GatewayFilterFactory类来创建,比如,AddRequestHeaderGatewayFilterFactory——添加请求头GatewayFilterFactory。

    配置路由时,只需要使用 AddRequestHeader即可——区分大小写。来自博客园

    在Spring容器中,还有以下GatewayFilterFactory Bean(共发现28个):

    GatewayFilterFactory Beans
    name=addRequestHeaderGatewayFilterFactory
    name=mapRequestHeaderGatewayFilterFactory
    name=addRequestParameterGatewayFilterFactory
    name=addResponseHeaderGatewayFilterFactory
    name=modifyRequestBodyGatewayFilterFactory
    name=dedupeResponseHeaderGatewayFilterFactory
    name=modifyResponseBodyGatewayFilterFactory
    name=prefixPathGatewayFilterFactory
    name=preserveHostHeaderGatewayFilterFactory
    name=redirectToGatewayFilterFactory
    name=removeRequestHeaderGatewayFilterFactory
    name=removeRequestParameterGatewayFilterFactory
    name=removeResponseHeaderGatewayFilterFactory
    name=rewritePathGatewayFilterFactory
    name=retryGatewayFilterFactory
    name=setPathGatewayFilterFactory
    name=secureHeadersGatewayFilterFactory
    name=setRequestHeaderGatewayFilterFactory
    name=setRequestHostHeaderGatewayFilterFactory
    name=setResponseHeaderGatewayFilterFactory
    name=rewriteResponseHeaderGatewayFilterFactory
    name=rewriteLocationResponseHeaderGatewayFilterFactory
    name=setStatusGatewayFilterFactory
    name=saveSessionGatewayFilterFactory
    name=stripPrefixGatewayFilterFactory
    name=requestHeaderToRequestUriGatewayFilterFactory
    name=requestSizeGatewayFilterFactory
    name=requestHeaderSizeGatewayFilterFactory

    试验:使用AddRequestHeaderGatewayFilterFactory

            # 过滤器配置
            filters:
            # 区分大小写
            - AddRequestHeader=addHead,abc

    访问/actuator/gateway/routes:filters不再为空了,出现了一个 order=1的filter

    [
        {
            "predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
            "route_id": "route1",
            "filters": [
                "[[AddRequestHeader addHead = 'abc'], order = 1]"
            ],
            "uri": "http://localhost:21001",
            "order": 0
        }
    ]

    测试请求通过请求到 web适配层应用 是否添加了请求头:来自博客园

    使用Postman看不到!

    使用curl命令也看不到:-v 或 --verbose参数!TODO

    看来要去 web适配层应用 做一些改造才是啊!

    改造代码及测试结果
    	// web适配层应用的/user/get接口
        @GetMapping(value="/get")
    	public UserVO getUser(@RequestParam Long id) {
    		if (Objects.isNull(id) || id < 1) {
    			return null;
    		}
    		log.info("getUser, id={}", id);
    		
    		// 测试网关的AddRequestHeader过滤器
    		String addHeadVal = req.getHeader("addHead");
    		log.info("addHeadVal={}", addHeadVal);
    		
    		return userFeign.getUser(id);
    	}
    
    /*
    测试结果:请求头addHead添加成功 日志如下:
    o.l.a.web.controller.UserController      : addHeadVal=abc
    */

    测试通过——AddRequestHeader过滤器生效了。来自博客园

    试验:使用RewritePathGatewayFilterFactory

            # 过滤器配置
            filters:
            - AddRequestHeader=addHead,abc
            # RewritePath过滤器,重写 /web开头的请求——去掉/web
            - RewritePath=/web/(?<segment>.*), /${segment}

    访问 /actuator/gateway/routes:过滤器order=2

    [
        {
            "predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
            "route_id": "route1",
            "filters": [
                "[[AddRequestHeader addHead = 'abc'], order = 1]",
                "[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]"
            ],
            "uri": "http://localhost:21001",
            "order": 0
        }
    ]

    执行结果:请求成功

    : [c3147423-1, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:65469] HTTP GET "/web/user/get?id=1"
    : [c3147423-1, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:65469] Completed 200 OK

    当然,没有被改造的 /user/get?id=1 也请求成功——可以通过 Path断言过滤掉 /user开头的请求:来自博客园

    Path断言使用
            predicates:
    配置中增加 Path断言:
            # After断言
            # 严格按照下面的格式来,小于10的话,前加0
            - After=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]
            # 路径断言:只接受 /web 开头的请求,,配合下面的 RewritePath过滤器一起使用
            - Path=/web/**
            # 过滤器配置
            filters:
            - AddRequestHeader=addHead,abc
            # RewritePath过滤器,重写 /web开头的请求——去掉开头的/web
            - RewritePath=/web/(?<segment>.*), /${segment}
    
    添加 Path断言后,/user/get?id=1 访问失败:
    {
        "timestamp": "2021-09-11T14:25:35.873+00:00",
        "path": "/user/get",
        "status": 404,
        "error": "Not Found",
        "message": null,
        "requestId": "73eb6d32-1, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:62670"
    }

    关于RewritePathGatewayFilterFactory的用法,还没搞懂,需要继续深入。来自博客园

    它可以取代强大的Nginx的rewrite吗?

    试验:自定义过滤器工厂

    内置的过滤器工厂可以满足很多场景的需求了。在不满足更多需求时,可以自定义过滤器或过滤器工厂。

    过滤器工厂的相关接口和抽象类:

    // 接口
    @FunctionalInterface
    public interface GatewayFilterFactory<C> extends ShortcutConfigurable, Configurable<C> {
    }
    
    // 抽象类1 上面顶级接口的直接抽象类:接收1个参数
    public abstract class AbstractGatewayFilterFactory<C> extends AbstractConfigurable<C>
    		implements GatewayFilterFactory<C>, ApplicationEventPublisherAware {
    }
    
    // 抽象类2 继承 上面的 抽象类1:?
    public abstract class AbstractChangeRequestUriGatewayFilterFactory<T> extends AbstractGatewayFilterFactory<T> {
    }
    
    // 抽象类3 继承 上面的 抽象类1:接收2个参数
    public abstract class AbstractNameValueGatewayFilterFactory
    		extends AbstractGatewayFilterFactory<AbstractNameValueGatewayFilterFactory.NameValueConfig> {
    }
    

    本试验展示 过滤器工厂的实现。

    需参考其它内置工厂的实现,实现自己的过滤器工厂

    其中还涉及到reactor的相关内容——Mono类

    自定义过滤器工厂功能:

    记录请求耗时,并根据配置(一个参数-true/false)决定是否输出日志。来自博客园

    实现简介:

    实现AbstractGatewayFilterFactory接口,注册为Spring容器管理的Bean,然后就可以在配置文件中使用了。

    RequestTimeGatewayFilterFactory.java
    package org.lib.external.gateway.filters;
    
    import java.util.Arrays;
    import java.util.List;
    import java.util.Objects;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.cloud.gateway.filter.GatewayFilter;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
    import org.springframework.web.server.ServerWebExchange;
    
    import reactor.core.publisher.Mono;
    
    /**
     * 请求时间日志输出
     * 一个参数:true-输出,false-不输出
     * @author ben
     * @date 2021-09-13 09:45:28 CST
     */
    public class RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> {
    
    	// 为什么是 GatewayFilter.class 而不是 当前类呢?
    	private static final Log log = LogFactory.getLog(GatewayFilter.class);
    	
    	private static final String REQUEST_TIME_BEGIN = "reqTimeBegin";
    	private static final String KEY = "logEnabled";
    
    	// 必须,否则不会输出日志
    	// 为何实现这个函数?
    	@Override
    	public List<String> shortcutFieldOrder() {
    		return Arrays.asList(KEY);
    	}
    	
    	// 默认构造函数
    	public RequestTimeGatewayFilterFactory() {
    		// 必须调用下面的语句,否则抛出 ClassCastException
    		super(Config.class);
    	}
    	
    	@Override
    	public GatewayFilter apply(Config config) {
    		// 匿名类方式(可以转换为 lambda表达式方式——这是个 函数式接口)
    		return new GatewayFilter() {
    
    			@Override
    			public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    				// 添加属性值
    				exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
    				
    				return chain
    						.filter(exchange)
    						.then(
    							Mono.fromRunnable(()->{
    								Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
    								if (config.logEnabled && Objects.nonNull(startTime)) {
    									// 输出日志
    									StringBuilder sb = new StringBuilder();
    									sb.append(exchange.getRequest().getURI().getRawPath())
    									  .append(": ")
    									  .append(System.currentTimeMillis() - startTime)
    									  .append("毫秒");
    									
    									log.info(sb.toString());
    								}
    							})
    						);
    			}};
    	}
    	
    	/**
    	 * RequestTimeGatewayFilter的配置
    	 * @author ben
    	 * @date 2021-09-13 09:43:35 CST
    	 */
    	public static class Config {
    		/**
    		 * true: 输出日志;false:不输出日志
    		 */
    		private boolean logEnabled;
    
    		public boolean isLogEnabled() {
    			return logEnabled;
    		}
    
    		public void setLogEnabled(boolean logEnabled) {
    			this.logEnabled = logEnabled;
    		}
    		
    	}
    	
    }
    
    APPConfig.java
    @Configuration
    public class APPConfig {
    
    	/**
    	 * 注册新增过滤器工厂
    	 * 注册后,即可在配置文件中使用
    	 */
    	@Bean
    	public RequestTimeGatewayFilterFactory RequestTimeGatewayFilterFactory() {
    		return new RequestTimeGatewayFilterFactory();
    	}
    	
    }

    使用RequestTimeGatewayFilterFactory:最后一行的配置,值为true,表示输出日志

            predicates:
            # After断言
            # 严格按照下面的格式来,小于10的话,前加0
            - After=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]
            # 路径断言:只接受 /web 开头的请求,,配合下面的 RewritePath过滤器一起使用
            - Path=/web/**
            # 过滤器配置
            filters:
            - AddRequestHeader=addHead,abc
            # RewritePath过滤器,重写 /web开头的请求——去掉/web
            - RewritePath=/web/(?<segment>.*), /${segment}
            # 自定义过滤器工厂:请求时间日志输出
            - RequestTime=true

    访问/actuator/gateway/routes:

    [
        {
            "predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
            "route_id": "route1",
            "filters": [
                "[[AddRequestHeader addHead = 'abc'], order = 1]",
                "[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]",
                "[org.lib.external.gateway.filters.RequestTimeGatewayFilterFactory$1@19a435c6, order = 3]"
            ],
            "uri": "http://localhost:21001",
            "order": 0
        }
    ]

    自定义路由过滤器工厂 配置成功。来自博客园

    但是,其展示的信息 和 内置过滤器工厂 很不一样,和是否重写ToString()有关系?是的,改造如下:

    	@Override
    	public GatewayFilter apply(Config config) {
    		// 匿名类方式(可以转换为 lambda表达式方式——这是个 函数式接口)
    		return new GatewayFilter() {
    			// ...省略了之前的filter函数...
    
    			// 重写
    			@Override
    			public String toString() {
    				return "[RequestTime logEnabled=" + config.isLogEnabled() + "]";
    			}
    		};
    	}

    改造后访问 /actuator/gateway/routes:改造成功

    [
        {
            "predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
            "route_id": "route1",
            "filters": [
                "[[AddRequestHeader addHead = 'abc'], order = 1]",
                "[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]",
                "[[RequestTime logEnabled=true], order = 3]"
            ],
            "uri": "http://localhost:21001",
            "order": 0
        }
    ]

    测试自定义过滤器工厂是否生效:成功。

    更改工厂类中的 log 的参数:

    配置文件中,值为false的时候是没有日志输出的。

    注,功能虽然实现了,但还需要深入了解才行,静态内部类Config、ServerWebExchange、Mono、GatewayFilterChain等

    注,实现的过滤器工厂是根据 参考文档1 中的实现的,本来是实现一个 是否打印请求参数——query params——的工厂:只要使用就会有日志,只不过是否输出 请求参数,而本文改为了 是否输出日志,设置为false的时候,没有日志、还会影响性能来自博客园

    ,过滤器是一种类型,除了使用过滤器工厂来生产之外,还可以自定义过滤器类——实现GatewayFilter、Ordered接口即可。实现过滤器后,可以通过编码建立路由的方式(本文暂未涉及)使用,或者,建立对应的过滤器工厂使用——此时怎么使用工厂中的配置呢?TODO

    试验:全局过滤器

    前面介绍的过滤器都是 单个路由的过滤器(GatewayFilter),还有一种 全局过滤器(GlobalFilter)——作用在所有路由上。

    对于GatewayFilter,除了可以配置给单个路由使用,也可以通过下面的配置让其全局生效(spring.cloud.gateway.default-filters):

    配置及结果
    # application.yml文件
        #
        # 路由配置
        gateway:
          # 配置2个GatewayFilter全局生效
          default-filters:
          - AddResponseHeader=X-Response-Default-Red, Default-Blue
          - RequestTime=true
          routes:
          ...省略...
    
    # 访问/actuator/gateway/routes
    # 注意 order值,,系统自动排的
    [
        {
            "predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
            "route_id": "route1",
            "filters": [
                "[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
                "[[AddRequestHeader addHead = 'abc'], order = 1]",
                "[[RequestTime logEnabled=true], order = 2]",
                "[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]"
            ],
            "uri": "http://localhost:21001",
            "order": 0
        }
    ]

    测试结果:响应头-已添加、日志-正常。

    全局过滤器的接口 及其实现类 前文展示过了,但在spring容器中有哪些Bean是全局过滤器呢?

    点击查看代码
    # 测试代码 ConfigurableApplicationContext ctx
    String[] beanNames = ctx.getBeanDefinitionNames();
    Arrays.stream(beanNames).forEach((name)->{
        Object bean = ctx.getBean(name);
        if (bean instanceof GlobalFilter) {
            cs.accept("name=" + name);
        }
    });
            
    # 测试结果
    name=routingFilter
    name=nettyWriteResponseFilter
    name=adaptCachedBodyGlobalFilter
    name=removeCachedBodyFilter
    name=routeToRequestUrlFilter
    name=forwardRoutingFilter
    name=forwardPathFilter
    name=websocketRoutingFilter
    name=gatewayMetricFilter
    name=noLoadBalancerClientFilter

    还可以使用 /actuator/gateway/globalfilters 端点查看系统的所有全局过滤器:

    {
        "org.springframework.cloud.gateway.filter.ForwardRoutingFilter@44bd4b0a": 2147483647,
        "org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@1a865273": -1,
        "org.springframework.cloud.gateway.filter.NettyRoutingFilter@26844abb": 2147483647,
        "org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@288ca5f0": -2147483648,
        "org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@4068102e": 10000,
        "org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@1aa6e3c0": -2147482648,
        "org.springframework.cloud.gateway.filter.GatewayMetricsFilter@21079a12": 0,
        "org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@216e0771": 2147483646,
        "org.springframework.cloud.gateway.filter.ForwardPathFilter@6c008c24": 0,
        "org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration$NoLoadBalancerClientFilter@fcc6023": 10150
    }

    GlobalFilter也可以自定义,参考其它实现类,其都实现了 GlobalFilter、Ordered接口。来自博客园

    自定义后,将其注册到Spring容器即可全局生效。

    实现一个GlobalFilter,功能:检查请求头是否有token参数,没有的话,禁止访问系统

    TokenGlobalFilter.java
    package org.lib.external.gateway.filters;
    
    import java.util.Objects;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.http.HttpStatus;
    import org.springframework.util.StringUtils;
    import org.springframework.web.server.ServerWebExchange;
    
    import reactor.core.publisher.Mono;
    
    /**
     * Token检查
     * 功能:请求头 有token 放行;无token 阻止。
     * 进一步:检查token是否有效——结合S.C.Security TODO
     * @author ben
     * @date 2021-09-13 11:46:03 CST
     */
    public class TokenGlobalFilter implements GlobalFilter, Ordered {
    
    	private static final Log log = LogFactory.getLog(TokenGlobalFilter.class);
    	
    	private static final String TOKEN = "token";
    	
    	@Override
    	public int getOrder() {
    		// 参考文档1 中,这里设置为 -100,,两个值都有效,-100的优先级更高
            return 0;
    	}
    
    	@Override
    	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    		String token = exchange.getRequest().getHeaders().getFirst(TOKEN);
    		if (!StringUtils.hasText(token)) {
    			// 没有token 阻止访问
    			log.warn("请求没有token或token无效,禁止访问: url=" + exchange.getRequest().getURI());
    			
    			exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
    			return exchange.getResponse().setComplete();
    		}
    		
    		// 还可以做安全校验 TODO
    		
    		// 放行
    		return chain.filter(exchange);
    	}
    
    }
    
    	/**
    	 * 全局过滤器注册:TokenGlobalFilter
    	 * @author ben
    	 * @date 2021-09-13 11:54:28 CST
    	 * @return
    	 */
    	@Bean
    	public TokenGlobalFilter tokenGlobalFilter() {
    		return new TokenGlobalFilter();
    	}

    测试结果:请求头 没有token 或 token值为空字符串时,输入日志,返回空,,通过。来自博客园

    o.l.e.gateway.filters.TokenGlobalFilter  : 请求没有token或token无效,禁止访问: url=http://localhost:25001/web/user/get?id=1

    访问 /actuator/gateway/globalfilters 端点,可以看到 自定义的全局过滤器:

    {
    ...
    "org.lib.external.gateway.filters.TokenGlobalFilter@312b34e3": 0,
    ...
    }

    本文介绍了:

    1)在配置文件中添加路由;

    2)在配置文件中使用断言;

    3)在配置文件中配置过滤器GatewayFilter;来自博客园

    4)自定义GatewayFilter;

    5)配置GatewayFilter为全局过滤器;

    6)自定义全局过滤器GlobalFilter等内容;

    ...

    基本上可以让S.C.Gateway运行起来了。来自博客园

    不过,Gateway还有更多内容需要研究的,比如,编程方式实现gaeway配置、服务化配合(结合服务注册中心)、实现限流等……

    》》》全文完《《《

    还需要多看官文、源码,这才可以get到更多、更准确的信息。

    使用S.C.Gateway的最佳实践是怎样的呢?待探索、实践。来自博客园

    先看官文,再写博文,效率会更高的。

    参考文档

    1、《深入理解Spring Cloud与微服务构建》

    2019年9月第2版,作者:方志朋

    2、

  • 相关阅读:
    树---对称的二叉树
    树---二叉树的下一个节点
    阿里一面电话面
    树---重建二叉树
    CVTE电话面
    聊聊平台型游戏
    Ogre-next with Visual Studio 2019, SDL Memset, CL.exe error
    中国能不能制作AAA游戏?
    Simple Cel Shading 钟馗
    Modeling -> Mixamo auto rigging -> UE4 retargeting
  • 原文地址:https://www.cnblogs.com/luo630/p/15204968.html
Copyright © 2020-2023  润新知