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、

 

posted @ 2021-09-13 12:18  快乐的凡人721  阅读(919)  评论(0编辑  收藏  举报