【设计模式与范式:行为型】63 | 职责链模式(下):框架中常用的过滤器、拦截器是如何实现的?

news2024/11/15 8:44:46

上一节课,我们学习职责链模式的原理与实现,并且通过一个敏感词过滤框架的例子,展示了职责链模式的设计意图。本质上来说,它跟大部分设计模式一样,都是为了解耦代码,应对代码的复杂性,让代码满足开闭原则,提高代码的可扩展性。

除此之外,我们还提到,职责链模式常用在框架的开发中,为框架提供扩展点,让框架的使用者在不修改框架源码的情况下,基于扩展点添加新的功能。实际上,更具体点来说,职责链模式最常用来开发框架的过滤器和拦截器。今天,我们就通过 Servlet Filter、Spring Interceptor 这两个 Java 开发中常用的组件,来具体讲讲它在框架开发中的应用。

话不多说,让我们正式开始今天的学习吧!

Servlet Filter

Servlet Filter 是 Java Servlet 规范中定义的组件,翻译成中文就是过滤器,它可以实现对 HTTP 请求的过滤功能,比如鉴权、限流、记录日志、验证参数等等。因为它是 Servlet 规范的一部分,所以,只要是支持 Servlet 的 Web 容器(比如,Tomcat、Jetty 等),都支持过滤器功能。为了帮助你理解,我画了一张示意图阐述它的工作原理,如下所示。

在这里插入图片描述
在实际项目中,我们该如何使用 Servlet Filter 呢?我写了一个简单的示例代码,如下所示。添加一个过滤器,我们只需要定义一个实现 javax.servlet.Filter 接口的过滤器类,并且将它配置在 web.xml 配置文件中。Web 容器启动的时候,会读取 web.xml 中的配置,创建过滤器对象。当有请求到来的时候,会先经过过滤器,然后才由 Servlet 来处理。

public class LogFilter implements Filter {
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    // 在创建Filter时自动调用,
    // 其中filterConfig包含这个Filter的配置参数,比如name之类的(从配置文件中读取的)
  }
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    System.out.println("拦截客户端发送来的请求.");
    chain.doFilter(request, response);
    System.out.println("拦截发送给客户端的响应.");
  }
  @Override
  public void destroy() {
    // 在销毁Filter时自动调用
  }
}
// 在web.xml配置文件中如下配置:
<filter>
  <filter-name>logFilter</filter-name>
  <filter-class>com.xzg.cd.LogFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>logFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

从刚刚的示例代码中,我们发现,添加过滤器非常方便,不需要修改任何代码,定义一个实现 javax.servlet.Filter 的类,再改改配置就搞定了,完全符合开闭原则。那 Servlet Filter 是如何做到如此好的扩展性的呢?我想你应该已经猜到了,它利用的就是职责链模式。现在,我们通过剖析它的源码,详细地看看它底层是如何实现的。

在上一节课中,我们讲到,职责链模式的实现包含处理器接口(IHandler)或抽象类(Handler),以及处理器链(HandlerChain)。对应到 Servlet Filter,javax.servlet.Filter 就是处理器接口,FilterChain 就是处理器链。接下来,我们重点来看 FilterChain 是如何实现的。

不过,我们前面也讲过,Servlet 只是一个规范,并不包含具体的实现,所以,Servlet 中的 FilterChain 只是一个接口定义。具体的实现类由遵从 Servlet 规范的 Web 容器来提供,比如,ApplicationFilterChain 类就是 Tomcat 提供的 FilterChain 的实现类,源码如下所示。

为了让代码更易读懂,我对代码进行了简化,只保留了跟设计思路相关的代码片段。完整的代码你可以自行去 Tomcat 中查看。

public final class ApplicationFilterChain implements FilterChain {
  private int pos = 0; //当前执行到了哪个filter
  private int n; //filter的个数
  private ApplicationFilterConfig[] filters;
  private Servlet servlet;
  
  @Override
  public void doFilter(ServletRequest request, ServletResponse response) {
    if (pos < n) {
      ApplicationFilterConfig filterConfig = filters[pos++];
      Filter filter = filterConfig.getFilter();
      filter.doFilter(request, response, this);
    } else {
      // filter都处理完毕后,执行servlet
      servlet.service(request, response);
    }
  }
  
  public void addFilter(ApplicationFilterConfig filterConfig) {
    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;
  }
}

