设计模式系列-责任链模式

你开发了一套博客系统,但用户老发一些涉黄、广告词汇。眼看网站就要被封了,你该怎么办?

对的,过滤掉敏感词,还广大用户一篇清静。

实现这个功能,你很自然地想到 Servlet 的过滤器,或者是 Spring 的拦截器。

然而,你有想过吗?框架中的过滤器、拦截器是怎么实现的呢?

虽然我们没机会开发一套拦截器机制,但了解开源框架的底层实现,会大大提高我们的敲代码水平。再不济,你也能在同事面前装下X。

责任链模式的原理和实现

拦截器的实现,离不开责任链模式。 无论是 Spring 的拦截器,还是 Tomcat 实现的 Servlet 过滤器,都用到了责任链模式。

我们先来看它的官方定义:将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。

看完之后,你一脸懵逼。简单来说,你定义了多个处理器,依次处理同一个请求。

比如说,一个http请求,先经过A处理器处理,再传给B处理器,然后再传给C处理器,就这样一直传下去,形成一条链条。在这条链条上,每个处理器的都承担自己的责任,所以叫做:责任链模式。

这就是责任链模式的原理了。那代码怎么写?

思路是这样的。我们先定义两个处理器类(HandlerA、HandlerB),来处理具体的业务。再定义一个处理器链条,负责不断调用处理器,处理客户的请求。最后,创建一个处理器链条,这就是我们的客户端代码。

你可以结合下面的代码,加深一下理解。

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: jiarupc
 * @Date: 2020/9/28
 * @Description:    责任链模式-演示demo
 */
public class ChainOfResponsibility {
    /*********************** 处理器接口及实现 *************************/
    public interface IHandler {
        boolean handle(String request);
    }

    public static class HandlerA implements IHandler {
        @Override
        public boolean handle(String request) {
            System.out.println("已经过A处理器处理");
            // ...你想要的业务

            return request != null && !"".equals(request);
        }
    }

    public static class HandlerB implements IHandler {
        @Override
        public boolean handle(String request) {
            System.out.println("已经过B处理器处理");
            // ...你想要的业务

            return request != null && !"".equals(request);
        }
    }

    /*********************** 处理链条 *************************/
    public static class HandlerChain {
        private List handlers = new ArrayList<>();

        public void addHandler(IHandler handler) {
            this.handlers.add(handler);
        }

        public void handle(String request) {
            for (IHandler handler : handlers) {
                boolean isHandle = handler.handle(request);
                if (!isHandle) {
                    System.out.println("处理失败,跳过链条,直接退出");
                    break;
                }
            }
            System.out.println("处理完成,共调用 " + handlers.size() + " 个处理器");
        }
    }

    /*********************** 客户端 *************************/
    public static void main(String[] args) {
        // 构建处理器链条,你可以用注解和反射,实现自动发现处理器
        HandlerChain chain = new HandlerChain();
        chain.addHandler(new HandlerA());
        chain.addHandler(new HandlerB());

        // 启动处理链,处理request
        String request = "请求模拟";
        chain.handle(request);
    }

}

为什么要用责任链模式?

拿文章开头的业务做例子,我们现在不但要过滤涉黄词汇,还要过滤替换一些特殊符号。

这时候,我们可以直接改拦截器的代码,直接在上面多写几个 if-else 判断。在小项目上,这样完全没问题。但要是碰上大项目,需求多了,你的代码很可能会越写越多,越写越乱。

比方说,除了过滤涉黄、广告等词汇,还得过滤替换特殊符号。要做的东西越来越多,你能在里面无限写下去吗?这时候该怎么办?

除此之外,在大项目、开源项目,是很多人、很多团队同时开发的,工作内容明确,泾渭分明。别人没法改你的代码,你必须给别人的开发留足空间。

责任链模式能提高代码的拓展性。

你不用再改别人的代码了,只要再新建一个 Handler 类,实现它的 handle() 方法,再调用 addHandler() 方法,添加进 HandlerChain,这就可以了。

你想想,Spring 的拦截器不也是这样吗?先自定义一个 intercepter,再加上注解,就能拦截处理http请求了。

这些各种高大上的框架,之所以用起来这么顺手,是因为给你留足了空间,让你能按自己的需求取开发。而这一切的开始,就是在上面的简单 demo 中,一步步迭代出来的。

Shiro 怎么添加过滤器?

责任链模式经常出现在框架中,让我们在不修改源码的情况下,添加新的功能。

Shiro 是一个Java安全框架,提供了各种安全认证功能。这种安全框架一般会用各种过滤器,来实现权限拦截功能。

我们就从 Shiro 的入手,来看看这些高大上的项目,是怎么运用责任链模式的。

Shiro 在 Servlet Filter 的基础上,自己定制了一套的过滤器功能。就是下面这副图:

设计模式系列-责任链模式_第1张图片
Shiro-拦截器流程

在开发的时候,我们该怎么使用 Shiro Filter 呢?首先,新建一个自己的 Filter 类,并且继承 UserFilter。然后,把刚才新建的 Filter 类,加到配置文件就行了。

