第一部分:过滤器Filter


一:Filter简介

Servlet API中提供了一个javax.servlet.Filter接口,开发web应用时,实现了这个接口的Java类,则把这个java类称之为过滤器Filter。通过Filter技术,开发人员可以实现用户在访问某个目标资源之前,对访问的请求和响应进行拦截,简单说,就是可以实现web容器对某目标资源的访问前截获进行相关的处理,还可以在某目标资源向web容器返回响应前进行截获进行处理。

Filter也称之为过滤器,它是Servlet技术中比较激动人心的技术,WEB开发人员通过Filter技术,对web服务器管理的所有web资源:
例如JSP, Servlet, 静态图片文件或静态html文件等进行拦截,从而实现一些特殊的功能。
例如实现URL级别的权限访问控制、过滤敏感词汇、压缩响应信息等一些高级功能。

Servlet的过滤器与Spring拦截器详解_java

二:Filter的生命周期

Filter接口有三个方法,初始化init、处理过滤器doFilter、销毁destroy

public interface Filter {

    public void init(FilterConfig filterConfig) throws ServletException;

    public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain)
            throws IOException, ServletException;

    public void destroy();
}

Filter的创建和销毁是由WEB服务器负责

  1. 在应用启动的时候就进行装载Filter类(与Servlet的load-on-startup配置效果相同)。
  2. 容器创建好Filter对象实例后,调用init()方法。接着被Web容器保存进应用级的集合容器中去了等待着,用户访问资源。
  3. 当用户访问的资源正好被Filter的url-pattern拦截时,容器会取出Filter类调用doFilter方法,下次或多次访问被拦截的资源时,Web容器会直接取出指定Filter对象实例调用doFilter方法(Filter对象常驻留Web容器了)。
  4. 当应用服务被停止或重新装载了,则会执行Filter的destroy方法,Filter对象销毁。

注意:init方法与destroy方法只会直接一次。

三:示例程序

1、创建两个过滤器MyFilter、MyFilter2

MyFilter

package com.mengdee.manager.filter;

import java.io.IOException;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyFilter implements Filter{

    @SuppressWarnings("unused")
    private FilterConfig filterConfig;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
        System.out.println("MyFilter init, filterConfig="+filterConfig);
        System.out.println("myParam="+filterConfig.getInitParameter("myParam"));
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("MyFilter doFilter before");
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html;charset=utf-8");

        chain.doFilter(request, response);

        System.out.println("MyFilter doFilter after");
    }

    @Override
    public void destroy() {
        System.out.println("MyFilter destroy");
    }

}

MyFilter2

package com.mengdee.manager.filter;

import java.io.IOException;

import javax.servlet.*;


public class MyFilter2 implements Filter{

    @SuppressWarnings("unused")
    private FilterConfig filterConfig;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
        System.out.println("MyFilter2 init, filterConfig="+filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("MyFilter2 doFilter before");

        chain.doFilter(request, response);

        System.out.println("MyFilter2 doFilter after");
    }

    @Override
    public void destroy() {
        System.out.println("MyFilter2 destroy");
    }

}

2、配置过滤器

配置过滤器可以在web.xml中配置,也可以通过使用注解方式 @WebFilter配置

web.xml配置方式

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <filter>
    <filter-name>MyFilter</filter-name>
    <filter-class>com.mengdee.manager.filter.MyFilter</filter-class>
    <init-param>
        <param-name>myParam</param-name>
        <param-value>hello</param-value>
    </init-param>
  </filter>

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

  <filter>
    <filter-name>MyFilter2</filter-name>
    <filter-class>com.mengdee.manager.filter.MyFilter2</filter-class>
    <init-param>
        <param-name>myParam</param-name>
        <param-value>hello</param-value>
    </init-param>
  </filter>

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

    <!-- 没有配置dispatcher就是默认request方式的 -->
    <dispatcher>REQUEST</dispatcher> 
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>ERROR</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
  </filter-mapping>
</web-app>

注解配置方式 @WebFilter

@WebFilter(filterName="MyFilter", urlPatterns="/*")
public class MyFilter implements Filter{

}

启动Tomcat:打印日志

MyFilter2 init, filterConfig=ApplicationFilterConfig[name=MyFilter2, filterClass=com.mengdee.manager.filter.MyFilter2]
MyFilter init, filterConfig=ApplicationFilterConfig[name=MyFilter, filterClass=com.mengdee.manager.filter.MyFilter]
myParam=hello

访问http://localhost:8081/platform-web/index.jsp 打印日志

MyFilter doFilter before
MyFilter2 doFilter before
MyFilter2 doFilter after
MyFilter doFilter after