ApplicationFilterChain 中的 doFilter() 函数的代码实现比较有技巧,实际上是一个递归调用。你可以用每个 Filter(比如 LogFilter)的 doFilter() 的代码实现,直接替换 ApplicationFilterChain 的第 12 行代码,一眼就能看出是递归调用了。我替换了一下,如下所示。

  @Override
  public void doFilter(ServletRequest request, ServletResponse response) {
    if (pos < n) {
      ApplicationFilterConfig filterConfig = filters[pos++];
      Filter filter = filterConfig.getFilter();
      //filter.doFilter(request, response, this);
      //把filter.doFilter的代码实现展开替换到这里
      System.out.println("拦截客户端发送来的请求.");
      chain.doFilter(request, response); // chain就是this
      System.out.println("拦截发送给客户端的响应.")
    } else {
      // filter都处理完毕后,执行servlet
      servlet.service(request, response);
    }
  }

这样实现主要是为了在一个 doFilter() 方法中,支持双向拦截,既能拦截客户端发送来的请求,也能拦截发送给客户端的响应,你可以结合着 LogFilter 那个例子,以及对比待会要讲到的 Spring Interceptor,来自己理解一下。而我们上一节课给出的两种实现方式,都没法做到在业务逻辑执行的前后,同时添加处理代码。

Spring Interceptor

刚刚讲了 Servlet Filter,现在我们来讲一个功能上跟它非常类似的东西,Spring Interceptor,翻译成中文就是拦截器。尽管英文单词和中文翻译都不同,但这两者基本上可以看作一个概念,都用来实现对 HTTP 请求进行拦截处理。

在这里插入图片描述
它们不同之处在于,Servlet Filter 是 Servlet 规范的一部分,实现依赖于 Web 容器。Spring Interceptor 是 Spring MVC 框架的一部分,由 Spring MVC 框架来提供实现。客户端发送的请求,会先经过 Servlet Filter,然后再经过 Spring Interceptor,最后到达具体的业务代码中。我画了一张图来阐述一个请求的处理流程,具体如下所示。

在项目中,我们该如何使用 Spring Interceptor 呢?我写了一个简单的示例代码,如下所示。LogInterceptor 实现的功能跟刚才的 LogFilter 完全相同,只是实现方式上稍有区别。LogFilter 对请求和响应的拦截是在 doFilter() 一个函数中实现的,而 LogInterceptor 对请求的拦截在 preHandle() 中实现,对响应的拦截在 postHandle() 中实现。

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="com.xzg.cd.LogInterceptor" />
   </mvc:interceptor>
</mvc:interceptors>

同样,我们还是来剖析一下,Spring Interceptor 底层是如何实现的。

当然,它也是基于职责链模式实现的。其中,HandlerExecutionChain 类是职责链模式中的处理器链。它的实现相较于 Tomcat 中的 ApplicationFilterChain 来说,逻辑更加清晰,不需要使用递归来实现,主要是因为它将请求和响应的拦截工作,拆分到了两个函数中实现。HandlerExecutionChain 的源码如下所示,同样,我对代码也进行了一些简化,只保留了关键代码。

public class HandlerExecutionChain {
 private final Object handler;
 private HandlerInterceptor[] interceptors;
 
 public void addInterceptor(HandlerInterceptor interceptor) {
  initInterceptorList().add(interceptor);
 }
 boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
  HandlerInterceptor[] interceptors = getInterceptors();
  if (!ObjectUtils.isEmpty(interceptors)) {
   for (int i = 0; i < interceptors.length; i++) {
    HandlerInterceptor interceptor = interceptors[i];
    if (!interceptor.preHandle(request, response, this.handler)) {
     triggerAfterCompletion(request, response, null);
     return false;
    }
   }
  }
  return true;
 }
 void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception {
  HandlerInterceptor[] interceptors = getInterceptors();
  if (!ObjectUtils.isEmpty(interceptors)) {
   for (int i = interceptors.length - 1; i >= 0; i--) {
    HandlerInterceptor interceptor = interceptors[i];
    interceptor.postHandle(request, response, this.handler, mv);
   }
  }
 }
 void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, Exception ex)
   throws Exception {
  HandlerInterceptor[] interceptors = getInterceptors();
  if (!ObjectUtils.isEmpty(interceptors)) {
   for (int i = this.interceptorIndex; i >= 0; i--) {
    HandlerInterceptor interceptor = interceptors[i];
    try {
     interceptor.afterCompletion(request, response, this.handler, ex);
    } catch (Throwable ex2) {
     logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
    }
   }
  }
 }
}

在 Spring 框架中,DispatcherServlet 的 doDispatch() 方法来分发请求,它在真正的业务逻辑执行前后,执行 HandlerExecutionChain 中的 applyPreHandle() 和 applyPostHandle() 函数,用来实现拦截的功能。具体的代码实现很简单,你自己应该能脑补出来,这里就不罗列了。感兴趣的话,你可以自行去查看。