// 这里只留下了过滤器的代码,删掉了其它的代码,大家可以看得更直观

/**
 * @Author: jiarupc
 * @Date: 2020/8/26
 * @Description:    登录token过滤器
 */
public class TokenFilter extends UserFilter {


    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("token过滤器已处理");
        // ...你想要的业务

        chain.doFilter(request, response);
    }

}

/**
 * @Author: jiarupc
 * @Date: 2020/08/26
 * @Description: shiro 配置类:用于完成 shiro 配置
 */
@Configuration
public class ShiroConfiguration {
    /***************** shiro 基础配置 *****************/
    /**
     * 配置shiro过滤器
     * @param manager 已在 Spring 容器中注册的“安全管理器”
     * @return
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {
        /** 基础配置 **/
        // 定义shiro过滤器工厂bean
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();

        /** 自定义过滤器 **/
        Map filterMap = Maps.newLinkedHashMap();
        // 登录token过滤器
        filterMap.put("token", new TokenFilter());

        bean.setFilters(filterMap);

        return bean;
    }

}

整个流程非常简单,我们只要继承 UserFilter,在上面写自己的业务,然后加一行配置就行了。拓展性这么好,Shiro 是怎么做到的呢?

解析 Shiro 源码

Shiro 的过滤器用到了责任链模式。追踪 Shiro 的源码,我们可以发现一个类 ProxiedFilterChain。我们看下里面的关键代码:

/**
 * FilterChain 的代理类,负责执行 FilterChain 
 */
public class ProxiedFilterChain implements FilterChain {

    // 初始化时,会从外部传入一个 FilterChain
    private FilterChain orig;
    // 需要执行的 Filter链
    private List filters;
    // 当前执行到哪个 Filter
    private int index = 0;

    public ProxiedFilterChain(FilterChain orig, List filters) {
        // ProxiedFilterChain 只是一个代理类,必须传入 FilterChain 才能执行
        if (orig == null) {
            throw new NullPointerException("original FilterChain cannot be null.");
        }
        this.orig = orig;
        this.filters = filters;
        this.index = 0;
    }


    public void doFilter(ServletRequest request, ServletResponse response) {
        // filter 链为空时,直接调用原来 FilterChain 的方法
        if (this.filters == null || this.filters.size() == this.index) {
            this.orig.doFilter(request, response);
        } else {
            this.filters.get(this.index++).doFilter(request, response, this);
        }
    }
}

我们可以看到,Shiro 拓展了责任链模式,增加了一个代理类。

作为一个权限框架,Shiro 要执行的责任链不止一条。如果每条责任链都写自己执行的代码,那肯定造成代码冗余。所以,Shiro 就把重复代码抽出来,成为一个代理类,专门来执行责任链的业务。

那么,Shiro 是怎么实现责任链模式的呢?

Shiro 的代码非常巧妙,我们把代理类和过滤器合起来看,会发现:它们实现了一个递归调用。先来看代理类 ProxiedFilterChain.doFilter() 方法。我重新贴上关键代码:

/**
 * FilterChain 的代理类,负责执行 FilterChain 
 */
public class ProxiedFilterChain implements FilterChain {
    // 初始化时,会从外部传入一个 FilterChain
    private FilterChain orig;
    // 需要执行的 Filter链
    private List filters;
    // 当前执行到哪个 Filter
    private int index = 0;
    
    public void doFilter(ServletRequest request, ServletResponse response) {
        // filter 链为空时,直接调用原来 FilterChain 的方法
        if (this.filters == null || this.filters.size() == this.index) {
            this.orig.doFilter(request, response);
        } 
        // 调用 filter.doFilter()
        else {
            this.filters.get(this.index++).doFilter(request, response, this);
        }
    }
}

在 ProxiedFilterChain 的第 19 行代码,它为了让 Filter 链顺利执行,做了两个操作。首先, this.filters.get(index++),这段代码的意思是:先获取计数器的下标,通过下标拿到对应的 Filter,再把计数器 + 1。最后,执行过滤器 Filter.doFilter() 方法的。

再来看看 TokenFilter,在实现了业务代码后,这里的第 10 行代码,会重新调用 ProxiedFilterChain.doFilter 方法。

public class TokenFilter extends UserFilter {

    // doFilterInternal 等于 doFilter,shiro 对 doFilter 做了特殊处理,不允许修改
    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("token过滤器已处理");
        // ...你想要的业务

        // 这行代码会调用 ProxiedFilterChain.doFilter(),找到实现递归调用
        chain.doFilter(request, response);
    }

}

这样一来,就实现了递归调用。如果你还是觉得很难理解,可以再看看下面这副流程图:

设计模式系列-责任链模式_第2张图片
Shiro-责任链流程

写在最后

责任链模式常用在框架开发上,来实现框架的过滤器、拦截器功能。使用者不需要修改框架源码,就能添加新的过滤拦截功能。在 Shiro 的过滤器中,就用到了责任链模式。

当然,我们在日常工作中,很少能碰上 Shiro 那样的大项目,但你总得做好准备。说不定,在某一天,机会就来的呢?

你可能感兴趣的:(设计模式系列-责任链模式)