5

设计模式:责任链模式的应用场景及源码应用 - 一只阿木木

 1 year ago
source link: https://www.cnblogs.com/yizhiamumu/p/16856158.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

设计模式:责任链模式的应用场景及源码应用

责任链模式(Chain of Responsibility Pattern)是将链中每一个节点看作是一个对象,每个节点处理的请求均不同,且内部自动维护一个下一节点对象。当一个请求从链式的首端发出时,会沿着链的路径依次传递给每一个节点对象,直至有对象处理这个请求为止,属于行为型模式。就像一场足球比赛,通过层层传递,最终射门。

责任链模式的应用场景

  1. 多个对象可以处理一个请求,但具体由哪个对象处理该请求在运行时自动确定。
  2. 可动态指定一组对象处理请求,或添加新的处理者。
  3. 需要在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。

设计模式只是帮助减少代码的复杂性,让其满足开闭原则,提高代码的扩展性。如果不使用同样可以完成需求。

假设业务场景是这样的,我们 系统处在一个下游服务,因为业务需求,系统中所使用的 基础数据需要从上游中台同步到系统数据库

基础数据包含了很多类型数据,虽然数据在中台会有一定验证,但是 数据只要是人为录入就极可能存在问题,遵从对上游系统不信任原则,需要对数据接收时进行一系列校验

最初是要进行一系列验证原则才能入库的,后来因为工期问题只放了一套非空验证,趁着春节期间时间还算宽裕,把这套验证规则骨架放进去

从我们系统的接入数据规则而言,个人觉得需要支持以下几套规则

  1. 必填项校验,如果数据无法满足业务所必须字段要求,数据一旦落入库中就会产生一系列问题
  2. 非法字符校验,因为数据如何录入,上游系统的录入规则是什么样的我们都不清楚,这一项规则也是必须的
  3. 长度校验,理由同上,如果系统某字段长度限制 50,但是接入来的数据 500长度,这也会造成问题

如果不使用责任链模式,上面说的真实同步场景面临两个问题

  1. 如果把上述说的代码逻辑校验规则写到一起,毫无疑问这个类或者说这个方法函数奇大无比。减少代码复杂性一贯方法是:将大块代码逻辑拆分成函数,将大类拆分成小类,是应对代码复杂性的常用方法。如果此时说:可以把不同的校验规则拆分成不同的函数,不同的类,这样不也可以满足减少代码复杂性的要求么。这样拆分是能解决代码复杂性,但是这样就会面临第二个问题
  2. 开闭原则:添加一个新的功能应该是,在已有代码基础上扩展代码,而非修改已有代码。大家设想一下,假设你写了三套校验规则,运行过一段时间,这时候领导让加第四套,是不是要在原有代码上改动

综上所述,在合适的场景运用适合的设计模式,能够让代码设计复杂性降低,变得更为健壮。朝更远的说也能让自己的编码设计能力有所提高。

  1. 将请求与处理解耦。
  2. 请求处理者(节点对象)只需要关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,转发给下一个节点。
  3. 具备链式传递处理请求功能,请求发送者无需知晓链路结构,只需等待请求处理结果。
  4. 链路结构灵活,可以通过改变链路的结构动态的新增或删减责任。
  5. 易于扩展新的请求处理类(节点),符合开闭原则
  1. 责任链太长或者处理时间过长,会影响整体性能。
  2. 如果节点对象存在循环引用时,会造成死循环,导致系统崩溃。

二、入门案例

2.1 类图

960593-20221103224337402-1114751952.png

2.2 基础类介绍

抽象接口RequestHandler

public interface RequestHandler {

    void doHandler(String req);
}

抽象类BaseRequestHandler

public abstract class BaseRequestHandler implements RequestHandler {

    protected RequestHandler next;

    public void next(RequestHandler next) {
        this.next = next;
    }
}

具体处理类AHandler

public class AHandler extends BaseRequestHandler {

    @Override
    public void doHandler(String req) {
        // 处理自己的业务逻辑
        System.out.println("A中处理自己的逻辑");
        // 传递给下个类(若链路中还有下个处理类)
        if (next != null) {
            next.doHandler(req);
        }
    }
}

当然还有具体的处理类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("链路待处理的数据");
    }
}

2.3 处理流程图

960593-20221103221020275-511001586.png

三、应用场景

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;
}

2、定义一个顶层的抽象接口IHandler,传入2个泛型参数,供后续自定义。

public interface IHandler<T, R> {
    /**
     * 抽象处理类
     * @param t
     * @return
     */
    R handle(T t);
}

3、定义一个责任链bean的管理类HandleChainManager,用来存放不同业务下的责任链路集合。在该类中,有一个Map和两个方法。

  1. handleMap:这个map会存放责任链路中,具体的执行类,key是注解@Duty中定义的type值,value是标记了@Duty注解的bean集合,也就是具体的执行类集合。
  2. setHandleMap:传入具体执行bean的集合,存放在map中。
  3. 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;
    }
}

