设计模式之职责链模式及其应用过滤器/拦截器

792 阅读5分钟

概念

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

​ 职责链模式有两种执行方式,一种是GoF定义中说明的,如果执行器链上某个处理器能够处理请求,那么就不会继续往下执行。另一种变体是,请求会被所有执行器链上的所有处理器都处理一遍,不存在中途退出的情况。

​ 实现方式同样有两种,一种是利用对象引用保存下一个关联的处理器,类似于记录链表的链头与链尾;另一种是利用数组存储所有处理器,执行时需要逐一遍历。

使用方式

关键点

  • 定义处理器接口 或 抽象类
  • 定义处理器链

​ 假设有这样的场景,接口需要对参数进行敏感词校验,例如博客需要校验文章是否包含敏感词,如政治、广告、黄色等敏感词则需要过滤。

过滤也有两种策略情况:

  • 打回策略,一种是发现文章包含了某类敏感词,那么立刻将文章打回,禁止发布;
  • 替换策略,另一种是发现文章包含了某类敏感词,不打回文章但会将敏感词会用 *** 代替。

​ 这两种过滤的方式就可以使用到职责链的两种执行方式,一种中途退出,一种全部执行。

​ 因为通过数组的实现方式较为简单,不需要考虑链表先后的顺序,下面用数组实现的方式来实现这种场景。

一、打回策略 - 中断执行

1. 定义敏感词过滤接口

public interface SensitiveWordFilter {
	boolean doFilter(String content);
}

2. 不同的过滤方式实现接口

  • 政治敏感词过滤器
public class PoliticsSensitiveWordFilter implements SensitiveWordFilter{
    @Override
    public boolean doFilter(String content) {
        boolean isLegal = content.contains("政治");
        System.out.println("政治敏感词过滤");
        return isLegal;
    }
}
  • 广告敏感词过滤器
public class AdsSexySensitiveWordFilter implements SensitiveWordFilter{

    @Override
    public boolean doFilter(String content) {
        System.out.println("广告词过滤");
        return content.contains("广告");
    }
}
  • 黄色敏感词过滤器
public class SexySensitiveWordFilter implements SensitiveWordFilter{
    @Override
    public boolean doFilter(String content) {
        System.out.println("过滤黄色敏感词");
        return content.contains("黄色");
    }
}

3. 过滤器执行链

public class SensitiveWordFilterChain {
    // 数组保存链处理器
    private List<SensitiveWordFilter> filters = new LinkedList<>();

    public void addFilter(SensitiveWordFilter filter) {
        filters.add(filter);
    }

    /**
     * 遍历过滤器都执行一遍,如果发现有敏感词,则打回文章
     * @param content 校验的内容
     */
    public boolean doChain(String content) {
        for(SensitiveWordFilter filter : filters) {
            if(filter.doFilter(content)) {
                return false;
            }
        }
        return true;
    }
}

4. 客户端执行

   /***
     * 实现方式:数组保存过滤器
     * 执行目标:其中一条过滤器不满足就停止过滤
     */
    @Test
    public void testChainFilter() {

        String content = "打打杀杀";    // 文章内容

        // 注册过滤器
        SensitiveWordFilterChain chain = new SensitiveWordFilterChain();
        chain.addFilter(new SexySensitiveWordFilter());
        chain.addFilter(new AdsSexySensitiveWordFilter());
        chain.addFilter(new PoliticsSensitiveWordFilter());

        // 执行过滤
        System.out.println(chain.doChain(content));
    }

5. 过滤器执行链的工厂方法

​ 这里为了方便测试,注册过滤器就直接在客户端上执行了。但由于每次执行都要手动注册过滤器,终究不太好。另外执行器链可能不只一个,也可能存在其它类型的过滤器,例如过滤xss、SQL 注入之类的,就需要另外一种执行器链去区分了。

所以这里可以通过一个工厂方法去初始化过滤器,并获取到对应的执行器链。而获取到不同的执行器链又可以考虑使用策略模式去处理,这里就省去了策略模式的东西了。

/**
 * 过滤器执行链工厂方法
 */
public class FilterFactory {

    private static SensitiveWordFilterChain sensitiveWordFilterChain = new SensitiveWordFilterChain();

    /**
     * 初始化过滤器执行链
     */
    static {
        // 初始化敏感词执行器链
        initSensitiveWordFilter();

        // 初始化xss、SQL 安全信息执行器链
        initXssSQLFilterChain();
    }


    private static void initSensitiveWordFilter() {
        sensitiveWordFilterChain.addFilter(new SexySensitiveWordFilter());
        sensitiveWordFilterChain.addFilter(new AdsSexySensitiveWordFilter());
        sensitiveWordFilterChain.addFilter(new PoliticsSensitiveWordFilter());
    }

    private static void initXssSQLFilterChain() {
        // init..
    }

    public static FilterChain getFilterChain() {
        // TODO  动态获取执行器链,省去了用策略模式获取不同过滤器的方式...
        return sensitiveWordFilterChain;
    }

}

二、 替换策略 - 全部执行

替换策略其实就是将过滤接口的返回值改一下,返回当前过滤的对象就行。由于测试用的是String字符串,那么直接用String返回即可。

1. 定义敏感过滤器接口

public interface SensitiveWordFilter {
	String doFilter(String content);
}

2. 不同的过滤方式实现接口

  • 政治敏感词
public class PoliticsSensitiveWordFilter implements SensitiveWordFilter{
    @Override
    public String doFilter(String content) {
        return content.replace("政治","***");
    }
}

