世界上最遥远的距离,不是生与死,而是它从你的世界路过无数次,你却选择视而不见,你无情,你冷酷啊 ……
被你忽略的就是责任链设计模式,希望它再次经过你身旁你会猛的发现,并对它微微一笑 ……
责任链设计模式介绍
抽象介绍
初次见面,了解表象,深入交流之后(看完文中的 demo 和框架中的实际应用后),你我便是灵魂之交(重新站在上帝视角来理解这个概念会更加深刻)
责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。在这种模式中,通常每个接收者都包含对另一个接收者的
引用
。如果一个对象能或不能处理该请求,它都会把相同的请求传给下一个接收者,依此类推,直至责任链结束。
接下来将概念图形化,用大脑图形处理区理解此概念
- 上图左侧的 UML 类图中,Sender 类不直接引用特定的接收器类。相反,Sender 引用 Handler 接口来处理请求
handler.handleRequest()
,这使得 Sender 独立于具体的接收器(概念当中说的解耦
)Receiver1,Receiver2 和 Receiver3 类通过处理或转发请求来实现 Handler 接口(取决于运行时条件) - 上图右侧的 UML 序列图显示了运行时交互,在此示例中,Sender 对象在 receiver1 对象(类型为 Handler)上调用
handleRequest()
,接收器 1 将请求转发给接收器 2,接收器 2 又将请求转发到处理(执行)请求的接收器 3
具象介绍
大家小时候都玩过击鼓传花的游戏,游戏的每个参与者就是责任链中的一个处理对象,花球就是待处理的请求,花球就在责任链(每个参与者中)进行传递,只不过责任链的结束时间点是鼓声的结束. 来看 Demo 和实际案例
Demo 设计
程序猿和 log 是老交情了,使用 logback 配置日志的时候有 ConsoleAppender 和 RollingFileAppender,这两个 Appender 就组成了一个 log 记录的责任链。下面的 demo 就是模拟 log 记录:ConsoleLogger 打印所有级别的日志;EmailLogger 记录特定业务级别日志;FileLogger 中只记录 warning 和 Error 级别的日志
抽象概念介绍中,说过实现责任链要有一个抽象接收器接口,和具体接收器,demo 中 Logger
就是这个抽象接口,由于该接口是 @FunctionalInterface
(函数式接口), 它的具体实现就是 Lambda 表达式,关键代码都已做注释标注
import java.util.Arrays;
import java.util.EnumSet;
import java.util.function.Consumer;
@FunctionalInterface
public interface Logger {
/**
* 枚举 log 等级
*/
public enum LogLevel {
// 定义 log 等级
INFO, DEBUG, WARNING, ERROR, FUNCTIONAL_MESSAGE, FUNCTIONAL_ERROR;
public static LogLevel[] all() {return values();
}
}
/**
* 函数式接口中的唯一抽象方法
* @param msg
* @param severity
*/
abstract void message(String msg, LogLevel severity);
default Logger appendNext(Logger nextLogger) {return (msg, severity) -> {
// 前序 logger 处理完才用当前 logger 处理
message(msg, severity);
nextLogger.message(msg, severity);
};
}
static Logger logger(LogLevel[] levels, Consumer<String> writeMessage) {EnumSet<LogLevel> set = EnumSet.copyOf(Arrays.asList(levels));
return (msg, severity) -> {
// 判断当前 logger 是否能处理传递过来的日志级别
if (set.contains(severity)) {writeMessage.accept(msg);
}
};
}
static Logger consoleLogger(LogLevel... levels) {return logger(levels, msg -> System.err.println("写到终端:" + msg));
}
static Logger emailLogger(LogLevel... levels) {return logger(levels, msg -> System.err.println("通过邮件发送:" + msg));
}
static Logger fileLogger(LogLevel... levels) {return logger(levels, msg -> System.err.println("写到日志文件中:" + msg));
}
public static void main(String[] args) {
/**
* 构建一个固定顺序的链【终端记录——邮件记录——文件记录】* consoleLogger:终端记录,可以打印所有等级的 log 信息
* emailLogger:邮件记录,打印功能性问题 FUNCTIONAL_MESSAGE 和 FUNCTIONAL_ERROR 两个等级的信息
* fileLogger:文件记录,打印 WARNING 和 ERROR 两个等级信息
*/
Logger logger = consoleLogger(LogLevel.all())
.appendNext(emailLogger(LogLevel.FUNCTIONAL_MESSAGE, LogLevel.FUNCTIONAL_ERROR))
.appendNext(fileLogger(LogLevel.WARNING, LogLevel.ERROR));
// consoleLogger 可以记录所有 level 的信息
logger.message("进入到订单流程,接收到参数,参数内容为 XXXX", LogLevel.DEBUG);
logger.message("订单记录生成.", LogLevel.INFO);
// consoleLogger 处理完,fileLogger 要继续处理
logger.message("订单详细地址缺失", LogLevel.WARNING);
logger.message("订单省市区信息缺失", LogLevel.ERROR);
// consoleLogger 处理完,emailLogger 继续处理
logger.message("订单短信通知服务失败", LogLevel.FUNCTIONAL_ERROR);
logger.message("订单已派送.", LogLevel.FUNCTIONAL_MESSAGE);
}
}
ConsoleLogger、EmailLogger 和 FileLogger 组成一个责任链,分工明确;FileLogger 中包含 EmailLogger 的引用,EmailLogger 中包含 ConsoleLogger 的引用,当前具体 Logger 是否记录日志的判断条件是传入的 log level 是否在它的责任范围内. 最终调用 message 方法时的责任链顺序 ConsoleLogger -> EmailLogger -> FileLogger
. 如果不能很好的理解 Lambda,我们可以通过接口与实现类的方式实现
案例介绍
为什么说责任链模式从我们身边路过无数次,你却忽视它,看下面这两个案例,你也许会一声长叹.
Filter 过滤器
下面这段代码有没有很熟悉,没错,我们配置拦截器重写 doFilter
方法时都会执行下面这段代码,传递给下一个 Filter 进行处理
chain.doFilter(request, response);
随意定义一个拦截器 CustomFilter,都要执行 chain.doFilter(request, response)
方法进行 Filter 链的传递
import javax.servlet.*;
import java.io.IOException;
/**
* @author tan 日拱一兵
* @date 2019-06-19 13:45
*/
public class CustomFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException { }
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {chain.doFilter(request, response);
}
@Override
public void destroy() {}
}
以 debug 模式启动应用,随意请求一个没有被加入 filter 白名单的接口,都会看到如下的调用栈信息:
红色标记框的内容是 Tomcat 容器设置的责任链,从 Engine 到 Cotext 再到 Wrapper 都是通过这个责任链传递请求,如下类图所示,他们都实现了 Valve 接口中的 invoke 方法
但这并不是这里要说明的重点,这里要看的是和我们自定义 Filter 息息相关的蓝色框的内容 ApplicationFilterChain,我们要了解它是如何应用责任链设计模式的?
既然是责任链,所有的过滤器是怎样加入到这个链条当中的呢?
ApplicationFilterChain
类中定义了一个ApplicationFilterConfig
类型的数组,用来保存过滤器
/**
* Filters.
*/
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
ApplicationFilterConfig 是什么?
ApplicationFilterConfig 是 Filter 的容器,类的描述是:在 web 第一次启动的时候管理 filter 的实例化
/**
* Implementation of a <code>javax.servlet.FilterConfig</code> useful in
* managing the filter instances instantiated when a web application
* is first started.
*
* @author Craig R. McClanahan
*/
ApplicationFilterConfig[] 是一个大小为 0 的空数组,那它在什么时候被重新赋值的呢?
是在 ApplicationFilterChain 类调用
addFilter
的时候重新赋值的
/**
* The int which gives the current number of filters in the chain.
*/
private int n = 0;
public static final int INCREMENT = 10;
/**
* Add a filter to the set of filters that will be executed in this chain.
*
* @param filterConfig The FilterConfig for the servlet to be executed
*/
void addFilter(ApplicationFilterConfig filterConfig) {
// Prevent the same filter being added multiple times
for(ApplicationFilterConfig filter:filters)
if(filter==filterConfig)
return;
if (n == filters.length) {ApplicationFilterConfig[] newFilters =
new ApplicationFilterConfig[n + INCREMENT];
System.arraycopy(filters, 0, newFilters, 0, n);
filters = newFilters;
}
filters[n++] = filterConfig;
}
变量 n 用来记录当前过滤器链里面拥有的过滤器数目,默认情况下 n 等于 0,ApplicationFilterConfig 对象数组的长度也等于 0,所以当第一次调用 addFilter() 方法时,if (n == filters.length)
的条件成立,ApplicationFilterConfig 数组长度被改变。之后 filters[n++] = filterConfig;将变量 filterConfig 放入 ApplicationFilterConfig 数组中并将当前过滤器链里面拥有的过滤器数目 +1(注意这里 n++ 的使用)
有了这些我们看整个链是怎样流转起来的
上图红色框的最顶部调用了 StandardWrapperValve
的 invoke
方法:
...
// Create the filter chain for this request
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
...
filterChain.doFilter(request.getRequest(), response.getResponse());
通过 ApplicationFilterFactory.createFilterChain
实例化 ApplicationFilterChain
(工厂模式),调用 filterChain.doFilter
方法正式进入责任链条,来看该方法,方法内部调用了 internalDoFilter
方法,来看关键代码:
/**
* The int which is used to maintain the current position
* in the filter chain.
*/
private int pos = 0;
// Call the next filter if there is one
if (pos < n) {ApplicationFilterConfig filterConfig = filters[pos++];
try {Filter filter = filterConfig.getFilter();
if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
if(Globals.IS_SECURITY_ENABLED) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
} else {filter.doFilter(request, response, this);
}
} catch (IOException | ServletException | RuntimeException e) {throw e;} catch (Throwable e) {e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
return;
}
pos
变量用来标记 filter chain 执行的当前位置,然后调用 filter.doFilter(request, response, this);
传递 this
(ApplicationFilterChain)进行链路传递,直至 pos > n 的时候停止 (类似击鼓传花中的鼓声停止),即所有拦截器都执行完毕。
继续向下看另外一个从我们身边路过无数次的责任链模式
Mybatis 拦截器
Mybatis 拦截器执行过程解析 中留一个问题彩蛋责任链模式,那在 Mybatis 拦截器中是怎样应用的呢?
public class InterceptorChain {private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {return Collections.unmodifiableList(interceptors);
}
}
以 Executor 类型的拦截为例,如果存在多个同类型的拦截器,当执行到 pluginAll 方法时,他们是怎样在责任链条中传递的呢?
调用interceptor.plugin(target)
为当前 target 生成代理对象,当多个拦截器遍历的时候,也就是会继续为代理对象再生成代理对象,直至遍历结束,拿到最外层的代理对象,触发 invoke
方法就可以完成链条拦截器的传递,以图来说明一下
看了这些,你和责任链设计模式会是灵魂之交吗?
总结与思考
敲黑板,敲黑板,敲黑板(重要的事情敲三次黑板)
看了这么多之后,我们要总结出责任链设计模式的关键了
- 设计一个链条,和抽象处理方法
- 将具体处理器初始化到链条中,并做抽象方法具体的实现
- 具体处理器之间的引用和处理条件判断
- 设计链条结束标识
1,2 都可以很模块化设计,3,4 设计可以多种多样,比如文中通过 pos 游标,或嵌套动态代理等.
在实际业务中,如果存在相同类型的任务需要顺序执行,我们就可以拆分任务,将任务处理单元最小化,这样易复用,然后串成一个链条,应用责任链设计模式就好了. 同时读框架源码时如果看到 chain
关键字,也八九不离十是应用责任链设计模式了,看看框架是怎样应用责任链设计模式的。
现在请你回看文章开头,重新站在上帝视角审视责任链设计模式,什么感觉,欢迎留言交流
灵魂追问
- Lambda 函数式编程,你可以灵活应用,实现优雅编程吗?
- 多个拦截器或过滤器,如果需要特定的责任链顺序,我们都有哪些方式控制顺序?
那些可以提高效率的工具
](https://upload-images.jianshu…
VNote
留言中有朋友让我推荐一款 MarkDown 编辑器,我用过很多种(包括在线的),这次推荐 VNote, VNote 是一个受 Vim 启发的更懂程序员和 Markdown 的一个笔记软件, 都说 vim 是最好的编辑器,更懂程序猿,但是多数还是应用在类 Unix 环境的 shell 脚本编写中,熟练使用 vim 也是我们必备的基本功,VNote 满足这一切需求,同时提供非常多方便的快捷键满足日常 MarkDown 的编写. 通过写文字顺路学习 vim,快哉 …