zoukankan      html  css  js  c++  java
  • 不得不知的责任链设计模式

    世界上最遥远的距离,不是生与死,而是它从你的世界路过无数次,你却选择视而不见,你无情,你冷酷啊......

    love-1089665_640.jpg

    被你忽略的就是责任链设计模式,希望它再次经过你身旁你会猛的发现,并对它微微一笑......

    责任链设计模式介绍

    抽象介绍

    初次见面,了解表象,深入交流之后(看完文中的 demo 和框架中的实际应用后),你我便是灵魂之交(重新站在上帝视角来理解这个概念会更加深刻)

    责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象能或不能处理该请求,它都会把相同的请求传给下一个接收者,依此类推,直至责任链结束。

    接下来将概念图形化,用大脑图形处理区理解此概念
    W3sDesign_Chain_of_Responsibility_Design_Pattern_UML.jpg

    1. 上图左侧的 UML 类图中,Sender 类不直接引用特定的接收器类。 相反,Sender 引用Handler 接口来处理请求handler.handleRequest(),这使得 Sender 独立于具体的接收器(概念当中说的解耦) Receiver1,Receiver2 和 Receiver3 类通过处理或转发请求来实现 Handler 接口(取决于运行时条件)
    2. 上图右侧的 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 白名单的接口,都会看到如下的调用栈信息:
    Xnip2019-06-19_13-53-51.jpg

    红色标记框的内容是 Tomcat 容器设置的责任链,从 Engine 到 Cotext 再到 Wrapper 都是通过这个责任链传递请求,如下类图所示,他们都实现了 Valve 接口中的 invoke 方法
    Xnip2019-06-19_13-54-39.jpg

    但这并不是这里要说明的重点,这里要看的是和我们自定义 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++ 的使用)

    有了这些我们看整个链是怎样流转起来的
    上图红色框的最顶部调用了 StandardWrapperValveinvoke 方法:

    ...
    // 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 方法就可以完成链条拦截器的传递,以图来说明一下
    Xnip2019-06-19_15-19-36.jpg

    看了这些,你和责任链设计模式会是灵魂之交吗?

    总结与思考

    敲黑板,敲黑板,敲黑板 (重要的事情敲三次黑板)
    看了这么多之后,我们要总结出责任链设计模式的关键了

    1. 设计一个链条,和抽象处理方法
    2. 将具体处理器初始化到链条中,并做抽象方法具体的实现
    3. 具体处理器之间的引用和处理条件判断
    4. 设计链条结束标识

    1,2 都可以很模块化设计,3,4 设计可以多种多样,比如文中通过 pos 游标,或嵌套动态代理等.

    在实际业务中,如果存在相同类型的任务需要顺序执行,我们就可以拆分任务,将任务处理单元最小化,这样易复用,然后串成一个链条,应用责任链设计模式就好了. 同时读框架源码时如果看到 chain 关键字,也八九不离十是应用责任链设计模式了,看看框架是怎样应用责任链设计模式的。

    现在请你回看文章开头,重新站在上帝视角审视责任链设计模式,什么感觉,欢迎留言交流


    灵魂追问

    1. Lambda 函数式编程,你可以灵活应用,实现优雅编程吗?
    2. 多个拦截器或过滤器,如果需要特定的责任链顺序,我们都有哪些方式控制顺序?

    那些可以提高效率的工具

    a (1).png

    VNote

    留言中有朋友让我推荐一款 MarkDown 编辑器,我用过很多种(包括在线的),这次推荐 VNote, VNote 是一个受Vim启发的更懂程序员和Markdown的一个笔记软件, 都说 vim是最好的编辑器,更懂程序猿,但是多数还是应用在类 Unix 环境的 shell 脚本编写中,熟练使用 vim 也是我们必备的基本功,VNote 满足这一切需求,同时提供非常多方便的快捷键满足日常 MarkDown 的编写. 通过写文字顺路学习 vim,快哉...

    Xnip2019-06-19_15-56-41.jpg


    a (1).png

  • 相关阅读:
    hibernate和mybatis区别
    Spring事务的传播行为和隔离级别
    数组拷贝
    spring mvc 一次请求 两次查询
    手游性能之渲染分析3
    Android pm 命令详解
    Android am命令使用
    Android dumpsys命令详细使用
    java处理高并发高负载类网站的优化方法
    关于ArrayList的5道面试题
  • 原文地址:https://www.cnblogs.com/FraserYu/p/11066835.html
Copyright © 2011-2022 走看看