ps:实际场景的实现不会这么简单的,这里只讲思路;

3. 过滤器执行链

public class SensitiveWordFilterChain {
    // 数组保存链处理器
    private List<SensitiveWordFilter> filters = new LinkedList<>();

    public void addFilter(SensitiveWordFilter filter) {
        filters.add(filter);
    }

    /**
     * 遍历过滤器都执行一遍,过滤所有的敏感词,将敏感词替换为***
     * @param content 校验的内容
     */
    public String doChain(String content) {
        for(SensitiveWordFilter filter : filters) {
            content = filter.doFilter(content); // 其实在疯狂创建字符串对象了 0.0
        }
        return content;
    }
}

ps:这里跟打回策略不同的是,处理过滤器链时会将所有的过滤器都执行一遍 且 返回值是 String。

4. 客户端执行

与打回策略的执行方式一样的。

   /***
     * 实现方式:数组保存过滤器
     * 执行目标:使用所有的敏感词过滤
     */
    @Test
    public void testChainFilter() {

        String content = "打打杀杀";    // 文章内容

        // 注册过滤器
        SensitiveWordFilterChain chain = new SensitiveWordFilterChain();
        chain.addFilter(new SexySensitiveWordFilter());
        chain.addFilter(new AdsSexySensitiveWordFilter());
        chain.addFilter(new PoliticsSensitiveWordFilter());

        // 执行过滤
        System.out.println(chain.doChain(content));
    }

过滤器与拦截器

​ 前面以敏感词的过滤为场景,用两种策略分别实现了中断执行全部执行两种方式。在日常开发中,所用到的框架有很多也是用职责链的思想进行扩展实现的。下面看下Servlet 的 Filter 和 Spring 的Interceptor。

Servlet Filter

Servlet Filter 是 Java Servlet 规范中定义的组件,翻译成中文就是过滤器,它可以实现对 HTTP 请求的过滤功能,比如鉴权、限流、记录日志、验证参数等等。因为它是 Servlet 规范的一部分,所以,只要是支持 Servlet 的 Web 容器(比如,Tomcat、Jetty 等),都支持过滤器功能。

​ 在实际项目中,如果想使用Servlet Filter,那么需要添加一个过滤器实现Filter接口,重写init、doFilter、destory三个方法。

​ Maven项目中需要引入servlet-api的依赖:

  <dependency>
       <groupId>javax.servlet</groupId>
       <artifactId>servlet-api</artifactId>
       <version>2.5</version>
       <scope>provided</scope>
 </dependency>

新增 LogFilter 过滤器,重写Filter接口的方法

public class LogFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("创建Filter时调用");
        // FilterConfig 参数从配置文件web.xml中读取
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("doFilter...拦截客户端发送过来的请求");
         // 实际上是个递归过程,一直向下递归,直到底,然后回溯,执行后面的方法
        filterChain.doFilter(servletRequest, servletResponse); 
        System.out.println("doFilter...拦截返回给客户端的响应");
    }

    @Override
    public void destroy() {
        System.out.println("销毁Filter时调用");
    }


}

由于需要在启动时被初始化,所以需要在web.xml配置文件中,配置该Filter对象的路径。这样当请求来时,Web容器会先经过Filter过滤器,然后才由Servlet处理。

// web.xml里的配置
<filter>
    <filter-name>logFilter</filter-name>
    <filter-class>behavior.chain.LogFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>logFilter</filter-name>
    <url-pattern>/*</url-pattern> // 拦截的路径
</filter-mapping>

Spring Interceptor

Spring Interceptor 是Spring MVC 框架的一部分,由SpringMvc框架来提供实现。翻译成中文就是拦截器,跟Servlet Filter功能类似,但不同在于Servlet Filter是Servlet规范的一部分,实现依赖于web容器。

在web请求中,请求会先经过Servlet Filter,然后再经过Spring Interceptor ,最后到达具体的业务代码中。

如果想要使用Spring Interceptor,那么可以实现 由SpringMvc 提供的HandlerInterceptor 接口。

  • LogInterceptor
public class LogInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("拦截请求,请求会先在这里处理");
        return true;    // 继续后续的处理
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("拦截请求,请求后到这里");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("这里总是会被执行");
    }
}

在Spring MVC 配置文件中配置interceptors

<mvc:interceptors>   
    <mvc:interceptor>       
        <mvc:mapping path="/*"/>       
        <bean class="behavior.chain.LogInterceptor" />   
    </mvc:interceptor>
</mvc:interceptors>

当然,它也是基于职责链模式实现的。其中,HandlerExecutionChain 类是职责链模式中的处理器链。它的实现相较于 Tomcat 中的 ApplicationFilterChain 来说,逻辑更加清晰,不需要使用递归来实现,主要是因为它将请求和响应的拦截工作,拆分到了两个函数中实现。

总结

​ 职责链在开发中,常用来实现框架的拦截器、过滤器功能,让使用者在不需要修改框架源码的情况下,添加新的过滤拦截功能。体现了对外扩展开放,对修改关闭的设计原则。

​ 在实际开发中,Servlet Filter、Spring Interceptor、Aop都可以实现鉴权、限流、日志等功能,由于三者的粒度不同,导致他们的应用范围也不同。例如Servlet Filter 是Servlet的规范,那么只要是实现Servlet规范的容器,都可以应用这个,而Spring Interceptor 必须依赖于Spring框架。

​ 此外,三者的执行先后也有不同。请求过来时先执行了Filter、才会执行Interceptor,而 Aop 是关注在方法层面,在执行某方法之时做代理拦截。