彻底理解Spring Interceptor和Servlet Filter

老丐说码
English

微服务时代,java依靠SpringBoot又再度提升热度。原本以为php, python之类的会持续蚕食Java的领地,熟知微服务又恢复了Java往日的地位。SpringBoot依赖Spring生态圈满满圈粉,热度更胜当年。

SpringBoot的本质就是实现了自动装配,解决了Spring研发的配置地域问题。但是它的基础依然是Spring, 对于web研发的基础依然是SpringMVC。因此有必要深入了解Spring, SpringMVC。

对于Spring来说,拦截器和过滤器是非常核心和重要的两个概念。因此本文针对这两个概念进行深入分析,让我们彻底理解拦截器和过滤器。

关于拦截器和过滤器是什么,有大量文章已经做了说明,我们这里不详细阐述。本文分为五个部分:

  1. 拦截器和过滤器的区别
  2. 拦截器使用
  3. 过滤器使用
  4. 拦截器和过滤器的应用场景
  5. 拦截器和过滤器的执行过程及流程

1. 拦截器和过滤器的区别

过滤器(Filter)拦截器(Interceptor)总结
Filter接口定义在javax.servlet包中HandlerInterceptor接口定义在org.springframework.web.servlet包中
Filter定义在web.xml中HandlerInterceptor是在应用程序上下文配置的。
Filter只在Servlet前后起作用。Filter通常将请求和响应当做黑盒子,Filter通常不考虑Servlet的实现。拦截器能深入到方法前后、异常抛出前后等,因此拦截器的使用具有更大的弹性。允许用户接入(hook into)请求的生命周期,在请求过程中获取信息,Interceptor通常和请求更加耦合。在Spring架构的程序中,要有限使用拦截器。几乎所有Filter能做的事情,Interceptor都能够轻松的实现。
Filter是Servlet规范规定的。拦截器既可以用于Web程序,也可以用于Application、Swing程序中。使用范围不同
Filter是在Servlet规范中定义的,是Servlet容器支持的。拦截器是在Spring容器内的,是Spring框架支持的。规范不同
Filter不能够使用Spring容器资源。拦截器是一个Spring的组件,归Spring管理,配置在Spring文件中,因此能使用Spring里的任何资源、对象,例如Service对象、数据源、事务管理等,通过IoC注入到拦截器即可。Spring中使用拦截器更容易
Filter是被Servlet(就像Tomcat)调用的。拦截器(Interceptor)是被Spring调用的。因此Filter总是优于Interceptor执行。

下面引用别人的一张图说明一下,在tomcat容器下,拦截器、过滤器、Servlet以及Spring Controller之间的关系。

在请求处理和响应过程中过滤器和拦截器等组件的执行流程及关系:

  1. 请求首先到达Servlet容器,将请求转给web容器。
  2. web容器调用Servlet过滤器,然后继续将请求转交给Spring的DispatcherServlet, 请求就转交给Spring上下文。
  3. DispatcherServlet就可以调用Spring的拦截器对请求进行拦截处理,最后交给资源控制器。
  4. 最终有资源控制器结合业务模型进行业务处理,然后沿着4到1一层层朝外对相应进行处理,最终返回给客户端。

2. 拦截器的使用

Interceptor的执行顺序大致为:

  • 请求到达DispatcherServlet
  • DispatcherServlet发送至Interceptor, 执行preHandle
  • 请求到达Controller
  • 请求结束后,postHandle执行

Spring中主要通过HandlerInterceptor接口来实现请求的拦截,实现HandlerInterceptor接口需要实现下面三个方法:

  • preHandle(): 在handle执行之前,返回boolean值,true表示继续执行,false为停止执行并返回。
  • postHandle(): 在handle执行之后,可以在返回之前对返回的结果进行修改。
  • afterCompletion(): 在请求完成,视图生成后调用。

2.1 利用拦截器进行请求耗时统计

请求耗时统计可以通过在请求处理之前记录一个时间,请求处理完成后再记录一个时间,打印出两个时间的差就是请求耗时。

2.1.1 定义一个拦截器

public class ExecuteTimeInterceptor extends HandlerInterceptorAdapter {
    // private static final Logger log = LoggerFactory.getLogger(ExecuteTimeInterceptor.class);

