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

40次阅读

共计 8227 个字符,预计需要花费 21 分钟才能阅读完成。

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

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

责任链设计模式介绍

抽象介绍

初次见面,了解表象,深入交流之后(看完文中的 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. 多个拦截器或过滤器,如果需要特定的责任链顺序,我们都有哪些方式控制顺序?

那些可以提高效率的工具


](https://upload-images.jianshu…

VNote

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


正文完
 0