重新发布:
MyFilter destroy
MyFilter2 destroy

注意多个过滤器在chain.doFilter(request, response);前后代码的调用顺序

四:注意

1、当有多个过滤的url-pattern一样时,过滤器的先后和配置的先后保持一致
FilterChain:过滤器链,当有多个过滤器配置了相同的url-pattern时,每个过滤器就像链子一样串联起来,如MyFilter–>MyFilter2—>MyFilterN,
当执行完第一个过滤器的时候通过调用chain.doFilter(request, response); 来执行过滤器链中的下一个过滤器的doFilter方法

五:Filter的应用场景

  • 通过控制对chain.doFilter的方法的是否调用,来决定是否需要访问目标资源。
    比如,判断用户是否有访问该些资源的权限,有权限就调用chain.doFilter方法,没权限就不调用chain.doFilter方法。
  • 通过在调用chain.doFilter方法之前,做些处理来达到某些目的。
    比如,解决中文乱码的问题、过滤敏感词、解决Ajax跨域请求等等。可以在doFilter方法调用前,执行设置请求编码与响应的编码。甚至可以对request接口进行封装装饰来处理get请求方式的中文乱码问题(重写相应的request.getParameter方法)。
  • 通过在调用chain.doFilter方法之后,做些处理来达到某些目的。
    比如对整个web网站进行压缩。在调用chain.doFilter方法之前用类A对response对象进行封装装饰,重写getOutputStream和重写getWriter方法。在类A内部中,将输出内容缓存进ByteArrayOutputStream流中,然后在chain.doFilter方法执行后,获取类A中ByteArrayOutputStream流缓存数据,用GZIPOutputStream流进行压缩下。
    对响应的.html/.js/.css 进行压缩 参考javascript:void(0)

六:Filter部署应用注意事项

  • filter-mapping标签中servlet-name与url-pattern。
    Filter不仅可以通过url-pattern来指定拦截哪些url匹配的资源。而且还可以通过servlet-name来指定拦截哪个指定的servlet(专门为某个servlet服务了,servlet-name对应Servlet的相关配置)。
  • filter-mapping标签中dispatcher。
    指定过滤器所拦截的资源被 Servlet 容器调用的方式,可以是REQUEST,INCLUDE,FORWARD和ERROR之一,默认REQUEST。用户可以设置多个 子元素用来指定 Filter 对资源的多种调用方式进行拦截。
  • REQUEST:
    当用户直接访问页面时,Web容器将会调用过滤器。如果目标资源是通过RequestDispatcher的include()或forward()方法访问或ERROR情况时,那么该过滤器就不会被调用。
  • INCLUDE:
    如果目标资源是通过RequestDispatcher的include()方法访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。
  • FORWARD:
    如果目标资源是通过RequestDispatcher的forward()方法访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用。
  • ERROR:

如若在A.jsp页面page指令中指定了error属性=examError.jsp,那么A.jsp中若出现了异常,会跳转到examError.jsp中处理。而在跳转到examError.jsp时,若过滤器配置了ERROR的dispather那么则会拦截,否则不会拦截。


第二部分:SpringMVC Interceptor拦截器


SpringMVC 中的Interceptor拦截请求是通过HandlerInterceptor 来实现的。
在SpringMVC 中定义一个Interceptor主要有两种方式,
第一种方式是要实现HandlerInterceptor接口,或者是继承实现了HandlerInterceptor接口的类,比如Spring已经提供的实现了HandlerInterceptor接口的抽象类HandlerInterceptorAdapter
第二种方式是实现WebRequestInterceptor接口,或者是继承实现了WebRequestInterceptor的类。

1、实现HandlerInterceptor接口

HandlerInterceptor接口包含三个方法:preHandle、postHandle、afterCompletion

public interface HandlerInterceptor {

    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception;


    void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception;

    void afterCompletion(
            HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception;
}

接口方法简介

