关于Spring的两三事:傻傻分不清楚的filter和interceptor

人生苦短,不如养狗

作者:闲宇

大众号:Brucebat的伪技术鱼塘

一、前言

  从触摸Spring开端咱们就常常能听到filter(过滤器)interceptor(阻拦器) 这两个概念,但当咱们真实要去运用它们的时分却又经常傻傻分不清楚两者的异同。这其间最大的原因就在于两者的功用(权限校验、日志处理、数据解压/紧缩处理等)过于类似,filter可以完结的场景interceptor同样也可以完结,导致两者的边界感十分含糊。为了弄清楚两者的异同,让咱们追根溯源,从源头上开端了解一下两者的来源和规划理念。

以下解说基于SpringBoot 2.7.5版别

二、舶来品和原住民

Filter:舶来品

1. 基本概念

  当咱们仔细阅览源码之后会发现filter这个概念竟然是一个源自于Servlet的舶来品(遵循Servlet标准),这儿可以看一下Filter类的全限定名:

javax.servlet.Filter

  可以看到Filter自身是用在Tomcat等Web容器进行Servlet相关处理时运用的东西,并非是Spring原生的东西。从这一发现中咱们不难推测在Spring中为什么Filter和Interceptor在功用上是如此的附近,因为这两者的作者并非一人,在构建各自体系时发生相同的想法和思路也是可以理解的,毕竟正人所见略同也是时有发生的工作。后续Spring为了引入和兼容Tomcat容器的处理逻辑,将两个较为类似地概念放置在同一个应用上下文中(留意,Spring并没有做兼并处理,仅仅兼容),导致开发者经常含糊也变得情有可原。

  为了更好地了解Filter的功用,这儿咱们引入官方注释来帮助理解:

A filter is an object that performs filtering tasks on either the request to a resource (a servlet or static content), or on the response from a resource, or both.

过滤器是对资源恳求(servlet 或静态内容)或来自资源的呼应或两者履行过滤使命的目标。

  从上面的界说中咱们可以得到两点有用信息:

  • 履行机遇Filter的履行机遇有两个,分别是对资源的恳求被履行前将来自资源的呼应回来前
  • 履行内容:过滤器实质是在履行一个过滤使命,而过滤条件需求根据对资源的恳求或许来自资源的呼应进行判别。

  除了上面的两点信息以外,在结合Tomcat中关于Servlet容器的结构规划之后咱们可以得到下面关于Filter履行进程的流程图

关于Spring的两三事:傻傻分不清楚的filter和interceptor

  在实践开发场景中,关于资源恳求的预处理或许资源呼应的后置处理可能不单只会有一类过滤使命,所以Tomcat在编码规划中运用了职责链形式来完结关于需求运用多个不同类型过滤器处理恳求或许呼应的场景,这一点在上面的流程图中也有所体现。这儿需求留意一点,因为运用了链式结构这一线性数据结构,在filter的实践履行进程中就会存在履行次序的问题,这就意味着咱们在完结自界说过滤器时不能呈现过滤器依靠倒置的状况,当然假如过滤器之间不存在依靠关系则无需考虑次序问题。在Tomcat中运用org.apache.catalina.core.ApplicationFilterChain来完结上面说到的职责链形式,这儿咱们可以结合部分代码简略了解一下:

public final class ApplicationFilterChain implements FilterChain {
 
