spring cloud项目06:网关(Gateway)(1)
JAVA 8
spring boot 2.5.2
spring cloud 2020.0.3
---
授人以渔:
最新版本,下载下来,以便查阅。
更多版本的官方文档:
https://docs.spring.io/spring-cloud/docs/
没有PDF版本,把网页保存下来。
本文使用的项目:
主要路径:前端请求经过 external.gateway 转发到 adapter.web。在此过程中,会做一些试验。
external.gateway | 网关服务 | 端口 25001 |
adapter.web | web适配层应用 | 端口 21001 |
data.user | user数据层应用 | 端口 20001 |
eureka.server | Eureka注册中心 | 端口 10001 |
目录
建立项目,引入 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.的官文中会有详情(下图来自官网)。
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过滤器逻辑可以:对响应数据进行修改,比如更改响应头、转换协议等。
访问 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过滤器,之后才到达 服务代理(或应用),下一节将介绍过滤器的使用。来自博客园
过滤器,从处理对象分为两种: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、