  • public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handle)
    该方法将在请求处理之前进行调用。SpringMVC中的Interceptor是链式的调用的,在一个应用中或者说是在一个请求中可以同时存在多个Interceptor 。
    每个Interceptor的调用会依据它的声明顺序依次执行,而且最先执行的都是Interceptor中的preHandle方法,所以可以在这个方法中进行一些前置初始化操作或者是对当前请求的一个预处理,也可以在这个方法中进行一些判断来决定请求是否要继续进行下去。
    该方法的返回值是布尔值Boolean类型的,当它返回为false 时,表示请求结束,后续的Interceptor和Controller都不会再执行;
    当返回值为true时就会继续调用下一个Interceptor的preHandle方法,如果已经是最后一个Interceptor的时候就会是调用当前请求的Controller方法。
  • postHandle(HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView)
    由preHandle方法的解释我们知道这个方法包括后面要说到的afterCompletion方法都只能是在当前所属的Interceptor的preHandle方法的返回值为true时才能被调用。
    postHandle方法,顾名思义就是在当前请求进行处理之后,也就是Controller方法调用之后执行,
    但是它会在DispatcherServlet进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller处理之后的ModelAndView对象进行操作。
    postHandle方法被调用的方向跟preHandle是相反的,也就是说先声明的Interceptor 的postHandle方法反而会后执行,这和Struts2里面的Interceptor 的执行过程有点类型。Struts2 里面的Interceptor 的执行过程也是链式的,只是在Struts2 里面需要手动调用ActionInvocation 的invoke 方法来触发对下一个Interceptor 或者是Action 的调用,然后每一个Interceptor 中在invoke 方法调用之前的内容都是按照声明顺序执行的,而invoke 方法之后的内容就是反向的。
  • afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)
    该方法也是需要当前对应的Interceptor 的preHandle 方法的返回值为true 时才会执行。
    顾名思义,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。
    这个方法的主要作用是用于进行资源清理工作的。

演示示例

LoginInterceptor

package com.mengdee.manager.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class LoginInterceptor implements HandlerInterceptor{

    @Override
    public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2) throws Exception {
        System.out.println("LoginInterceptor:preHandle");
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
            throws Exception {
        System.out.println("LoginInterceptor:afterCompletion");
    }

    @Override
    public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3)
            throws Exception {
        System.out.println("LoginInterceptor:postHandle");
    }
}

在spring配置文件中配置拦截器

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd 
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context.xsd 
       http://www.springframework.org/schema/mvc 
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/user/**"/>
            <mvc:exclude-mapping path="/public/**"/>
            <bean class="com.mengdee.manager.interceptor.LoginInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>

</beans>

配置拦截器,每个mvc:interceptors可以包含多个mvc:interceptor或者bean 节点
如果直接在interceptors节点下配置bean表示要拦截所有请求即/*
如果在根节点interceptors配置interceptor子节点,需要配置拦截器bean和要拦截的路径模式mvc:mapping,也可以配置要排除拦截的路径exclude-mapping(该配置是可选的)

访问http://localhost:8081/Spring-Mybatis-Druid/user/list

打印结果:

LoginInterceptor:preHandle

UserController:getUserList

LoginInterceptor:postHandle

LoginInterceptor:afterCompletion


2、实现 WebRequestInterceptor接口

public interface WebRequestInterceptor {

    void preHandle(WebRequest request) throws Exception;

    void postHandle(WebRequest request, ModelMap model) throws Exception;

    void afterCompletion(WebRequest request, Exception ex) throws Exception;
}

WebRequestInterceptor 中也定义了三个方法,我们也是通过这三个方法来实现拦截的。这三个方法都传递了同一个参数WebRequest ,
这个WebRequest 是Spring定义的一个接口,它里面的方法定义都基本跟HttpServletRequest 一样,
在WebRequestInterceptor 中对WebRequest 进行的所有操作都将同步到HttpServletRequest 中,然后在当前请求中一直传递。

  • void preHandle(WebRequest request)
    该方法将在请求处理之前进行调用,也就是说会在Controller方法调用之前被调用。
    这个方法跟HandlerInterceptor中的preHandle 是不同的,主要区别在于该方法的返回值是void ,也就是没有返回值,所以我们一般主要用它来进行资源的准备工作
    比如我们在使用Hibernate的时候可以在这个方法中准备一个Hibernate 的Session 对象,然后利用WebRequest 的void setAttribute(String name, Object value, int scope)把它放到WebRequest 的属性中。
  • WebRequest.SCOPE_REQUEST :它的值是0 ,代表只有在request 中可以访问。
  • WebRequest.SCOPE_SESSION :它的值是1 ,如果环境允许的话它代表的是一个局部的隔离的session,否则就代表普通的session,并且在该session范围内可以访问。
  • WebRequest.SCOPE_GLOBAL_SESSION :它的值是2 ,如果环境允许的话,它代表的是一个全局共享的session,否则就代表普通的session,并且在该session 范围内可以访问。
  • postHandle(WebRequest request, ModelMap model)
    该方法将在请求处理之后,也就是在Controller方法调用之后被调用,但是会在视图返回被渲染之前被调用
    所以可以在这个方法里面通过改变数据模型ModelMap 来改变数据的展示。
    该方法有两个参数,WebRequest 对象是用于传递整个请求数据的,比如在preHandle 中准备的数据都可以通过WebRequest 来传递和访问;
    ModelMap就是Controller 处理之后返回的Model 对象,我们可以通过改变它的属性来改变返回的Model模型。
  • afterCompletion(WebRequest request, Exception ex)
    该方法会在整个请求处理完成,也就是在视图返回并被渲染之后执行。
    所以在该方法中可以进行资源的释放操作。而WebRequest参数就可以把我们在preHandle中准备的资源传递到这里进行释放。
    Exception 参数表示的是当前请求的异常对象,如果在Controller中抛出的异常已经被Spring 的异常处理器给处理了的话,那么这个异常对象就是是null 。

应用场景:
权限验证,或者是来判断用户是否登陆,access_token的校验等

示例程序:

TokenInterceptor

package com.mengdee.manager.interceptor;

import org.springframework.ui.ModelMap;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.WebRequestInterceptor;

public class TokenInterceptor implements WebRequestInterceptor{

    // 在请求处理之前执行,该方法主要是用于准备资源数据的,然后可以把它们当做请求属性放到WebRequest中 
    @Override
    public void preHandle(WebRequest request) throws Exception {
        System.out.println("preHandle");

        //这个是放到request范围内的,所以只能在当前请求中的request中获取到  
        request.setAttribute("key1", "request", WebRequest.SCOPE_REQUEST);

        //这个是放到session范围内的,如果环境允许的话它只能在局部的隔离的会话中访问,否则就是在普通的当前会话中可以访问  
        request.setAttribute("key2", "session", WebRequest.SCOPE_SESSION);

        //如果环境允许的话,它能在全局共享的会话中访问,否则就是在普通的当前会话中访问
        request.setAttribute("key3", "globalSession", WebRequest.SCOPE_GLOBAL_SESSION);  

    }

    /** 
     * 该方法将在Controller执行之后,返回视图之前执行,ModelMap表示请求Controller处理之后返回的Model对象,所以可以在 
     * 这个方法中修改ModelMap的属性,从而达到改变返回的模型的效果。 
     */  
    @Override
    public void postHandle(WebRequest request, ModelMap modelMap) throws Exception {
        System.out.println("postHandle");

        for (String key:modelMap.keySet()){
            System.out.println(key + "=" + modelMap.get(key));;  
        } 

        modelMap.put("key5", "value5");         
    }

    /** 
     * 该方法将在整个请求完成之后,也就是说在视图渲染之后进行调用,主要用于进行一些资源的释放 
     */
    @Override
    public void afterCompletion(WebRequest request, Exception ex) throws Exception {
        System.out.println("afterCompletion");
    }

}

