Spring过滤器和拦截器执行流程

1. 几种组件介绍

dangdangdang.jpg

1.1. 监听器Listener

Listener 可以监听 web 服务器中某一个 事件操作,并触发注册的 回调函数。通俗的语言就是在 application,session,request 三个对象 创建/消亡 或者 增删改 属性时,自动执行代码的功能组件。

1.2. Servlet

Servlet 是一种运行 服务器端 的 java 应用程序,具有 独立于平台和协议 的特性,并且可以动态的生成 web 页面,它工作在 客户端请求 与 服务器响应 的中间层。

1.3. 过滤器Filter

Filter 对 用户请求 进行 预处理,接着将请求交给 Servlet 进行 处理 并 生成响应,最后 Filter 再对 服务器响应 进行 后处理。Filter 是可以复用的代码片段,常用来转换 HTTP 请求、响应 和 头信息。Filter 不像 Servlet,它不能产生 响应,而是只 修改 对某一资源的 请求 或者 响应。

1.4. 拦截器Interceptor

类似 面向切面编程 中的 切面 和 通知,我们通过 动态代理 对一个 service() 方法添加 通知 进行功能增强。比如说在方法执行前进行 初始化处理,在方法执行后进行 后置处理。拦截器 的思想和 AOP 类似,区别就是 拦截器 只能对 Controller 的 HTTP 请求进行拦截。

2. 过滤器 VS 拦截器

2.1. 两者的区别

Filter 是基于 函数回调的,而 Interceptor 则是基于 Java反射 和 动态代理。
Filter 依赖于 Servlet 容器,而 Interceptor 不依赖于 Servlet 容器。
Filter 对几乎 所有的请求 起作用,而 Interceptor 只对 Controller 对请求起作用。

2.2. 执行顺序

对于自定义 Servlet 对请求分发流程:

Filter 过滤请求处理;
Servlet 处理请求;
Filter 过滤响应处理。
对于自定义 Controller 的请求分发流程:

Filter 过滤请求处理;
Interceptor 拦截请求处理;
对应的 HandlerAdapter 处理请求;
Interceptor 拦截响应处理;
Interceptor 的最终处理;
Filter 过滤响应处理。

3. 环境准备

配置gradle依赖
利用 Spring Initializer 创建一个 gradle 项目 spring-boot-web-async-task,创建时添加相关依赖。得到的初始 build.gradle 如下:

buildscript {
    ext {
        springBootVersion = '2.0.3.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'io.ostenant.springboot.sample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

配置启动入口类
配置一个 Spring Boot 启动入口类,这里需要配置两个注解。

@ServletComponentScan: 允许 Spring Boot 扫描和装载当前 包路径 和 子路径 下配置的 Servlet。
@EnableWvc: 允许 Spring Boot 配置 Spring MVC 相关自定义的属性,比如:拦截器、资源处理器、消息转换器等。

@EnableWebMvc
@ServletComponentScan
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
4. 配置监听器Listener
配置一个 ServletContext 监听器,使用 @WebListener 标示即可。在 Servlet 容器 初始化 过程中,contextInitialized() 方法会被调用,在容器 销毁 时会调用 contextDestroyed()。

@WebListener
public class IndexServletContextListener implements ServletContextListener {
    private static final Logger LOGGER = LoggerFactory.getLogger(IndexServletContextListener.class);
    public static final String INITIAL_CONTENT = "Content created in servlet Context";

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        LOGGER.info("Start to initialize servlet context");
        ServletContext servletContext = sce.getServletContext();
        servletContext.setAttribute("content", INITIAL_CONTENT);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        LOGGER.info("Destroy servlet context");
    }
}

这里在容器初始化时,往 ServletContext 上下文设置了参数名称为 INITIAL_CONTENT,可以全局直接访问。

5. 配置Servlet

配置 IndexHttpServlet,重写 HttpServlet 的 doGet() 方法,直接输出 IndexHttpServlet 定义的 初始化参数 和在 IndexServletContextListener 设置的 ServletContext 上下文参数。

@WebServlet(name = "IndexHttpServlet",
        displayName = "indexHttpServlet",
        urlPatterns = {"/index/IndexHttpServlet"},
        initParams = {
                @WebInitParam(name = "createdBy", value = "Icarus"),
                @WebInitParam(name = "createdOn", value = "2018-06-20")
        }
)
public class IndexHttpServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.getWriter().println(format("Created by %s", getInitParameter("createdBy")));
        resp.getWriter().println(format("Created on %s", getInitParameter("createdOn")));
        resp.getWriter().println(format("Servlet context param: %s",
                req.getServletContext().getAttribute("content")));
    }
}

