Spring是如何使用责任链模式的?
1
外部控制模式
对于外部控制的方式,这种方式比较简单,链的每个节点只需要专注于各自的逻辑即可,而当前节点调用完成之后是否继续调用下一个节点,这个则由外部控制逻辑进行。
这里我们以一个过滤器的实现逻辑为例进行讲解,在平常工作中,我们经常需要根据一系列的条件对某个东西进行过滤。
比如任务服务的设计,在执行某个任务时,其需要经过诸如时效性检验,风控拦截,任务完成次数等过滤条件的检验之后才能判断当前任务是否能够执行,只有在所有的过滤条件都完成之后,我们才能执行该任务。那么这里我们就可以抽象出一个Filter
接口,其设计如下:
public interface Filter {
/**
* 用于对各个任务节点进行过滤
*/
boolean filter(Task task);
}
这里的Filter.filter()
方法只有一个参数Task
,主要就是控制当前task是否需要被过滤掉,其有一个boolean类型的返回值,通过该返回值以告知外部控制逻辑是否需要将该task过滤掉。对于该接口的子类,我们只需要将其声明为Spring所管理的一个bean即可:
// 时效性检验
@Component
public class DurationFilter implements Filter {
@Override
public boolean filter(Task task) {
System.out.println("时效性检验");
return true;
}
}
// 风控拦截
@Component
public class RiskFilter implements Filter {
@Override
public boolean filter(Task task) {
System.out.println("风控拦截");
return true;
}
}
// 次数限制校验
@Component
public class TimesFilter implements Filter {
@Override
public boolean filter(Task task) {
System.out.println("次数限制检验");
return true;
}
}
上面我们模拟声明了三个Filter
的子类,用于设计一系列的控制当前task是否需要被过滤的逻辑,结构上的逻辑其实比较简单,主要就是需要将其声明为Spring所管理的一个bean。下面是我们的控制逻辑:
@Service
public class ApplicationService {
@Autowired
private List<Filter> filters;
public void mockedClient() {
Task task = new Task(); // 这里task一般是通过数据库查询得到的
for (Filter filter : filters) {
if (!filter.filter(task)) {
return;
}
}
// 过滤完成,后续是执行任务的逻辑
}
}
在上述的控制逻辑中,对于过滤器的获取,只需要通过Spring的自动注入即可,这里注入的是一个List<Filter>
,也就是说,如果我们有新的Filter
实例需要参与责任链的过滤,只需要将其声明为一个Spring容器所管理的bean即可。
这种责任链设计方式的优点在于链的控制比较简单,只需要实现一个统一的接口即可,其基本上能够满足大部分的逻辑控制,但是对于某些需要动态调整链的需求其就无能为力了。
比如在执行到某个节点之后需要动态的判断是否执行下一个节点,或者说要执行某些分叉的节点等等。这个时候我们就需要将链节点的传递工作交由各个节点进行。
2
节点控制模式
对于节点控制调用的方式,其主要有三个控制点:Handler,HandlerContext和Pipeline。Handler中是用于编写具体的业务代码的;HandlerContext则主要是用于对Handler进行包裹,并且用于控制进行下一个节点的调用的。
Pipeline则主要是用于控制整体的流程调用的,比如对于任务的执行,其有任务的查询,任务的过滤和执行任务等等流程,这些流程整体的逻辑控制就是由Pipeline来控制的,在每个流程中又包含了一系列的子流程,这些子流程则是由一个个的HandlerContext和Handler进行梳理的。这种责任链的控制方式整体逻辑如下图所示:
从图中可以看出,我们将整个流程通过Pipeline
对象进行了抽象,这里主要分为了三个步骤:查询task,过滤task和执行task。在每个步骤中,我们都使用了一系列的链式调用。
图中需要注意的是,在每次调用链的下一个节点的时候,我们都是通过具体的Handler进行的,也就是说是否进行链的下一个节点的调用,我们是通过业务实现方来进行动态控制的。
关于该模式的设计,我们首先需要强调的就是Handler
接口的设计,其设计如下所示:
public interface Handler {
/**
* 处理接收到前端请求的逻辑
*/
default void receiveTask(HandlerContext ctx, Request request) {
ctx.fireTaskReceived(request);
}
/**
* 查询到task之后,进行task过滤的逻辑
*/
default void filterTask(HandlerContext ctx, Task task) {
ctx.fireTaskFiltered(task);
}
/**
* task过滤完成之后,处理执行task的逻辑
*/
default void executeTask(HandlerContext ctx, Task task) {
ctx.fireTaskExecuted(task);
}
/**
* 当实现的前面的方法抛出异常时,将使用当前方法进行异常处理,这样可以将每个handler的异常
* 都只在该handler内进行处理,而无需额外进行捕获
*/
default void exceptionCaught(HandlerContext ctx, Throwable e) {
throw new RuntimeException(e);
}
/**
* 在整个流程中,保证最后一定会执行的代码,主要是用于一些清理工作
*/
default void afterCompletion(HandlerContext ctx) {
ctx.fireAfterCompletion(ctx);
}
}
这里的Handler
接口主要是对具体的业务逻辑的一个抽象,对于该Handler
主要有如下几点需要说明:
在前面图中
Pipeline
的每个层级中对应于该Handler
都有一个方法,在需要进行具体的业务处理的时候,用户只需要声明一个bean,具体实现某个当前业务所需要处理的层级的方法即可,而无需管其他的逻辑;每个层级的方法中,第一个参数都是一个
HandlerContext
类型的,该参数主要是用于进行流程控制的,比如是否需要将当前层级的调用链往下继续传递,这里链的传递工作主要是通过ctx.fireXXX()
方法进行的;每个层级的方法都有默认实现,默认实现方式就是将链的调用继续往下进行传递;
每个
Handler
中都有一个exceptionCaught()
方法和afterCompletion()
方法,这两个方法分别用于异常控制和所有调用完成后的清理的,这里的异常控制主要是捕获当前Handler
中的异常,而afterCompletion()
方法则会保证在所有步骤之后一定会进行调用的,无论是否抛出异常;对于
Handler
的使用,我们希望能够达到的目的是,适用方只需要实现该接口,并且使用某个注解来将其标志为Spring
的bean即可,而无需管整个Pipeline
的组装和流程控制。通过这种方式,我们即保留了每个Spring提供给我们的便利性,也使用了Pipeline
模式的灵活性。
上述流程代码中,我们注意到,每个层级的方法中都有一个HandlerContext
用于传递链相关的控制信息,这里我们来看一下其源码:
@Component
@Scope("prototype")
public class HandlerContext {
HandlerContext prev;
HandlerContext next;
Handler handler;
private Task task;
public void fireTaskReceived(Request request) {
invokeTaskReceived(next(), request);
}
/**
* 处理接收到任务的事件
*/
static void invokeTaskReceived(HandlerContext ctx, Request request) {
if (ctx != null) {
try {
ctx.handler().receiveTask(ctx, request);
} catch (Throwable e) {
ctx.handler().exceptionCaught(ctx, e);
}
}
}
public void fireTaskFiltered(Task task) {
invokeTaskFiltered(next(), task);
}
/**
* 处理任务过滤事件
*/
static void invokeTaskFiltered(HandlerContext ctx, Task task) {
if (null != ctx) {
try {
ctx.handler().filterTask(ctx, task);
} catch (Throwable e) {
ctx.handler().exceptionCaught(ctx, e);
}
}
}
public void fireTaskExecuted(Task task) {
invokeTaskExecuted(next(), task);
}
/**
* 处理执行任务事件
*/
static void invokeTaskExecuted(HandlerContext ctx, Task task) {
if (null != ctx) {
try {
ctx.handler().executeTask(ctx, task);
} catch (Exception e) {
ctx.handler().exceptionCaught(ctx, e);
}
}
}
public void fireAfterCompletion(HandlerContext ctx) {
invokeAfterCompletion(next());
}
static void invokeAfterCompletion(HandlerContext ctx) {
if (null != ctx) {
ctx.handler().afterCompletion(ctx);
}
}
private HandlerContext next() {
return next;
}
private Handler handler() {
return handler;
}
}
在HandlerContext
中,我们需要说明如下几点:
之前
Handler
接口默认实现的ctx.fireXXX()
方法,在这里都委托给了对应的invokeXXX()
方法进行调用,而且我们需要注意到,在传递给invokeXXX()
方法的参数里,传入的HandlerContext
对象都是通过next()
方法获取到的。也就是说我们在Handler
中调用ctx.fireXXX()
方法时,都是在调用当前handler的下一个handler对应层级的方法,通过这种方式我们就实现了链的往下传递。在上一点中我们说到,在某个
Handler
中如果想让链往下传递,只需要调用ctx.fireXXX()
方法即可,也就是说,如果我们在某个Handler
中,如果根据业务,当前层级已经调用完成,而无需调用后续的Handler
,那么我们就不需要调用ctx.fireXXX()
方法即可;在
HandlerContext
中,我们也实现了invokeXXX()
方法,该方法的主要作用是供给外部的Pipeline
进行调用的,以开启每个层级的链;在每个
invokeXXX()
方法中,我们都使用try…catch将当前层级的调用抛出的异常给捕获了,然后调用ctx.handler().exceptionCaught()
方法处理该异常,这也就是我们前面说的,如果想处理当前Handler
中的异常,只需要实现该Handler
中的exceptionCaught()
方法即可,异常捕获流程就是在这里的HandlerContext
中进行处理的;在
HandlerContext
的声明处,我们需要注意到,其使用了@Component
和@Scope("prototype")
注解进行标注了,这说明我们的HandlerContext
是由Spring所管理的一个bean,并且由于我们每一个Handler
实际上都由一个HandlerContext
维护着,所以这里必须声明为prototype
类型。通过这种方式,我们的HandlerContext
也就具备了诸如Spring相关的bean的功能,也就能够根据业务需求进行一些额外的处理了;
前面我们讲解了Handler
和HandlerContext
的具体实现,以及实现的过程中需要注意的问题,下面我们就来看一下进行流程控制的Pipeline
是如何实现的,如下是Pipeline
接口的定义:
public interface Pipeline {
Pipeline fireTaskReceived();
Pipeline fireTaskFiltered();
Pipeline fireTaskExecuted();
Pipeline fireAfterCompletion();
}
这里 主要是定义了一个Pipeline
接口,该接口定义了一系列的层级调用,是每个层级的入口方法。
如下是该接口的一个实现类:
@Component("pipeline")
@Scope("prototype")
public class DefaultPipeline implements Pipeline, ApplicationContextAware, InitializingBean {
// 创建一个默认的handler,将其注入到首尾两个节点的HandlerContext中,其作用只是将链往下传递
private static final Handler DEFAULT_HANDLER = new Handler() {};
// 将ApplicationContext注入进来的主要原因在于,HandlerContext是prototype类型的,因而需要
// 通过ApplicationContext.getBean()方法来获取其实例
private ApplicationContext context;
// 创建一个头结点和尾节点,这两个节点内部没有做任何处理,只是默认的将每一层级的链往下传递,
// 这里头结点和尾节点的主要作用就是用于标志整个链的首尾,所有的业务节点都在这两个节点中间
private HandlerContext head;
private HandlerContext tail;
// 用于业务调用的request对象,其内部封装了业务数据
private Request request;
// 用于执行任务的task对象
private Task task;
// 最初始的业务数据需要通过构造函数传入,因为这是驱动整个pipeline所需要的数据,
// 一般通过外部调用方的参数进行封装即可
public DefaultPipeline(Request request) {
this.request = request;
}
// 这里我们可以看到,每一层级的调用都是通过HandlerContext.invokeXXX(head)的方式进行的,
// 也就是说我们每一层级链的入口都是从头结点开始的,当然在某些情况下,我们也需要从尾节点开始链
// 的调用,这个时候传入tail即可。
@Override
public Pipeline fireTaskReceived() {
HandlerContext.invokeTaskReceived(head, request);
return this;
}
// 触发任务过滤的链调用
@Override
public Pipeline fireTaskFiltered() {
HandlerContext.invokeTaskFiltered(head, task);
return this;
}
// 触发任务执行的链执行
@Override
public Pipeline fireTaskExecuted() {
HandlerContext.invokeTaskExecuted(head, task);
return this;
}
// 触发最终完成的链的执行
@Override
public Pipeline fireAfterCompletion() {
HandlerContext.invokeAfterCompletion(head);
return this;
}
// 用于往Pipeline中添加节点的方法,读者朋友也可以实现其他的方法用于进行链的维护
void addLast(Handler handler) {
HandlerContext handlerContext = newContext(handler);
tail.prev.next = handlerContext;
handlerContext.prev = tail.prev;
handlerContext.next = tail;
tail.prev = handlerContext;
}
// 这里通过实现InitializingBean接口来达到初始化Pipeline的目的,可以看到,这里初始的时候
// 我们通过ApplicationContext实例化了两个HandlerContext对象,然后将head.next指向tail节点,
// 将tail.prev指向head节点。也就是说,初始时,整个链只有头结点和尾节点。
@Override
public void afterPropertiesSet() throws Exception {
head = newContext(DEFAULT_HANDLER);
tail = newContext(DEFAULT_HANDLER);
head.next = tail;
tail.prev = head;
}
// 使用默认的Handler初始化一个HandlerContext
private HandlerContext newContext(Handler handler) {
HandlerContext context = this.context.getBean(HandlerContext.class);
context.handler = handler;
return context;
}
// 注入ApplicationContext对象
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.context = applicationContext;
}
}
关于DefaultPipeline
的实现,主要有如下几点需要说明:
DefaultPipeline
使用@Component
和@Scope("prototype")
注解进行了标注,前一个注解用于将其声明为一个Spring容器所管理的bean,而后一个注解则用于表征DefaultPipeline
是一个多例类型的,很明显,这里的Pipeline
是有状态的。这里需要进行说明的是,"有状态"主要是因为我们可能会根据业务情况动态的调整个链的节点情况,而且这里的Request
和Task
对象都是与具体的业务相关的,因而必须声明为prototype
类型;上面的示例中,
Request
对象是通过构造Pipeline
对象的时候传进来的,而Task
对象则是在Pipeline
的流转过程中生成的,这里比如通过完成fireTaskReceived()
链的调用之后,就需要通过外部请求Request
得到一个Task
对象,从而进行整个Pipeline
的后续处理;
这里我们已经实现了Pipeline
,HandlerContext
和Handler
,知道这些bean都是被Spring所管理的bean,那么我们接下来的问题主要在于如何进行整个链的组装。
这里的组装方式比较简单,其主要需要解决两个问题:
对于后续写业务代码的人而言,其只需要实现一个
Handler
接口即可,而无需处理与链相关的所有逻辑,因而我们需要获取到所有实现了Handler
接口的bean;将实现了
Handler
接口的bean通过HandlerContext
进行封装,然后将其添加到Pipeline
中。
这里的第一个问题比较好处理,因为通过ApplicationContext就可以获取实现了某个接口的所有bean,而第二个问题我们可以通过声明一个实现了BeanPostProcessor接口的类来实现。如下是其实现代码:
@Component
public class HandlerBeanProcessor implements BeanPostProcessor, ApplicationContextAware {
private ApplicationContext context;
// 该方法会在一个bean初始化完成后调用,这里主要是在Pipeline初始化完成之后获取所有实现了
// Handler接口的bean,然后通过调用Pipeline.addLast()方法将其添加到pipeline中
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof DefaultPipeline) {
DefaultPipeline pipeline = (DefaultPipeline) bean;
Map<String, Handler> handlerMap = context.getBeansOfType(Handler.class);
handlerMap.forEach((name, handler) -> pipeline.addLast(handler));
}
return bean;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.context = applicationContext;
}
}
这里我们整个链的维护工作就已经完成,可以看到,现在基本上已经实现了前面图中整个链式流程的控制。
这里需要说明的一点是,上面的HandlerBeanProcessor.postProcessAfterInitialization()
方法的执行是在InitializingBean.afterPropertySet()
方法之后执行的,也就是说这里HandlerBeanProcessor
在执行时,整个Pipeline
是已经初始化完成了的。
下面我们来看一下外部客户端如何进行整个链是流程的控制:
@Service
public class ApplicationService {
@Autowired
private ApplicationContext context;
public void mockedClient() {
Request request = new Request(); // request一般是通过外部调用获取
Pipeline pipeline = newPipeline(request);
try {
pipeline.fireTaskReceived();
pipeline.fireTaskFiltered();
pipeline.fireTaskExecuted();
} finally {
pipeline.fireAfterCompletion();
}
}
private Pipeline newPipeline(Request request) {
return context.getBean(DefaultPipeline.class, request);
}
}
这里我们模拟了一个客户端的调用,首先创建了一个Pipeline
对象,然后依次调用其各个层级的方法,并且这里我们使用try…finally结构来保证Pipeline.fireAfterCompletion()
方法一定会执行。
如此我们就完成了整个责任链模式的构造。这里我们使用前面用到的时效性过滤的filter来作为示例来实现一个Handler
:
@Component
public class DurationHandler implements Handler {
@Override
public void filterTask(HandlerContext ctx, Task task) {
System.out.println("时效性检验");
ctx.fireTaskFiltered(task);
}
}
关于这里的具体业务Handler
我们需要说明的有如下几点:
该
Handler
必须使用@Conponent
注解来将其声明为Spring容器所管理的一个bean,这样我们前面实现的HandlerBeanProcessor
才能将其动态的添加到整个Pipeline
中;在每个
Handler
中,需要根据当前的业务需要来实现具体的层级方法,比如这里是进行时效性检验,就是"任务过滤"这一层级的逻辑,因为时效性检验通过我们才能执行这个task,因而这里需要实现的是Handler.filterTask()
方法,如果我们需要实现的是执行task的逻辑,那么需要实现的就是Handler.executeTask()
方法;在实现完具体的业务逻辑之后,我们可以根据当前的业务需要看是否需要将当前层级的链继续往下传递,也就是这里的
ctx.fireTaskFiltered(task);
方法的调用,我们可以看前面HandlerContext.fireXXX()
方法就是会获取当前节点的下一个节点,然后进行调用。如果根据业务需要,不需要将链往下传递,那么就不需要调用ctx.fireTaskFiltered(task);
;
3
小结
如此,我们就通过两种方式实现了责任链模式,而且我们实现的责任链模式都是符合"开-闭"原则的,也就是说后续我们要为链添加新的节点的时候,只需要根据规范实现相应的接口即可,而无需处理链的维护相关的工作。
关于第二种实现方式,这里我们并没有实现链节点的顺序控制功能,以及如何动态的添加或删除链的节点,更有甚者,如果控制每个Handler是单例的还是多例的。
当然,有了前面的框架,这些点实现起来也比较简单,这里权当起到一个抛砖引玉的作用,读者朋友可根据自己的需要进行实现。
作者:爱宝贝丶
来源:my.oschina.net/zhangxufeng/blog/3055328
点击「阅读原文」和栈长学更多~
Spring是如何使用责任链模式的?的更多相关文章
- Spring 设计模式之责任链模式
[应用] 以下是一段代码,Spring MVC 的 diapatcherServlet 的 doDispatch 方法中,获取与请求匹配的处理器(HandlerExecutionChain) getH ...
- 【责任链模式】责任链模式结合Spring实战Demo
备注: 责任链与策略模式有很多相似之处,如都是行为型设计模式,都能够处理代码中的if-else逻辑 主要区别在于: 策略模式 封装了算法,通过上下文对象去接受客户端的数据,根据数据类型执行不同的算法 ...
- 设计模式之jdk动态代理模式、责任链模式-java实现
设计模式之JDK动态代理模式.责任链模式 需求场景 当我们的代码中的类随着业务量的增大而不断增大仿佛没有尽头时,我们可以考虑使用动态代理设计模式,代理类的代码量被固定下来,不会随着业务量的增大而增大. ...
- java责任链模式及项目实际运用
1.前言 上次我们认识了java责任链模式的设计,那么接下来将给大家展示责任链模式项目中的实际运用.如何快速搭建责任链模式的项目中运用. 2.简单技术准备 我们要在项目中使用借助这样的几个知识的组合运 ...
- Java实现责任链模式
责任链模式: 将接受者对象连成一条链,并在该链上传递请求,直到一个几首这对象处理它.通过让更多对象有机会处理请求,避免了请求发送者和接受者之间的耦合. 责任链模式的优缺点: 优点:高内聚,低耦合.业务 ...
- Java描述设计模式(15):责任链模式
本文源码:GitHub·点这里 || GitEE·点这里 一.生活场景描述 1.请假审批流程 公司常见的请假审批流程:请假天数 当 day<=3 天,项目经理审批 当 3<day<= ...
- Java设计模式13:责任链模式
前言 来菜鸟这个大家庭10个月了,总得来说比较融入了环境,同时在忙碌的工作中也深感技术积累不够,在优秀的人身边工作必须更加花时间去提升自己的技术能力.技术视野,所以开一个系列文章,标题就轻松一点叫做最 ...
- java设计模式解析(11) Chain责任链模式
设计模式系列文章 java设计模式解析(1) Observer观察者模式 java设计模式解析(2) Proxy代理模式 java设计模式解析(3) Factory工厂模式 java设计模式解析(4) ...
- 拦截器——原理(AOP、责任链模式、配置)
1.Struts2拦截器概述: (1)Struts2框架封装中封装了很多功能,这些功能其实都是封装在Struts2的拦截器里面,Struts2里面有很多拦截器,每次不是这些拦截器都执行,每次只执行默认 ...
随机推荐
- Visual Studio 2008:路径设置
造冰箱的大熊猫,本文适用于Visual Studio 2008中文版@cnblogs 2018/11/30 1.头文件路径设置 如果头文件所在路径未在环境变量中定义,编译时会出现C1083错误,提示无 ...
- luoguP3353 在你窗外闪耀的星星
P3353 在你窗外闪耀的星星 题目描述 飞逝的的时光不会模糊我对你的记忆.难以相信从我第一次见到你以来已经过去了3年.我仍然还生动地记得,3年前,在美丽的集美中学,从我看到你微笑着走出教室,你将头向 ...
- H5 video全屏与取消全屏兼容
H5 video全屏与取消全屏各浏览器兼容, requestFullscreen()全屏方法,exitFullscreen()退出全屏方法.兼容各个浏览器与css3兼容一样加个前缀即可. // 全屏 ...
- JS框架_(JQuery.js)模拟刮奖
百度云盘:传送门 密码:6p5q 纯CSS模拟刮奖效果 <!DOCTYPE html> <html lang="en"> <head> < ...
- [CSP-S模拟测试]:你相信引力吗(单调栈)
题目传送门(内部题124) 输入格式 第一行一个整数$n$代表环的长度. 第二行$n$个整数表示每个冰锥的高度. 输出格式 一行一个整数表示有多少对冰锥是危险的. 样例 样例输入1: 51 2 4 5 ...
- Spring boot之添加JSP支持
大纲 (1) 创建Maven web project: (2) 在pom.xml文件添加依赖 (3) 配置application.properties支持jsp (4) 编写测试Controller ...
- Spring Boot使用阿里云证书启用HTTPS
1.到阿里云下载证书页面下载证书 2.根据页面内容,可以使用2种证书:PFX JKS 把对应证书放到src/main/resources目录下 在application.properties文件中加入 ...
- linux iconv文件编码转换
linux系统里提供的文件转化编码的命令iconv,例如: iconv -t utf-8 -f gb2312 -c test.xml > text_UTF8.xml 1 -f 源编码-t 目标编 ...
- 一、基础篇--1.1Java基础-MVC设计思想
MVC简介: MVC(Model View Controller) 是模型(model)-视图(view)-控制器(controller)的缩写.一种软件设计典范,用一种业务逻辑.数据.界面显示分离的 ...
- leetcode 140 单词拆分2 word break II
单词拆分2,递归+dp, 需要使用递归,同时使用记忆化搜索保存下来结果,c++代码如下 class Solution { public: //定义一个子串和子串拆分(如果有的话)的映射 unorder ...