在spring配置文件中配置拦截器

<mvc:interceptors>
    <!-- 单独配置一个bean的Interceptor,将拦截所有的请求 -->
    <bean class="com.mengdee.manager.interceptor.TokenInterceptor"/>
</mvc:interceptors>

访问http://localhost:8081/Spring-Mybatis-Druid/user/index 打印日志

preHandle
UserController:index
request-session-globalSession
postHandle
key4=value4
afterCompletion


第三部分: Servlet的过滤器与Spring拦截器之间的关系与区别


Spring的拦截器与Servlet的Filter有相似之处,比如二者都是AOP编程思想的体现,都能实现权限检查、日志记录、请求地址参数检查等,不同的是

  • 使用范围不同
    Filter是Servlet中定义的,所以只能用于Web程序中;
    拦截器Spring定义的,既可以用于Web程序中,也可以用于其他程序中(如Application、Swing)
  • 使用范围不同
    Filter是Servlet中定义的,需要依赖于Servlet容器支持;
    拦截器是Spring中定义的,只需要Spring框架的支持即可
  • 使用的资源不同
    因拦截器是Spring中的一个组件,配置在Spring的上下文配置文件中,因此能与Spring里的任何资源、对象(例如Service层、数据源、事务管理等)通过IoC注入到拦截器中,能与Spring其他组件完美结合
    过滤器只能配置在web.xml中,无法获得更多的Spring的支持
  • 使用的深度不同
    Filter只在Servlet前后起作用;即 chain.doFilter(request, response)方法调用的前后
    拦截器能够深入到方法的前后(preHandle、postHandle)、异常抛出前后(如:org.springframework.web.bind.annotation.ExceptionHandler)等,因此拦截器具有更大的弹性
  • 关于初始化
    Filter 在容器启动时执行init方法,而拦截器是没有初始化方法,只有在Controller方法调用前的preHandle方法

总结:在Spring框架的程序中要优先使用拦截器