不得不知的责任链设计模式


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

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

责任链设计模式介绍

抽象介绍

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

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

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

  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 白名单的接口,都会看到如下的调用栈信息:

红色标记框的内容是 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++ 的使用)

有了这些我们看整个链是怎样流转起来的
上图红色框的最顶部调用了 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 方法就可以完成链条拦截器的传递,以图来说明一下

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

总结与思考

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

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

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

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

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


灵魂追问

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

那些可以提高效率的工具

VNote

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


优质内容筛选与推荐>>
1、css3 position fixed居中的问题
2、JVM (1) JVM Internals : What does JVM do?
3、TCP的三次握手和四次握手
4、2的m次方 内存对齐
5、学生管理系统


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号