设计模式:责任链模式的应用场景及源码应用
一、概述
责任链模式(Chain of Responsibility Pattern)是将链中每一个节点看作是一个对象,每个节点处理的请求均不同,且内部自动维护一个下一节点对象。当一个请求从链式的首端发出时,会沿着链的路径依次传递给每一个节点对象,直至有对象处理这个请求为止,属于行为型模式。就像一场足球比赛,通过层层传递,最终射门。
责任链模式的应用场景
- 多个对象可以处理一个请求,但具体由哪个对象处理该请求在运行时自动确定。
- 可动态指定一组对象处理请求,或添加新的处理者。
- 需要在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。
设计模式只是帮助减少代码的复杂性,让其满足开闭原则,提高代码的扩展性。如果不使用同样可以完成需求。
假设业务场景是这样的,我们 系统处在一个下游服务,因为业务需求,系统中所使用的 基础数据需要从上游中台同步到系统数据库
基础数据包含了很多类型数据,虽然数据在中台会有一定验证,但是 数据只要是人为录入就极可能存在问题,遵从对上游系统不信任原则,需要对数据接收时进行一系列校验
最初是要进行一系列验证原则才能入库的,后来因为工期问题只放了一套非空验证,趁着春节期间时间还算宽裕,把这套验证规则骨架放进去
从我们系统的接入数据规则而言,个人觉得需要支持以下几套规则
- 必填项校验,如果数据无法满足业务所必须字段要求,数据一旦落入库中就会产生一系列问题
- 非法字符校验,因为数据如何录入,上游系统的录入规则是什么样的我们都不清楚,这一项规则也是必须的
- 长度校验,理由同上,如果系统某字段长度限制 50,但是接入来的数据 500长度,这也会造成问题
如果不使用责任链模式,上面说的真实同步场景面临两个问题
- 如果把上述说的代码逻辑校验规则写到一起,毫无疑问这个类或者说这个方法函数奇大无比。减少代码复杂性一贯方法是:将大块代码逻辑拆分成函数,将大类拆分成小类,是应对代码复杂性的常用方法。如果此时说:可以把不同的校验规则拆分成不同的函数,不同的类,这样不也可以满足减少代码复杂性的要求么。这样拆分是能解决代码复杂性,但是这样就会面临第二个问题
- 开闭原则:添加一个新的功能应该是,在已有代码基础上扩展代码,而非修改已有代码。大家设想一下,假设你写了三套校验规则,运行过一段时间,这时候领导让加第四套,是不是要在原有代码上改动
综上所述,在合适的场景运用适合的设计模式,能够让代码设计复杂性降低,变得更为健壮。朝更远的说也能让自己的编码设计能力有所提高。
优点
- 将请求与处理解耦。
- 请求处理者(节点对象)只需要关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,转发给下一个节点。
- 具备链式传递处理请求功能,请求发送者无需知晓链路结构,只需等待请求处理结果。
- 链路结构灵活,可以通过改变链路的结构动态的新增或删减责任。
- 易于扩展新的请求处理类(节点),符合开闭原则。
缺点
- 责任链太长或者处理时间过长,会影响整体性能。
- 如果节点对象存在循环引用时,会造成死循环,导致系统崩溃。
二、入门案例
2.1 类图
image2.2 基础类介绍
抽象接口RequestHandler
public interface RequestHandler {
void doHandler(String req);
}
image.gif
抽象类BaseRequestHandler
public abstract class BaseRequestHandler implements RequestHandler {
protected RequestHandler next;
public void next(RequestHandler next) {
this.next = next;
}
}
image.gif
具体处理类AHandler
public class AHandler extends BaseRequestHandler {
@Override
public void doHandler(String req) {
// 处理自己的业务逻辑
System.out.println("A中处理自己的逻辑");
// 传递给下个类(若链路中还有下个处理类)
if (next != null) {
next.doHandler(req);
}
}
}
image.gif
当然还有具体的处理类B、C等等,这里不展开赘述。 使用类Client
public class Client {
public static void main(String[] args) {
BaseRequestHandler a = new AHandler();
BaseRequestHandler b = new BHandler();
BaseRequestHandler c = new CHandler();
a.next(b);
b.next(c);
a.doHandler("链路待处理的数据");
}
}
image.gif
2.3 处理流程图
三、应用场景
3.1 场景举例
场景一
金融业务其中就有一个业务场景:一笔订单进来,会先在后台通过初审人员进行审批,初审不通过,订单流程结束。初审通过以后,会转给终审人员进行审批,不通过,流程结束;通过,流转到下个业务场景。 对于这块业务代码,一套if-else干到底。后来,技术老大CodeReview,点名要求改掉这块。(当然,比较复杂的情况,还是可以用工作流来处理这个场景)。
场景二
有的公司业务会调用我们接口,将数据同步过来。同样,我们需要将处理好的数据,传给他们。由于双方传输数据都是加密传输,所以在接受他们数据之前,需要对数据进行解密,验签,参数校验等操作。同样,我们给他们传数据也需要进行加签,加密操作。
具体案例
对于场景二,我们结合代码一起探讨一下。 1、一切从注解开始,我这里自定义了一个注解@Duty,这个注解有spring的@Component注解,也就是标记了这个自定义注解的类,都是交给spring的bean容器去管理。 注解中,有两个属性:1.type,定义相同的type类型的bean,会被放到一个责任链集合中。2.order,同一个责任链集合中,bean的排序,数值越小,会放到链路最先的位置,优先处理。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Service
public @interface Duty {
/**
* 标记具体业务场景
* @return
*/
String type() default "";
/**
* 排序:数值越小,排序越前
* @return
*/
int order() default 0;
}
image.gif
2、定义一个顶层的抽象接口IHandler,传入2个泛型参数,供后续自定义。
public interface IHandler<T, R> {
/**
* 抽象处理类
* @param t
* @return
*/
R handle(T t);
}
image.gif
3、定义一个责任链bean的管理类HandleChainManager,用来存放不同业务下的责任链路集合。在该类中,有一个Map和两个方法。
- handleMap:这个map会存放责任链路中,具体的执行类,key是注解@Duty中定义的type值,value是标记了@Duty注解的bean集合,也就是具体的执行类集合。
- setHandleMap:传入具体执行bean的集合,存放在map中。
- executeHandle:从map中找到具体的执行bean集合,并依次执行。
public class HandleChainManager {
/**
* 存放责任链路上的具体处理类
* k-具体业务场景名称
* v-具体业务场景下的责任链路集合
*/
private Map<String, List<IHandler>> handleMap;
/**
* 存放系统中责任链具体处理类
* @param handlerList
*/
public void setHandleMap(List<IHandler> handlerList) {
handleMap = handlerList
.stream()
.sorted(Comparator.comparingInt(h -> AnnotationUtils.findAnnotation(h.getClass(), Duty.class).order()))
.collect(Collectors.groupingBy(handler -> AnnotationUtils.findAnnotation(handler.getClass(), Duty.class).type()));
}
/**
* 执行具体业务场景中的责任链集合
* @param type 对应@Duty注解中的type,可以定义为具体业务场景
* @param t 被执行的参数
*/
public <T, R> R executeHandle(String type, T t) {
List<IHandler> handlers = handleMap.get(type);
R r = null;
if (CollectionUtil.isNotEmpty(handlers)) {
for (IHandler<T, R> handler : handlers) {
r = handler.handle(t);
}
}
return r;
}
}
image.gif
4、定义一个配置类PatternConfiguration,用于装配上面的责任链管理器HandleChainManager。
@Configuration
public class PatternConfiguration {
@Bean
public HandleChainManager handlerChainExecute(List<IHandler> handlers) {
HandleChainManager handleChainManager = new HandleChainManager();
handleChainManager.setHandleMap(handlers);
return handleChainManager;
}
}
image.gif
5、具体的处理类:SignChainHandler、EncryptionChainHandler、RequestChainHandler,这里我以SignChainHandler为例。 在具体处理类上标记自定义注解@Duty,该类会被注入到bean容器中,实现IHandler接口,只需关心自己的handle方法,处理具体的业务逻辑。
@Duty(type = BusinessConstants.REQUEST, order = 1)
public class SignChainHandler implements IHandler<String, String> {
/**
* 处理加签逻辑
* @param s
* @return
*/
@Override
public String handle(String s) {
// 加签逻辑
System.out.println("甲方爸爸要求加签");
return "加签";
}
}
image.gif
6、具体怎么调用?这里我写了个测试controller直接调用,具体如下:
@RestController
@Slf4j
public class TestController {
@Resource
private HandleChainManager handleChainManager;
@PostMapping("/send")
public String duty(@RequestBody String requestBody) {
String response = handleChainManager.executeHandle(BusinessConstants.REQUEST, requestBody);
return response;
}
}
image.gif
7、执行结果,会按照注解中标记的order依次执行。
image image.gif编辑
至此,完工。又可以开心的撸代码了,然后在具体的执行类中,又是一顿if-else。。。
四、源码中运用
4.1Mybatis源码中的运用
Mybatis中的缓存接口Cache,cache作为一个缓存接口,最主要的功能就是添加和获取缓存的功能,作为接口它有11个实现类,分别实现不同的功能,下面是接口源码和实现类。
package org.apache.ibatis.cache;
import java.util.concurrent.locks.ReadWriteLock;
public interface Cache {
String getId();
void putObject(Object var1, Object var2);
Object getObject(Object var1);
Object removeObject(Object var1);
void clear();
int getSize();
default ReadWriteLock getReadWriteLock() {
return null;
}
}
image.gif
image
image.gif
编辑
下面,我们来看下其中一个子类LoggingCache的源码。主要看他的putObject方法和getObject方法,它在方法中直接传给下一个实现去执行。这个实现类其实是为了在获取缓存的时候打印缓存的命中率的。
public class LoggingCache implements Cache {
private final Log log;
private final Cache delegate;
protected int requests = 0;
protected int hits = 0;
public LoggingCache(Cache delegate) {
this.delegate = delegate;
this.log = LogFactory.getLog(this.getId());
}
// ...
public void putObject(Object key, Object object) {
this.delegate.putObject(key, object);
}
public Object getObject(Object key) {
++this.requests;
Object value = this.delegate.getObject(key);
if (value != null) {
++this.hits;
}
if (this.log.isDebugEnabled()) {
this.log.debug("Cache Hit Ratio [" + this.getId() + "]: " + this.getHitRatio());
}
return value;
}
// ...
}
image.gif
最后,经过Cache接口各种实现类的处理,最终会到达PerpetualCache这个实现类。与之前的处理类不同的是,这个类中有一个map,在map中做存取,也就是说,最终缓存还是会保存在map中的。
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap();
public PerpetualCache(String id) {
this.id = id;
}
// ...
public void putObject(Object key, Object value) {
this.cache.put(key, value);
}
public Object getObject(Object key) {
return this.cache.get(key);
}
// ...
}
image.gif
4.2spring源码中的运用
4.2.1DispatcherServlet类
DispatcherServlet 核心方法 doDispatch。HandlerExecutionChain只是维护HandlerInterceptor的集合,可以向其中注册相应的拦截器,本身不直接处理请求,将请求分配给责任链上注册处理器执行,降低职责链本身与处理逻辑之间的耦合程度。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
image.gif
4.2.2HandlerExecutionChain类
这里分析的几个方法,都是从DispatcherServlet类的doDispatch方法中请求的。
- 获取拦截器,执行preHandle方法
boolean applyPreHandle(HttpServletRequest request,
HttpServletResponse response) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
}
return true;
}
image.gif
- 在applyPreHandle方法中,执行triggerAfterCompletion方法
void triggerAfterCompletion(HttpServletRequest request,
HttpServletResponse response, Exception ex) throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for(int i = this.interceptorIndex; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[i];
try {
interceptor.afterCompletion(request, response, this.handler, ex);
} catch (Throwable var8) {
logger.error("HandlerInterceptor.afterCompletion threw exception", var8);
}
}
}
}
image.gif
- 获取拦截器,执行applyPostHandle方法
void applyPostHandle(HttpServletRequest request,
HttpServletResponse response, ModelAndView mv)
throws Exception {
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for(int i = interceptors.length - 1; i >= 0; --i) {
HandlerInterceptor interceptor = interceptors[i];
interceptor.postHandle(request, response, this.handler, mv);
}
}
}
image.gif
五、代码示例
员工在OA系统中提交请假申请,首先项目经理处理,他能审批3天以内的假期,如果大于3天,则由项目经理则转交给总经理处理。接下来我们用责任链模式实现这个过程。
1、封装请假信息实体类
public class LeaveRequest {
private String name; // 请假人姓名
private int numOfDays; // 请假天数
private int workingAge; //员工工龄(在公司大于2年则总经理会审批)
//省略get..set..
}
image.gif
2、抽象处理者类 Handler,维护一个nextHandler属性,该属性为当前处理者的下一个处理者的引用;
声明了抽象方法process,其实在这里也用了方法模板模式:
public abstract class ApproveHandler {
protected ApproveHandler nextHandler;//下一个处理者(与类一致,这段代码很重要)
public void setNextHandler(ApproveHandler approveHandler){
this.nextHandler=approveHandler;
}
public abstract void process(LeaveRequest leaveRequest); // 处理请假(这里用了模板方法模式)
}
image.gif
3、项目经理处理者,能处理小于3天的假期,而请假信息里没有名字时,审批不通过:
public class PMHandler extends ApproveHandler{
@Override
public void process(LeaveRequest leaveRequest) {
//未填写姓名的请假单不通过
if(null != leaveRequest.getName()){
if(leaveRequest.getNumOfDays() <= 3){
System.out.println(leaveRequest.getName()+",你通过项目经理审批!");
}else {
System.out.println("项目经理转交总经理");
if(null != nextHandler){
nextHandler.process(leaveRequest);
}
}
}else {
System.out.println("请假单未填写完整,未通过项目经理审批!");
return;
}
}
}
image.gif
4、总经理处理者,能处理大于3天的假期,且工龄超过2年才会审批通过:
public class GMHandler extends ApproveHandler{
@Override
public void process(LeaveRequest leaveRequest) {
//员工在公司工龄超过2年,则审批通过
if(leaveRequest.getWorkingAge() >=2 && leaveRequest.getNumOfDays() > 3){
System.out.println(leaveRequest.getName()+",你通过总经理审批!");
if(null != nextHandler){
nextHandler.process(leaveRequest);
}
}else {
System.out.println("在公司年限不够,长假未通过总经理审批!");
return;
}
}
}
image.gif
实例代码完成,我们测试一下:
public class Test {
public static void main(String[] args) {
PMHandler pm = new PMHandler();
GMHandler gm = new GMHandler();
LeaveRequest leaveRequest = new LeaveRequest();
leaveRequest.setName("张三");
leaveRequest.setNumOfDays(4);//请假4天
leaveRequest.setWorkingAge(3);//工龄3年
pm.setNextHandler(gm);//设置传递顺序
pm.process(leaveRequest);
}
}
image.gif
运行结果:
项目经理转交总经理 张三,你通过总经理审批!
六、源码中的典型应用
源码中的典型应用:
- Netty 中的 Pipeline和ChannelHandler通过责任链设计模式来组织代码逻辑。
- Spring Security 使用责任链模式,可以动态地添加或删除责任(处理 request 请求)。
- Spring AOP 通过责任链模式来管理 Advisor。
- Dubbo Filter 过滤器链也是用了责任链模式(链表),可以对方法调用做一些过滤处理,譬如超时(TimeoutFilter),异常(ExceptionFilter),Token(TokenFilter)等。
- Mybatis 中的 Plugin 机制使用了责任链模式,配置各种官方或者自定义的 Plugin,与 Filter 类似,可以在执行 Sql 语句的时候做一些操作。
- Tomcat 调用 ApplicationFilterFactory过滤器链。
spring安全框架security使用责任链模式
spring安全框架security使用责任链模式,框架使用者可以动态地添加删除责任(处理request请求)。
UML 类图
image image.gif编辑
活动图:
image image.gif编辑
源码解析:currentPosition表示责任链的要处理请求链条节点的位置,使用additionalFilters来依次处理request请求。additionalFilters中的每个Filter成员都承担某一项具体职责,并且每个Filter都会被执行到。 责任链条的成员执行完自己的职责后,会回调链条的处理请求方法,责任链条会找到下一个链条成员来执行职责,直到链条尾端。
private static class VirtualFilterChain implements FilterChain {
private final FilterChain originalChain; //链条中的节点全部执行完后,处理request请求的对象
private final List<Filter> additionalFilters; //请求实际执行者,
private final FirewalledRequest firewalledRequest;
private final int size;
private int currentPosition = 0; //链条移动的位置,当currentPosition==size,到达链条的尾端。
private VirtualFilterChain(FirewalledRequest firewalledRequest,
FilterChain chain, List<Filter> additionalFilters) {
this.originalChain = chain;
this.additionalFilters = additionalFilters;
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (currentPosition == size) { //到达链条尾端
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
}
else {
currentPosition++; //依次移动链条指针到具体节点
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " at position " + currentPosition + " of " + size
+ " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
nextFilter.doFilter(request, response, this);//将链条本身的对象传递给链条成员
}
}
}
image.gif
链条成员Filter会执行chain.doFilter(request, response )方法,而chain是链条本身的引用,这样成员就将请求又重新交给了链条。看SecurityContextHolderAwareRequestFilter源码:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(this.requestFactory.create((HttpServletRequest) req,
(HttpServletResponse) res), res);
}
image.gif
七:设计模式重语意
最后说一下需要达成的业务需求。将一个批量数据经过处理器链的处理,返回出符合要求的数据分类
image image.gif编辑
定义顶级验证接口和一系列处理器实现类没什么难度,但是应该如何进行链式调用呢?
这一块代码需要有一定 Spring 基础才能理解,一起来看下 VerifyHandlerChain 如何将所有处理器串成一条链
image image.gif编辑
VerifyHandlerChain 处理流程如下:
- 实现自 InitializingBean 接口,在对应实现方法中获取 IOC 容器中类型为 VerifyHandler 的 Bean,也就是 EmptyVerifyHandler、SexyVerifyHandler
- 将 VerifyHandler 类型的 Bean 添加到处理器链容器中
- 定义校验方法 verify(),对入参数据展开处理器链的全部调用,如果过程中发现已无需要验证的数据,直接返回
这里使用 SpringBoot 项目中默认测试类,来测试一下如何调用
@SpringBootTest
class ChainApplicationTests {
@Autowired
private VerifyHandlerChain verifyHandlerChain;
@Test
void contextLoads() {
List<Object> verify = verifyHandlerChain.verify(Lists.newArrayList("源码圈", "@一只阿木木"));
System.out.println(verify);
}
}
image.gif
这样的话,如果客户或者产品提校验相关的需求时,我们只需要实现 VerifyHandler 接口新建个校验规则实现类就 OK 了,这样符合了设计模式的原则:满足开闭原则,提高代码的扩展性
熟悉之前作者写过设计模式的文章应该知道,强调设计模式重语意,而不是具体的实现过程。所以,你看这个咱们这个校验代码,把责任链两种模式结合了使用