关于过滤链设计的实践记录
一、背景
最近,在做一个API网关的小项目,提升Java基础及项目设计能力。对于一个网关来说,过滤链的设计是其核心的设计思想,简单做了一个该网关的过滤链的流程,如下。为了更好地去理解过滤链的设计,该篇文章从基础理论的责任链设计模式 及 应用实践Demo、开源框架相关实践分析 三方面 进行分析记录。
关于过滤链设计的实践记录
一、背景
最近,在做一个API网关的小项目,提升Java基础及项目设计能力。对于一个网关来说,过滤链的设计是其核心的设计思想,简单做了一个该网关的过滤链的流程,如下。为了更好地去理解过滤链的设计,该篇文章从基础理论的责任链设计模式 及 应用实践Demo、开源框架相关实践分析 三方面 进行分析记录。
二、责任链模式
问题背景
在日常生活,我们去做一件事,往往会去梳理各个步骤,并按照各个步骤去执行形成一条链路。如果这件事情是自己一个人的职责,那么恭喜你,你可以很自由地完成该件事情(可以理解为一个类中的方法,可以自己调用则很轻松),但是如果这个链路涉及到不同人,而且人还是不同领域,那么你将会感到复杂,此时你会说没事儿,我可以挨个去找对应的人,没错这样还好。那么进步思考,若每次找的人组合的步骤发生变化,且当你找到该人去负责,该人又让你去找其他人,问题复杂度是不是一下子上来了。
而责任链模式则是解决以上问题,去抽离 **客户端(调用者)**和 具体执行者的职责,调用者无需关心整个链路怎么执行,只需提交一次对应的人即可,剩下的则交给具体执行者的组合(即责任链)去做。
举一个场景例子,工单审批。
对于工单审批是一件比较复杂的事情,而你关心的只是最终结果,例如请假我只关心能否请假成功。具体怎么批的过程无需关心。
代码举例
若想去实现简单的一个请求工单流程,简单示例代码如下所示,表明看这段代码还是十分简洁的没什么大碍。但细致想一想,定义审核流程的业务逻辑是不是全部放在一个方法中了,这样是不是导致该类(客户端或业务类)的职责过大。若我想突然改变这个执行过程,是不是得修改代码,而且也不便于灵活组合,同时需要业务类去组织整个链路的执行过程。
java">boolean audit(WorkerOrder workerOrder){
Leader leader = new Leader();
Manager manager = new Manager();
Boss boss = new Boss();
if(workerOrder.getDay()<3){
return leader.audit(workerOrder);
}else if (workerOrder.getDay()<7){
return leader.audit(workerOrder) && manager.audit(workerOrder);
}else {
return leader.audit(workerOrder) && manager.audit(workerOrder) && boss.audit(workerOrder);
}
}
对上面进行稍微改造一下:
java">@Data
public abstract class AbstractAuditFilter implements AuditHandler<WorkerOrder>{
protected AuditHandler<WorkerOrder> next;
}
java">boolean audit(WorkerOrder workerOrder){
Leader leader = new Leader();
List<AbstractAuditFilter> filterList = new ArrayList<>();
filterList.add(leader);
FilterChain filterChain = FilterChainFactory.build(filterList);
return filterChain.audit(workerOrder);
// Manager manager = new Manager();
// Boss boss = new Boss();
// if(workerOrder.getDay()<3){
// return leader.audit(workerOrder);
// }else if (workerOrder.getDay()<7){
// return leader.audit(workerOrder) && manager.audit(workerOrder);
// }else {
// return leader.audit(workerOrder) && manager.audit(workerOrder) && boss.audit(workerOrder);
// }
}
通过以上简单的改造,可以实现对执行链路的顺序编排,组合。而对于如何编排则可结合动态配置中心进行,定义规则,来加载选取需要执行哪些过滤链,另外也可以通过传参的方式进行。客户端或业务类执行关系选择何种执行链路或规则即可。无需再组织业务逻辑。
概括一下,对应原有的缺点则有以下:
1.业务类比较庞大,各个上级的审批方法都集中在该类中,
违反了 “单一职责原则”
,测试和维护难度大。
2.当需要修改该请假流程,譬如增加当天数大于30天时还需提交给董事长处理,必须修改该类源代码(并重新进行严格地测试),违反了 “开闭原则”
3.该流程缺乏灵活性
,流程确定后不可再修改(除非修改源代码),客户端无法定制流程
但个人认为,若在执行链路并不长,执行的规则也不复杂(链路的业务逻辑相对比较固定),感觉没有必要去使用该设计模式,因为它会带来额外的问题,例如对链条的管理定义,对规则的定义等额外复杂性。[而对于网关这种过滤链、插件较多,同时需要灵活组合,则十分适合该方式]
关键类图
Handler(抽象处理者):它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象,作为其对下家的引用。通过该引用,处理者可以连成一条链。
ConcreteHandler(具体处理者):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发。
关于链路
- 采用数组的方式进行管理整个链路【对于filter则无需去指定执行 next,遍历链表数组并执行即可】
- tomcat的server的filter
- nacos的configMangaer
- 采用next,引用链表的方式进行管理链路【每个filter需要去手动触发next的执行】
业务场景
- 工单审批
- 业务商品审核
- web请求过滤链(Tomat—的Servelet的filter)
- 做非功能性的切面工作
三、相关应用
-
Netty 中的 Pipeline 和 ChannelHandler 通过责任链设计模式来组织代码逻辑
-
Spring Security 使用责任链模式,可以动态地添加或删除责任(处理 request 请求)
-
Spring AOP 通过责任链模式来管理 Advisor
-
Dubbo Filter 过滤器链也是用了责任链模式(链表),可以对方法调用做一些过滤处理,譬如超时(TimeoutFilter),异常(ExceptionFilter),Token(TokenFilter)等
-
Mybatis 中的 Plugin 机制使用了责任链模式,配置各种官方或者自定义的 Plugin,与 Filter 类似,可以在执行 Sql 语句的时候做一些操作
-
nacos利用tomcat的内置filter做过滤:
-
nacos:
Config Filter Chain Management
,com.alibaba.nacos.client.config.filter.impl``ConfigFilterChainManager
[版本1.4.4],后续nacos:ConfigEncryptionFilter
实现该filter了 -
zuul: 采用的数组进行管理
四、源码分析
抓住核心概念:
- filter,hanlder,等定义过滤链的类,具体的执行者( 一般来说按照处理逻辑可以分为 前中后,错误)
- filterchain,责任链,负责编排责任链,filter的组合
- filterFactory,创建责任链(至于创建的时机,则需要因情况而定
- Config:如何去创建责任链,需要一定的配置,或者其他方式就进行创建。
关于服务器设计的概念:
- 找入口启动入口、请求入口
- 加载基础配置(静态固定的信息),动态配置则运行时更新处理
- 找Server的生命周期
Tomcat的过滤链设计
基本流程:
- 过滤器的加载具体是在 ContextConfig 类的 configureContext 方法中,分别加载 filter 和 filterMap 的相关信息,并保存在上下文环境中
- 过滤器的初始化在 StandardContext 类的 startInternal 方法中完成,保存在 filterConfigs 中并存到上下文环境中
- 请求流转到
StandardWrapperValve
时,在 invoke 方法中,会根据过滤器映射配置信息,为每个请求创建对应的 ApplicationFilterChain,其中包含了目标 Servlet 以及对应的过滤器链,并调用过滤器链的 doFilter 方法执行过滤器
- StandardEngine处理请求:一旦请求进入Tomcat,它会被传递到**
StandardEngine
,这是Tomcat中的一个核心组件。StandardEngine
负责根据请求的主机名(Host
)将请求路由到适当的StandardHost
**。- StandardHost选择合适的Context:
StandardHost
会根据请求的上下文路径(Context Path)来选择适当的StandardContext
,这是一个Web应用程序的容器- 请求到达StandardContext:一旦**
StandardContext
选择了适当的Web应用程序,请求将进入该Web应用程序的上下文,然后由StandardContext
**继续处理- Filter链的创建和执行:在**
StandardContext
中,存在一个名为ApplicationFilterChain
的过滤器链。当请求进入Web应用程序的StandardContext
**时,会首先进入这个过滤器链
五、思考总结
- 关于编排链路的方式:默认去编排定义[例如加载SPI插件,再通过插件的优先级排序],通过用户参数去编排
- 加载所有的插件列表,执行时通过规则校验是否执行该插件
- 无加载插件列表,通过参数指定id列表集合,动态编排链路
- 关于filter的职责问题
- 执行部分(通过Request,Response去透传给下游即下一个Filter),OR执行全部
- 对比Aspect的切面操作,前后置的概念(实际上Spring AOP的执行即采用的责任链模式)
- 加载Aspect的配置,通过注解 加入容器
- 通过动态代理实现函数的调用(基于反射机制),包装前后置操作
- 通过责任链的方式进行执行
六、相关参考
- 图灵学院(强烈推荐):https://baijiahao.baidu.com/s?id=1660719391207352664&wfr=spider&for=pc