 public void doFilter(ServletRequest request, ServletResponse response)
    throws IOException, ServletException {
​
    if( Globals.IS_SECURITY_ENABLED ) {
      final ServletRequest req = request;
      final ServletResponse res = response;
      try {
        java.security.AccessController.doPrivileged(
             (java.security.PrivilegedExceptionAction<Void>) () -> {
             // 进行实践的filter履行
              internalDoFilter(req,res);
              return null;
             }
         );
       } catch( PrivilegedActionException pe) {
         ...
       }
     } else {
     // 进行实践的filter履行
      internalDoFilter(request,response);
     }
   }
 
 
 private void internalDoFilter(ServletRequest request,
                 ServletResponse response)
    throws IOException, ServletException {
​
    // Call the next filter if there is one
    if (pos < n) {
      ApplicationFilterConfig filterConfig = filters[pos++];
      try {
        Filter filter = filterConfig.getFilter();
         ...
        if( Globals.IS_SECURITY_ENABLED ) {
           ...
         } else {
         // 这儿需求结合Filter类一同分析,实践上这儿履行的是一个回调函数,
         // 该办法的第三个参数将当前applicationFilterChain目标传入,结合上面的pos指针来判别是否现已将过滤器链履行完结
          filter.doFilter(request, response, this);
         }
       } catch (IOException | ServletException | RuntimeException e) {
        throw e;
       } catch (Throwable e) {
         ...
       }
      return;
     }
​
    // We fell off the end of the chain -- call the servlet instance
    try {
       ...
       // 实践履行servlet对应服务,留意这儿仅仅进入到servlet实例傍边,并没有真实进入到某个handle傍边
        servlet.service(request, response);
       ...
     } catch (IOException | ServletException | RuntimeException e) {
      throw e;
     } catch (Throwable e) {
       ...
     } finally {
       ...
     }
   }
}

  从上面的代码中咱们可以看到Tomcat运用了pos指针来完结关于过滤器链中过滤器履行方位的记录,在完结链中一切过滤器履行而且经过之后,requestresponse目标才会提交给servlet实例进行对应服务的处理。需求留意,此刻并没有涉及到某个详细handler,也便是说filter的处理并不能细化到某一类详细的handler恳求/呼应,只能较为含糊处理整个servlet实例维度的恳求/呼应。

  当然,从上面的代码咱们可以发现另外一个问题:代码中好像只要针对资源恳求维度的过滤处理而没有关于资源呼应的过滤处理。其实关于资源呼应的过滤处理被隐藏在每个过滤器的doFilter办法中了,在完结自界说过滤器时咱们需求按照以下逻辑来编写代码才干完结关于资源呼应的处理:

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    // TODO 前置处理
    // 调用applicationFilterChain目标的doFilter办法(这儿实践上是一个回调逻辑),这儿一定要加上,不然链式结构就会从这儿断开。
    chain.doFilter(request, response);
    // TODO 后置处理
   }

  结合ApplicationFilterChaininternalDoFilter办法可以发现这儿隐含了一个入栈出栈(其实便是办法栈)的逻辑。关于资源恳求的预处理进程实践上是一个入栈的进程,当一切的预处理过滤器入栈结束则就会开端履行servlet.service(request, response)。在完结servlet服务处理之后,就会进入到出栈进程,此刻会从最终一个过滤器的后置处理逻辑(也便是上面代码中最终一行的方位)逐一履行并退出办法。

  不得不说,这儿的逻辑关于刚入门的新手来说的确不是十分友爱。因为Filter自身仅仅一个接口,并不能像抽象类相同供给一个模板办法,导致初学者在运用时假如没有一个比较好的案例参照,仅仅单纯看源码的话可能会发生和上面相同的疑问。这儿也要提醒大家在完结自界说过滤器时一定要按照上面的模板完结,不然会呈现链式进程断开或许后置逻辑无法完结的状况。

2. Spring中的运用

  尽管写着Spring,但实践上闲宇这儿要讲的是在SpringBoot傍边的运用办法。结合SpringBoot来完结自界说过滤器实践上只需求在原有的流程中加上注入到Spring容器中的逻辑就可以了,在SpringBoot中供给了两种办法完结这一操作:

  • 在自界说过滤器上运用@Component注解;
  • 在自界说过滤器上运用@WebFilter注解,并在发动类上运用@ServletComponentScan注解;

  这儿闲宇更推荐运用第二种办法来完结过滤器的注入,因为Spring在兼容过滤器的处理进程时还供给了原有Tomcat不存在的功用,即url匹配才能。结合@WebFilter注解中的urlPattern字段,Spring可以将过滤器的处理粒度进一步细化,让开发人员在运用上变得愈加灵活。除此之外,为了确定过滤器注入的次序,咱们还可以运用Spring供给的@Order注解来自界说过滤器的次序。

Interceptor:Spring的原住民