配置 @WebServlet 注解用于注册这个 Servlet,@WebServlet 注解的 各个参数 分别对应 web.xml 中的配置:

<servlet-mapping>  
    <servlet-name>IndexHttpServlet</servlet-name>
    <url-pattern>/index/IndexHttpServlet</url-pattern>
</servlet-mapping>
<servlet>  
    <servlet-name>IndexHttpServlet</servlet-name>  
    <servlet-class>io.ostenant.springboot.sample.servlet.IndexHttpServlet</servlet-class>
    <init-param>
        <param-name>createdBy</param-name>
        <param-value>Icarus</param-value>
    </init-param>
    <init-param>
        <param-name>createdOn</param-name>
        <param-value>2018-06-20</param-value>
    </init-param>
</servlet>

6. 配置过滤器Filter

一个 Servlet 请求可以经由多个 Filter 进行过滤,最终由 Servlet 处理并响应客户端。这里配置两个过滤器示例:

FirstIndexFilter.java

@WebFilter(filterName = "firstIndexFilter",
        displayName = "firstIndexFilter",
        urlPatterns = {"/index/*"},
        initParams = @WebInitParam(
                name = "firstIndexFilterInitParam",
                value = "io.ostenant.springboot.sample.filter.FirstIndexFilter")
)
public class FirstIndexFilter implements Filter {
    private static final Logger LOGGER = LoggerFactory.getLogger(FirstIndexFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        LOGGER.info("Register a new filter {}", filterConfig.getFilterName());
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        LOGGER.info("FirstIndexFilter pre filter the request");
        String filter = request.getParameter("filter1");
        if (isEmpty(filter)) {
            response.getWriter().println("Filtered by firstIndexFilter, " +
                    "please set request parameter \"filter1\"");
            return;
        }
        chain.doFilter(request, response);
        LOGGER.info("FirstIndexFilter post filter the response");
    }

    @Override
    public void destroy() {
        LOGGER.info("Destroy filter {}", getClass().getName());
    }
}

以上 @WebFilter 相关的配置属性,对应于 web.xml 的配置如下:

<filter-mapping>
    <filter-name>firstIndexFilter</filter-name>
    <filter-class>io.ostenant.springboot.sample.filter.FirstIndexFilter</filter-class>
    <url-pattern>/index/*</url-pattern>
    <init-param>
        <param-name>firstIndexFilterInitParam</param-name>
        <param-value>io.ostenant.springboot.sample.filter.FirstIndexFilter</param-value>
    </init-param>
</filter-mapping>

配置 FirstIndexFilter,使用 @WebFilter 注解进行标示。当 FirstIndexFilter 初始化时,会执行 init() 方法。每次请求路径匹配 urlPatterns 配置的路径时,就会进入 doFilter() 方法进行具体的 请求 和 响应过滤。

当 HTTP 请求携带 filter1 参数时,请求会被放行;否则,直接 过滤中断,结束请求处理。

SecondIndexFilter.java

@WebFilter(filterName = "secondIndexFilter",
        displayName = "secondIndexFilter",
        urlPatterns = {"/index/*"},
        initParams = @WebInitParam(
                name = "secondIndexFilterInitParam",
                value = "io.ostenant.springboot.sample.filter.SecondIndexFilter")
)
public class SecondIndexFilter implements Filter {
    private static final Logger LOGGER = LoggerFactory.getLogger(SecondIndexFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        LOGGER.info("Register a new filter {}", filterConfig.getFilterName());
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        LOGGER.info("SecondIndexFilter pre filter the request");
        String filter = request.getParameter("filter2");
        if (isEmpty(filter)) {
            response.getWriter().println("Filtered by firstIndexFilter, " +
                    "please set request parameter \"filter2\"");
            return;
        }
        chain.doFilter(request, response);
        LOGGER.info("SecondIndexFilter post filter the response");

    }

    @Override
    public void destroy() {
        LOGGER.info("Destroy filter {}", getClass().getName());
    }
}

以上 @WebFilter 相关的配置属性,对应于 web.xml 的配置如下:

<filter-mapping>
    <filter-name>secondIndexFilter</filter-name>
    <filter-class>io.ostenant.springboot.sample.filter.SecondIndexFilter</filter-class>
    <url-pattern>/index/*</url-pattern>
    <init-param>
        <param-name>secondIndexFilterInitParam</param-name>
        <param-value>io.ostenant.springboot.sample.filter.SecondIndexFilter</param-value>
    </init-param>
</filter-mapping>

配置 SecondIndexFilter,使用 @WebFilter 注解进行标示。当 SecondIndexFilter 初始化时,会执行 init() 方法。每次请求路径匹配 urlPatterns 配置的路径时,就会进入 doFilter() 方法进行具体的 请求 和 响应过滤。

当 HTTP 请求携带 filter2 参数时,请求会被放行;否则,直接 过滤中断,结束请求处理。

来看看 doFilter() 最核心的三个参数:

ServletRequest: 未到达 Servlet 的 HTTP 请求;
ServletResponse: 由 Servlet 处理并生成的 HTTP 响应;
FilterChain: 过滤器链 对象,可以按顺序注册多个 过滤器。
FilterChain.doFilter(request, response);
解释: 一个 过滤器链 对象可以按顺序注册多个 过滤器。符合当前过滤器过滤条件,即请求 过滤成功 直接放行,则交由下一个 过滤器 进行处理。所有请求过滤完成以后,由 IndexHttpServlet 处理并生成 响应,然后在 过滤器链 以相反的方向对 响应 进行后置过滤处理。
配置控制器Controller
配置 IndexController,用于测试 /index/IndexController 路径是否会被 Filter 过滤和 Interceptor 拦截,并验证两者的先后顺序。

@RestController
@RequestMapping("index")
public class IndexController {
    @GetMapping("IndexController")
    public String index() throws Exception {
        return "IndexController";
    }
}

7. 配置拦截器Interceptor

拦截器 Interceptor 只对 Handler 生效。Spring MVC 会为 Controller 中的每个 请求方法 实例化为一个 Handler对象,由 HandlerMapping 对象路由请求到具体的 Handler,然后由 HandlerAdapter 通过反射进行请求 处理 和 响应,这中间就穿插着 拦截处理。

编写拦截器
为了区分日志,下面同样对 IndexController 配置两个拦截器类:

FirstIndexInterceptor.java

public class FirstIndexInterceptor implements HandlerInterceptor {
    private static final Logger LOGGER = LoggerFactory.getLogger(FirstIndexInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        LOGGER.info("FirstIndexInterceptor pre intercepted the request");
        String interceptor = request.getParameter("interceptor1");
        if (isEmpty(interceptor)) {
            response.getWriter().println("Filtered by FirstIndexFilter, " +
                    "please set request parameter \"interceptor1\"");
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        LOGGER.info("FirstIndexInterceptor post intercepted the response");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LOGGER.info("FirstIndexInterceptor do something after request completed");
    }
}
SecondIndexInterceptor.java

public class SecondIndexInterceptor implements HandlerInterceptor {
    private static final Logger LOGGER = LoggerFactory.getLogger(SecondIndexInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        LOGGER.info("SecondIndexInterceptor pre intercepted the request");
        String interceptor = request.getParameter("interceptor2");
        if (isEmpty(interceptor)) {
            response.getWriter().println("Filtered by SecondIndexInterceptor, " +
                    "please set request parameter \"interceptor2\"");
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        LOGGER.info("SecondIndexInterceptor post intercepted the response");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LOGGER.info("SecondIndexInterceptor do something after request completed");
    }
}
配置拦截器
在 Spring Boot 中 配置拦截器 很简单,只需要实现 WebMvcConfigurer 接口,在 addInterceptors() 方法中通过 InterceptorRegistry 添加 拦截器 和 匹配路径 即可。

@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    private static final Logger LOGGER = LoggerFactory.getLogger(WebConfiguration.class);

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new FirstIndexInterceptor()).addPathPatterns("/index/**");
        registry.addInterceptor(new SecondIndexInterceptor()).addPathPatterns("/index/**");
        LOGGER.info("Register FirstIndexInterceptor and SecondIndexInterceptor onto InterceptorRegistry");
    }
}

对应的 Spring XML 配置方式如下:

<bean id="firstIndexInterceptor"
class="io.ostenant.springboot.sample.interceptor.FirstIndexInterceptor"></bean>
<bean id="secondIndexInterceptor"
class="io.ostenant.springboot.sample.interceptor.SecondIndexInterceptor"></bean>

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/index/**"/>
        <ref local="firstIndexInterceptor"/>
    </mvc:interceptor>
    <mvc:interceptor>
        <mvc:mapping path="/index/**"/>
        <ref local="secondIndexInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

全局拦截器

<!-- 配置全局mapping的拦截器 -->
<mvc:interceptors>
    <!-- 如果有多个拦截器,则按照顺序进行配置 -->
    <mvc:interceptor>
        <!-- /**表示所有URL和子URL路径 -->
        <mvc:mapping path="/**"/>
        <bean class="top.alanshelby.interceptor.LoginInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

原理剖析
我们通过实现 HandlerInterceptor 接口来开发一个 拦截器,来看看 HandlerInterceptor 接口的三个重要的方法:

preHandle(): 在 controller 接收请求、处理 request 之前执行,返回值为 boolean,返回值为 true 时接着执行 postHandle() 和 afterCompletion() 方法;如果返回 false 则 中断 执行。
postHandle(): 在 controller 处理请求之后, ModelAndView 处理前执行,可以对 响应结果 进行修改。
afterCompletion(): 在 DispatchServlet 对本次请求处理完成,即生成 ModelAndView 之后执行。
下面简单的看一下 Spring MVC 中心调度器 DispatcherServlet 的 doDispatch() 方法的原理,重点关注 拦截器 的以上三个方法的执行顺序。

doDispatch(): DispatchServlet 处理请求分发的核心方法。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    try {
        ModelAndView mv = null;
        Exception dispatchException = null;
        try {
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);
            // Determine handler for the current request.
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }
            // Determine handler adapter for the current request.
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
            // Process last-modified header, if supported by the handler.
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (logger.isDebugEnabled()) {
                    logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                }
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }

            // 1. 按从前往后的顺序调用各个拦截器preHandle()方法
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // 2. HandlerAdapter开始真正的请求处理并生产响应视图对象
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            applyDefaultViewName(processedRequest, mv);

            // 3. 按照从后往前的顺序依次调用各个拦截器的postHandle()方法
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        } catch (Exception ex) {
            dispatchException = ex;
        } catch (Throwable err) {
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    } catch (Exception ex) {
        // 4. 最终会调用拦截器的afterCompletion()方法
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    } catch (Throwable err) {
        // 4. 最终会调用拦截器的afterCompletion()方法
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                new NestedServletException("Handler processing failed", err));
    } finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        } else {
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}

上面注释的几个 HandlerExecutionChain 的方法: applyPreHandle()、applyPostHandle() 和 triggerAfterCompletion()。
小结

本文详细介绍了 Listener,Servlet,Filter,Controller 和 Interceptor 等 Web 多种组件的功能、方法、顺序、作用域和生命周期。给出了详细的示例代码,结合 源码 分析了流程,结合 测试 验证了结论。长篇大论,希望大家对 Servlet 组件和 Spring MVC 的框架组件有了更清晰的认识。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349