    /**
     * 在handler执行之前执行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        long startTime = System.currentTimeMillis();
        request.setAttribute("startTime", startTime);
        return super.preHandle(request, response, handler);
    }

    /**
     * 在handler执行之后执行
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        long startTime = (long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        long executeTime = endTime - startTime;

        System.out.println("[" + handler + "] executeTime : " + executeTime + "ms");
        super.postHandle(request, response, handler, modelAndView);
    }
}

2.1.2 配置拦截器

前面介绍我们知道拦截器是Spring上下文的东西,因此我们需要再Spring的上下文进行配置,使用springmvc-servlet.xml配置的方式如下:

<mvc:interceptors>
  <mvc:interceptor>
    <mvc:mapping path="/**"/>
    <bean class="com.xxx.interceptor.ExecuteTimeInterceptor" />
  </mvc:interceptor>
</mvc:interceptors>

2.1.3 运行测试

运行起来,然后访问一个接口,就会看到控制台有输出如下:

2.2 使用拦截器实现应用安全检查

通常我们的应用分为前端应用和管理后台应用,它们的安全检查策略是不同的。我们就可以通过拦截器实现不同安全策略的检查。具体的拦截器逻辑实现我们就不列举了,仅简单描述一下:

  1. 对于所有前端应用的请求都进行一般的安全检查,我们暂且让FrontSecurityInterceptor拦截器来完成具体的业务。
  2. 对于所有后端应用的请求都进行较严格的安全检查,因为后台数据很重要。我们姑且用BackendSecurityInterceptor拦截器来完成这个艰巨的任务。
  3. 另外,我们前端应用和管理端应用都部署在同一个域名www.example.com下,前台应用直接访问这个链接即可,后台管理我们通过添加前缀/admin来访问。

那么我们就可以配置拦截器如下所示:

<mvc:interceptors>
  <mvc:interceptor>
    <mvc:mapping path="/**"/>
    <mvc:exclude-mapping path="/admin/**" />
    <bean class="com.yang.interceptor.FrontSecurityInterceptor" />
  </mvc:interceptor>
  <mvc:interceptor>
    <mvc:mapping path="/admin/**"/>
    <bean class="com.yang.interceptor.BackendSecurityInterceptor" />
  </mvc:interceptor>
</mvc:interceptors>

2.3 Spring拦截器总结

我们可以利用mvc:interceptors标签声明一系列的拦截器,然后它们就可以形成一个拦截器链,拦截器的执行顺序是按声明的先后顺序执行的,先声明的拦截器中的preHandle方法会先执行,然而它的postHandle方法和afterCompletion方法却会后执行。

在mvc:interceptors标签下声明interceptor主要有两种方式:

  1. 直接定义一个Interceptor实现类的bean对象。使用这种方式声明的Interceptor拦截器将会对所有的请求进行拦截。
  2. 使用mvc:interceptor标签进行声明。使用这种方式进行声明的Interceptor可以通过mvc:mapping子标签来定义需要进行拦截的请求路径。

3. Filter的使用

Servlet 的 Filter 接口需要实现如下方法:

  • void init(FilterConfig paramFilterConfig) – 当容器初始化 Filter 时调用,该方法在 Filter 的生命周期只会被调用一次,一般在该方法中初始化一些资源,FilterConfig 是容器提供给 Filter 的初始化参数,在该方法中可以抛出 ServletException 。init 方法必须执行成功,否则 Filter 可能不起作用,出现以下两种情况时,web 容器中 Filter 可能无效:

    • 抛出 ServletException
    • 超过 web 容器定义的执行时间。
  • doFilter(ServletRequest paramServletRequest, ServletResponse paramServletResponse, FilterChain paramFilterChain) – Web 容器每一次请求都会调用该方法。该方法将容器的请求和响应作为参数传递进来, FilterChain 用来调用下一个 Filter。
  • void destroy() – 当容器销毁 Filter 实例时调用该方法,可以在方法中销毁资源,该方法在 Filter 的生命周期只会被调用一次。

Filter和Interceptor的一些用途

以下是Filter接口源代码中列举出的过滤器的一些用途

  • Authentication Filters
  • Logging and Auditing Filters
  • Image conversion Filters
  • Data compression Filters
  • Encryption Filters
  • Tokenizing Filters
  • Filters that trigger resource access events
  • XSL/T filters
  • Mime-type chain Filter

4.1 请求过滤器应用场景

  • 执行安全检查(perform security checks)
  • 格式化请求header和body(reformat request headers or bodies)
  • 审查或记录日志(audit or log requests)
  • 根据请求内容授权或者限制用户访问(Authentication-Blocking requests based on user identity)
  • 根据请求频率限制用户访问

4.2 响应过滤器应用场景

  • 压缩响应内容,比如让下载的内容更小(Compress the response stream)
  • 追加或者修改响应(append or alter the response stream)
  • 创建或者整体修改响应(create a different response altogether)
  • 根据地方不同修改响应内容(Localization-Targeting the request and response to a particular locale)

5. 验证过滤器和拦截器执行过程

5.1 定义两个过滤器和拦截器

下面我们验证一下过滤器和拦截器的执行流程和顺序:

  1. 定义两个filter, 在web.xml中进行配置: 分别是FirstFilter和SecondFilter, 对所有请求进行过滤。
  2. 定义两个拦截器,在spring上下文中进行配置,这里在springmvc-servlet.xml中进行配置: 分别是FirstInterceptor和SecondInterceptor。

两个Filter实现基本类似:

// Filter
public class FirstFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println(this.getClass().getName() + ": init");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println(this.getClass().getName() + ": doFilter");
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        System.out.println(this.getClass().getName() + ": destroy");
    }
}

两个Interceptor的实现也基本相似:

public class FirstInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(this.getClass().getName() + ": preHandle");
        return super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(this.getClass().getName() + ": postHandle");
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(this.getClass().getName() + ": afterCompletion");
        super.afterCompletion(request, response, handler, ex);
    }
}

5.2 配置过滤器和拦截器

在web.xml配置过滤器:

<!-- 定义两个filter -->
<filter>
  <filter-name>firstFilter</filter-name>
  <filter-class>com.yang.filter.FirstFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>firstFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
  <filter-name>secondFilter</filter-name>
  <filter-class>com.yang.filter.SecondFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>secondFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

在springmvc-servlet.xml中配置拦截器:

<mvc:interceptors>
  <mvc:interceptor>
    <mvc:mapping path="/**"/>
    <bean class="com.yang.interceptor.ExecuteTimeInterceptor" />
  </mvc:interceptor>

  <mvc:interceptor>
    <mvc:mapping path="/**"/>
    <bean class="com.yang.interceptor.FirstInterceptor" />
  </mvc:interceptor>

  <mvc:interceptor>
    <mvc:mapping path="/**"/>
    <bean class="com.yang.interceptor.SecondInterceptor" />
  </mvc:interceptor>
</mvc:interceptors>

5.3 验证结果

然后运行Tomcat, 可以看到FirstFilter, SecondFilter的init()方法在启动的时候就被执行了。

我们启动tomcat后自动打开一个jsp页面: 我们看到两个filter的doFilter也执行了:

然后我们访问一个API接口: GET /demo/hello, 我们看到控制台输出:

5.4 总结

  1. 过滤器是被容器调用的,容器启动会调用过滤器的init()方法。
  2. 过滤器是针对所有请求的,包括普通的jsp页面,比如首页。
  3. 多个过滤器执行顺序是按照在web.xml中配置的顺序执行的。
  4. 拦截器是Spring上下文执行的,是在容器启动之后,请求被转给Spring的时候。
  5. 多个拦截器preHandle执行顺序按照在容器中配置的顺序进行执行。
  6. 多个拦截器afterHandle执行顺序按照在容器中配置的倒序进行执行。
  7. 多个拦截器afterCompletion执行顺序按照在容器中配置的倒序进行执行,并且在所有afterHandle执行完成之后执行。

码字不易,如果对大家有用,请关注 “老丐说码”。

老丐说码: 专注Java研发、SpringBoot、Spring Cloud,后面持续退出一系列相关文章。

另外推荐大家一个免费好用的本地markdown编辑器: Typora

代码地址: springmvc - springmvc-handleflow
参考文章:

阅读 367

10多年互联网研发经验,技术老丐一枚。

872 声望
285 粉丝
0 条评论
你知道吗?

10多年互联网研发经验,技术老丐一枚。

872 声望
285 粉丝
宣传栏