重点回顾

好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。

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

今天,我们通过 Servlet Filter、Spring Interceptor 两个实际的例子,给你展示了在框架开发中职责链模式具体是怎么应用的。从源码中,我们还可以发现,尽管上一节课中我们有给出职责链模式的经典代码实现,但在实际的开发中,我们还是要具体问题具体对待,代码实现会根据不同的需求有所变化。实际上,这一点对于所有的设计模式都适用。

课堂讨论

  • 前面在讲代理模式的时候,我们提到,Spring AOP 是基于代理模式来实现的。在实际的项目开发中,我们可以利用 AOP 来实现访问控制功能,比如鉴权、限流、日志等。今天我们又讲到,Servlet Filter、Spring Interceptor 也可以用来实现访问控制。那在项目开发中,类似权限这样的访问控制功能,我们该选择三者(AOP、Servlet Filter、Spring Interceptor)中的哪个来实现呢?有什么参考标准吗?
  • 除了我们讲到的 Servlet Filter、Spring Interceptor 之外,Dubbo Filter、Netty ChannelPipeline 也是职责链模式的实际应用案例,你能否找一个你熟悉的并且用到职责链模式的框架,像我一样分析一下它的底层实现呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/658696.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ribbon LoadBalanced底层机制源码探秘

&#x1f34a; Java学习&#xff1a;社区快速通道 &#x1f34a; 深入浅出RocketMQ设计思想&#xff1a;深入浅出RocketMQ设计思想 &#x1f34a; 绝对不一样的职场干货&#xff1a;大厂最佳实践经验指南 &#x1f4c6; 最近更新&#xff1a;2023年6月18日 &#x1f34a; 点…

如何自己开发浏览器js插件

大家都知道在网页控制台编写的js脚本一刷新就没了下面教程教大家如何自己写一个js插件&#xff0c;此教程是小白在网上看到的的确认有效才发出来的&#xff0c;无需借助油猴。 最近工作需要小白研究了一下浏览器插件编写的过程下面分享给大家 步骤 在桌面创建一个文件夹&…

指针与数组---指针与一维数组的关系

C语言的高效得益于它指针功能的强大。然而C语言中的指针和数组的关系似乎很“纠结”&#xff0c;让人爱恨交织。指向数组的指针变量、指针数组等&#xff0c;似乎总是“你中有我&#xff0c;我中有你”。 目录 一、数组名的特殊意义及其在访问数组元素中的作用 二、指针运算…

Linux常用指令和知识(1)

目录 ls cd pwd 相对路径&绝对路径&特殊路径符 mkdir touch-cat-more cp-mv-rm which-find grep-wc 管道符 | echo 重定向符 tail &#x1f636;‍&#x1f32b;️&#x1f618;创作不易, 多多支持 前言: 我们学习的Linux命令, 其实他们的本体就是一个个…

ctf 逆向 专题题解

本文的目标是&#xff0c;记录一些不具备通用性的&#xff0c;或者比较进阶的题目。之前的另一篇文章则用于记录一些基础知识和通用性较强的基本手法。 文章目录 跨科题目buu fungame&#xff1a;reverse与pwn的结合reverseweb 反跟踪Easyhook&#xff1a;hook例题 vm类型总结一…

我的创作纪念日——512

机缘 没想到不知不觉在CSDN创作就512天了&#xff0c;想到一开始就仅仅想在CSDN记笔记&#xff0c;到现在成为一个小博主&#xff0c;认识到了很多志同道合的伙伴&#xff0c;中间创作我也曾经懒惰过&#xff0c;放弃过&#xff0c;但我一次又一次重新进行创作&#xff0c;虽然…

AcWing801: 二进制中1的个数(两种方法详解)

原题引出 方法一&#xff1a;使用lowbit 算法的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)&#xff0c;使用lowbit操作&#xff0c;每次操作截取一个数字的最后一个1后面的所有位&#xff0c;每次减去lowbit得到的数字&#xff0c;直到数字减到0&#xff0c;就得到了最终…

【MySQL】选择专题(七)

文章目录 选择题选择题 在关系R ( R # , RN , S # )和S ( S # , SN , SD )中, R 的主码是R # , S 的主码是S #,则S#在R 中称为( A )。 A 外码 B 候选码 C 主码 D 超码 设关系R和S的属性个数分别为2和3,那么等价于( B )。 A. σ1<2(RS) B. σ1<4(RS) C. σ1<2(R…

我们世界中的计算机——从大师视角诠释计算常识

计算机和通信系统&#xff0c;以及由它们所实现的许多事物遍布我们周围。其中一些在日常生活中随处可见&#xff0c;比如笔记本电脑、手机和互联网。今天&#xff0c;在任何公共场所&#xff0c;都会看到许多人在使用手机查询交通路线、购物以及和朋友聊天。与此同时&#xff0…

【大数据】大数据相关概念

文章目录 大数据&#xff1a;一种规模大到在获取、存储、管理、分析方面大大超出了传统数据库软件工具能力范围的数据集合&#xff0c;具有海量的数据规模、快速的数据流转、多样的数据类型以及价值密度四大特征。Hadoop&#xff1a;是一个能够对大量数据进行分布式处理的软件框…

15-3.自定义组件的生命周期函数

目录 1 组件自身的生命周期函数 1.1 使用lifetimes声明生命周期函数 1.2 不使用lifetimes声明生命周期函数 2 组件所在页面的生命周期函数 1 组件自身的生命周期函数 created 组件实例刚刚被创建后执行&#xff0c;可以理解为 html模板刚刚搞好attached 组件被放入节…

万物的算法日记|第六天

笔者自述&#xff1a; 一直有一个声音也一直能听到身边的大佬经常说&#xff0c;要把算法学习搞好&#xff0c;一定要重视平时的算法学习&#xff0c;虽然每天也在学算法&#xff0c;但是感觉自己一直在假装努力表面功夫骗了自己&#xff0c;没有规划好自己的算法学习和总结&am…

DJ4-2 数据报网络和虚电路网络

目录 一、连接和无连接服务 二、数据报网络 1、数据报网络的转发表 2、数据报网络的特点 三、虚电路网络 (Virtual Circuits)* 1、虚电路网络的工作方式 2、虚电路网络的特点 一、连接和无连接服务 任何网络中的网络层只会提供两种服务之一&#xff0c;不会同时提供 数…

Mysql数据库之事务(山高水远,他日江湖再见)

文章目录 一、事务的概念二、事务的ACID特点1.原子性&#xff08;Atomicity&#xff09;2.一致性&#xff08;Consistency&#xff09;3.隔离性&#xff08;lsolation&#xff09;4.持久性&#xff08;Durability) 三、并发访问表的一致性问题和事务的隔离级别1.并发访问表的一…

融合模型stacking14条经验总结和5个成功案例(互联网最全,硬核收藏)_机器学习_人工智能_模型竞赛_论文参考

我看了很多关于融合模型stacking文章&#xff0c;很多作者倾向于赞美融合模型stacking&#xff0c;对其缺点轻描淡写&#xff0c;这容易误导初学者。一叶障目就是这意思。 我的很多学员喜欢用融合模型作为论文或专利创新点&#xff0c;这是一个热门技术。 最近有个同学在论文…

设计模式之单例模式笔记

设计模式之单例模式笔记 说明Singleton(单例)目录单例模式之饿汉式-静态成员变量写法测试类 单例模式之饿汉式-静态代码块写法测试类 单例模式之懒汉式-线程不安全写法和线程安全写法测试类 单例模式之懒汉式-双重检查锁方式(推荐使用的方式)单例模式之懒汉式-静态内部类方式(推…

Mysql数据库之存储引擎(羡慕她人,不如提升自己)

一、存储引擎概念 MySQL中的数据用各种不同的技术存储在文件中&#xff0c;每一种技术都使用不同的存储机制、索引技巧、锁定水平并最终提供不同的功能和能力&#xff0c;这些不同的技术以及配套的功能在MySQL中称为存储引擎。 存储引擎是MySQL将数据存储在文件系统中的存储方…

ELK日志收集系统简述

一、概述 &#xff08;一&#xff09;ELK由三个组件构成 ELK是三个开源软件的缩写&#xff0c;分别是Elasticsearch、Logstash、Kibana ELK 架构基本组成 &#xff08;二&#xff09;作用 1、日志收集 2、日志分析 3、日志可视化 &#xff08;三&#xff09;为什么使用EL…

计网之应用层

因特网协议概述 常用协议应用层HTTP&#xff08;超文本传输协议&#xff09;、FTP&#xff08;文件传输协议&#xff09;、SMTP&#xff08;简单邮件传输协议&#xff09;、DNS&#xff08;域名系统&#xff09;、DHCP&#xff08;动态主机配置协议&#xff09;、SNMP&#xff…

15-6.自定义组件的代码共享

在微信小程序中使用 behaviors 进行代码共享&#xff0c;功能类似于vue的mixins 每个behavior可以包含一组属性、数据、生命周期函数和方法 每个组件可以引用多个behavior&#xff0c;behavior也可以引用其他的behavior 目录 1 创建behavior 2 使用behavior 3 behavio…