1. 基本概念

  看完了filter,咱们再将目光转回到Interceptor上。这一次咱们可以发现Interceptor这样一个概念是Spring原创的,其对应的详细接口类为HandlerInterceptor(当然还有一个异步阻拦器接口类,这儿咱们就不做扩展,有爱好的同学可以自行阅览源码学习)。在阅览完对应的源码之后咱们可以发现,区别于Filter只供给了一个简略的doFilter办法, 在HandlerInterceptor傍边清晰供给了三个与履行机遇相关的办法:

  • preHandle: 在履行对应handler之前会履行该办法进行前置处理;
  • postHandle: 在对应handler完结恳求处理之后且在ModelAndView目标被烘托之前会履行该办法来进行一些关于ModelAndView目标的后置处理;
  • afterCompletion: 在ModelAndView目标完结烘托之后且在呼应回来之前会履行该办法对结果进行后置处理;

  相比Filter类中仅仅简略供给了一个doFilter办法,HandlerInterceptor中的办法界说显得愈加清晰和友爱。在不阅览源码和参考运用典范的状况下,咱们也能大致猜测到需求如何完结自界说阻拦器。

  结合org.springframework.web.servlet.DispatcherServlet#doDispatch中的源码咱们可以绘制出如下的流程图(这儿就不贴出详细代码了,有爱好的同学可以自行阅览):

关于Spring的两三事:傻傻分不清楚的filter和interceptor

  可以看到此刻interceptor的履行逻辑都是包含在servlet实例傍边,结合上面filter的履行进程咱们不难发现,filter就像夹心饼干的两个饼干相同将servlet和interceptor夹在中心,interceptor的履行机遇是要晚于filter的前置处理而且早于filter的后置处理的。除此以外,在阅览源码进程中咱们可以发现Spring在运用interceptor时同样也是用了职责链形式,不得不说在这种需求逐一履行不同使命处理逻辑的场景下职责链形式仍是十分好用的。需求留意,因为Spring在界说阻拦器时现已清晰了不同阶段履行的办法,所以在实践履行阻拦器时并没有采用和过滤器相同的入栈出栈办法。

2. Spring中的运用

  在SpringBoot傍边运用interceptor除了需求完结HandlerInterceptor接口,还需求显示注册Spring的Web配置傍边,详细代码如下:

@Configuration
public class WebConfig implements WebMvcConfigurer {
​
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new DemoInterceptor()).addPathPatterns("/api/*").excludePathPatterns("/api/ok");
   }
}

  从上面的代码中咱们可以看到,Spring也给自界说阻拦器供给了和filter相同路径匹配功用,经过这样一个功用自界说阻拦器可以针对更细粒度的handler恳求和呼应处理。(再一次和filter撞功用,当然这儿是Spring内部供给的才能)

三、常见运用场景

  其实在文章的最开端咱们现已介绍过一部分两者的功用,这儿咱们再来简略总结一下。从上面的分析中咱们不难发现filter和interceptor的规划者创造这两个东西的目的都是为了将针对恳求的预处理和针对呼应的后置处理从业务代码中剥离开来,将两者作为一个通用处理逻辑供给给开发人员自行扩展完结,从这一思想中咱们很容易看到AOP的身影(公然优异的思想总是相通的)。

  在实践的开发场景中,咱们常常会运用自界说过滤器或阻拦器来完结如下操作:

  • 用户登录校验;
  • 权限校验;
  • 日志阻拦处理;
  • 数据紧缩/解压处理;
  • 加密/解密处理;
  • ……

  这儿咱们就不展现每个场景的编码完结了,有爱好的同学可以自行查找了解一下。这儿可以给出一点主张,尽管上述场景看起来许多,但其实质上仍是针对恳求参数或许呼应结果在进行一些数据处理,基于这样一个认知咱们去规划完结上述这些场景就会显得相对轻松。

四、总结

  在咱们仔细分析之后可以看到filter和interceptor并没有实质上的区别,作为一个东西来说,他们两者可以供给的才能基本是相同的。仅有需求留意的便是在履行机遇上两者的不同(一个是在servlet履行前后处理,一个是在servlet内部履行),其他方面并没有显著的不同。这这也就意味着咱们在实践开发运用时并不需求太过于纠结,毕竟老话说的好:黑猫白猫,抓到老鼠的便是好猫。

  最终,自始自终,祝愿大家身体健康,早日升值加薪。近期好像疫情开端重复,出门在外留意防护,保重身体。