4、定义一个配置类PatternConfiguration,用于装配上面的责任链管理器HandleChainManager

@Configuration
public class PatternConfiguration {

    @Bean
    public HandleChainManager handlerChainExecute(List<IHandler> handlers) {
        HandleChainManager handleChainManager = new HandleChainManager();
        handleChainManager.setHandleMap(handlers);
        return handleChainManager;
    }

}

5、具体的处理类:SignChainHandlerEncryptionChainHandlerRequestChainHandler,这里我以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 "加签";
    }
}

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;
    }
}

7、执行结果,会按照注解中标记的order依次执行。

960593-20221103221518244-1855117032.png

至此,完工。又可以开心的撸代码了,然后在具体的执行类中,又是一顿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;
    }
}
960593-20221103221447049-98560115.png

下面,我们来看下其中一个子类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;
    }
    // ...
}

最后,经过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);
    }
    // ...

}

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);
                }
            }
        }
    }

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;
}
  • 在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);
            }
        }
    }
}
  • 获取拦截器,执行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);
        }
    }
}

五、代码示例

员工在OA系统中提交请假申请,首先项目经理处理,他能审批3天以内的假期,如果大于3天,则由项目经理则转交给总经理处理。接下来我们用责任链模式实现这个过程。

1、封装请假信息实体类

public class LeaveRequest {
    private String name;    // 请假人姓名
    private int numOfDays;  // 请假天数
    private int workingAge;  //员工工龄(在公司大于2年则总经理会审批)
   //省略get..set..
}

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); // 处理请假(这里用了模板方法模式)

}

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;
        }
    }
}

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;
        }
    }
}

实例代码完成,我们测试一下:

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);
    }
}

运行结果:


项目经理转交总经理
张三,你通过总经理审批!

六、源码中的典型应用 

源码中的典型应用:

  1. Netty 中的 Pipeline和ChannelHandler通过责任链设计模式来组织代码逻辑。
  2. Spring Security 使用责任链模式,可以动态地添加或删除责任(处理 request 请求)。
  3. Spring AOP 通过责任链模式来管理 Advisor。
  4. Dubbo Filter 过滤器链也是用了责任链模式(链表),可以对方法调用做一些过滤处理,譬如超时(TimeoutFilter),异常(ExceptionFilter),Token(TokenFilter)等。
  5. Mybatis 中的 Plugin 机制使用了责任链模式,配置各种官方或者自定义的 Plugin,与 Filter 类似,可以在执行 Sql 语句的时候做一些操作。
  6. Tomcat 调用 ApplicationFilterFactory过滤器链。

spring安全框架security使用责任链模式

spring安全框架security使用责任链模式,框架使用者可以动态地添加删除责任(处理request请求)。

UML 类图

960593-20221103222422080-59330503.png
960593-20221103222505160-2020916960.png

源码解析: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);//将链条本身的对象传递给链条成员
			}
		}
	}

链条成员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);
}

七:设计模式重语意

最后说一下需要达成的业务需求。将一个批量数据经过处理器链的处理,返回出符合要求的数据分类

960593-20221103224504373-680538574.png

定义顶级验证接口和一系列处理器实现类没什么难度,但是应该如何进行链式调用呢?

这一块代码需要有一定 Spring 基础才能理解,一起来看下 VerifyHandlerChain 如何将所有处理器串成一条链

960593-20221103224527794-1685013144.png

VerifyHandlerChain 处理流程如下:

  1. 实现自 InitializingBean 接口,在对应实现方法中获取 IOC 容器中类型为 VerifyHandler 的 Bean,也就是 EmptyVerifyHandler、SexyVerifyHandler
  2. 将 VerifyHandler 类型的 Bean 添加到处理器链容器中
  3. 定义校验方法 verify(),对入参数据展开处理器链的全部调用,如果过程中发现已无需要验证的数据,直接返回

这里使用 SpringBoot 项目中默认测试类,来测试一下如何调用

@SpringBootTest
class ChainApplicationTests {

    @Autowired
    private VerifyHandlerChain verifyHandlerChain;

    @Test
    void contextLoads() {
        List<Object> verify = verifyHandlerChain.verify(Lists.newArrayList("源码圈", "@一只阿木木"));
        System.out.println(verify);
    }
}

这样的话,如果客户或者产品提校验相关的需求时,我们只需要实现 VerifyHandler 接口新建个校验规则实现类就 OK 了,这样符合了设计模式的原则:满足开闭原则,提高代码的扩展性

熟悉之前作者写过设计模式的文章应该知道,强调设计模式重语意,而不是具体的实现过程。所以,你看这个咱们这个校验代码,把责任链两种模式结合了使用


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK