實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor

零壹技術棧發表於2018-06-23

前言

使用者認證授權、日誌記錄 MDC、編碼解碼、UA 檢查、多端對應等都需要通過 攔截請求 來進行處理。這時就需要 ServletFilterListenerInterceptor 這幾種元件。而把非 Spring Boot 專案轉換成 Spring Boot 專案,需要沿用以前的這些程式碼,所以有必要了解這它們的 用法生命週期

實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor

本系列文章

  1. 實戰Spring Boot 2.0系列(一) - 使用Gradle構建Docker映象
  2. 實戰Spring Boot 2.0系列(二) - 全域性異常處理和測試
  3. 實戰Spring Boot 2.0系列(三) - 使用@Async進行非同步呼叫詳解
  4. 實戰Spring Boot 2.0系列(四) - 使用WebAsyncTask處理非同步任務
  5. 實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor
  6. 實戰Spring Boot 2.0系列(六) - 單機定時任務的幾種實現

正文

1. 幾種元件介紹

1.1. 監聽器Listener

Listener 可以監聽 web 伺服器中某一個 事件操作,並觸發註冊的 回撥函式。通俗的語言就是在 applicationsessionrequest 三個物件 建立/消亡 或者 增刪改 屬性時,自動執行程式碼的功能元件。

1.2. Servlet

Servlet 是一種執行 伺服器端java 應用程式,具有 獨立於平臺和協議 的特性,並且可以動態的生成 web 頁面,它工作在 客戶端請求伺服器響應 的中間層。

1.3. 過濾器Filter

Filter使用者請求 進行 預處理,接著將請求交給 Servlet 進行 處理生成響應,最後 Filter 再對 伺服器響應 進行 後處理Filter 是可以複用的程式碼片段,常用來轉換 HTTP 請求響應頭資訊Filter 不像 Servlet,它不能產生 響應,而是隻 修改 對某一資源的 請求 或者 響應

1.4. 攔截器Interceptor

類似 面向切面程式設計 中的 切面通知,我們通過 動態代理 對一個 service() 方法新增 通知 進行功能增強。比如說在方法執行前進行 初始化處理,在方法執行後進行 後置處理攔截器 的思想和 AOP 類似,區別就是 攔截器 只能對 ControllerHTTP 請求進行攔截。

2. 過濾器 VS 攔截器

2.1. 兩者的區別

  1. Filter 是基於 函式回撥的,而 Interceptor 則是基於 Java 反射動態代理

  2. Filter 依賴於 Servlet 容器,而 Interceptor 不依賴於 Servlet 容器。

  3. Filter 對幾乎 所有的請求 起作用,而 Interceptor 只對 Controller 對請求起作用。

2.2. 執行順序

對於自定義 Servlet 對請求分發流程:

  1. Filter 過濾請求處理;
  2. Servlet 處理請求;
  3. Filter 過濾響應處理。

對於自定義 Controller 的請求分發流程:

  1. Filter 過濾請求處理;
  2. Interceptor 攔截請求處理;
  3. 對應的 HandlerAdapter 處理請求;
  4. Interceptor 攔截響應處理;
  5. Interceptor 的最終處理;
  6. Filter 過濾響應處理。

3. 環境準備

配置gradle依賴

利用 Spring Initializer 建立一個 gradle 專案 spring-boot-listener-servlet-filter-interceptor,建立時新增相關依賴。得到的初始 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,重寫 HttpServletdoGet() 方法,直接輸出 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: 未到達 ServletHTTP 請求;
  • 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>
複製程式碼

原理剖析

我們通過實現 HandlerInterceptor 介面來開發一個 攔截器,來看看 HandlerInterceptor 介面的三個重要的方法:

  • preHandle(): 在 controller 接收請求、處理 request 之前執行,返回值為 boolean,返回值為 true 時接著執行 postHandle()afterCompletion() 方法;如果返回 false中斷 執行。

  • postHandle(): 在 controller 處理請求之後, ModelAndView 處理前執行,可以對 響應結果 進行修改。

  • afterCompletion(): 在 DispatchServlet 對本次請求處理完成,即生成 ModelAndView 之後執行。

下面簡單的看一下 Spring MVC 中心排程器 DispatcherServletdoDispatch() 方法的原理,重點關注 攔截器 的以上三個方法的執行順序。

  • 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()

  • applyPreHandle(): 按 從前往後 的順序呼叫各個攔截器的 preHandle() 方法。任意一個 HandlerInterceptor 攔截返回 false ,則 preHandle() 返回 false,記錄攔截器的位置 interceptorIndex,然後中斷攔截處理,最終觸發 AfterCompletion() 方法並返回 false
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;
            }
            this.interceptorIndex = i;
        }
    }
    return true;
}
複製程式碼
  • applyPostHandle(): 按照 從後往前 的順序依次呼叫各個攔截器的 postHandle() 方法。只有當所有 HandlerInterceptorpreHandle() 方法返回 true 時,才有機會執行到 applyPostHandle() 方法。
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable 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);
        }
    }
}
複製程式碼
  • triggerAfterCompletion: triggerAfterCompletion() 只在 preHandle() 方法返回 false程式丟擲異常 時執行。在 preHandle() 方法中,通過 interceptorIndex 記錄了返回 false攔截器索引。一旦 applyPreHandle() 方法返回 false,則從當前返回 false 的攔截器 從後往前 的執行 afterCompletion() 方法。
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable 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);
            }
        }
    }
}
複製程式碼

8. 開始測試

生命週期測試

啟動 Spring Boot 應用程式,觀察啟動時的程式日誌,下面我按照 順序 來分析啟動過程中完成了哪些事情。

  • 註冊 Spring MVCdispatcherServlet 和自定義的 IndexHttpServlet
2018-06-23 09:39:55.400  INFO 12301 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Servlet dispatcherServlet mapped to [/]
2018-06-23 09:39:55.404  INFO 12301 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Servlet IndexHttpServlet mapped to [/index/IndexHttpServlet]
複製程式碼

注意: dispatcherServletload-up-onstartup1,會優先於其他 Servlet 進行載入。

  • 按照先後順序,將所有的過濾器 Filter 物件與路徑進行對映,其中 characterEncodingFilterSpring MVC 自帶的解決亂碼的 Filter
2018-06-23 09:39:55.408  INFO 12301 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-23 09:39:55.409  INFO 12301 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'firstIndexFilter' to urls: [/index/*]
2018-06-23 09:39:55.409  INFO 12301 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'secondIndexFilter' to urls: [/index/*]
複製程式碼
  • 初始化 IndexServletContextListener,並執行 contextInitialized() 方法進行上下文初始化操作。
2018-06-23 09:39:55.429  INFO 12301 --- [ost-startStop-1] i.o.s.s.l.IndexServletContextListener    : Start to initialize servlet context
複製程式碼
  • 依次執行 Filterinit() 方法進行初始化處理。
2018-06-23 09:39:55.432  INFO 12301 --- [ost-startStop-1] i.o.s.sample.filter.SecondIndexFilter     : Register a new filter secondIndexFilter
2018-06-23 09:39:55.434  INFO 12301 --- [ost-startStop-1] i.o.s.sample.filter.FirstIndexFilter      : Register a new filter firstIndexFilter
複製程式碼
  • 建立、初始化攔截器,並統一註冊到 InterceptorRegistry 上。
2018-06-23 09:39:55.502  INFO 13150 --- [           main] i.o.s.s.interceptor.WebConfiguration     : Register FirstIndexInterceptor and SecondIndexInterceptor onto InterceptorRegistry
複製程式碼
  • IndexController 進行處理,把 請求 URI處理方法 對映到 HandlerMapping 上並進行快取。
2018-06-23 09:39:55.541  INFO 12301 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/index/IndexController],methods=[GET]}" onto public java.lang.String io.ostenant.springboot.sample.controller.IndexController.index() throws java.lang.Exception
複製程式碼

關閉 Spring Boot 應用程式時,觀察輸出日誌如下:

2018-06-23 10:07:03.294  INFO 12301 --- [ost-startStop-2] i.o.s.sample.filter.FirstIndexFilter     : Destroy filter io.ostenant.springboot.sample.filter.SecondIndexFilter
2018-06-23 10:07:03.294  INFO 12301 --- [ost-startStop-2] i.o.s.sample.filter.FirstIndexFilter     : Destroy filter io.ostenant.springboot.sample.filter.FirstIndexFilter
2018-06-23 10:07:03.294  INFO 12301 --- [ost-startStop-2] i.o.s.s.l.IndexServletContextListener    : Destroy servlet context
複製程式碼

可以看到上面配置的過濾器的 destroy() 方法和 IndexServletContextListenercontextDestroyed() 方法都被呼叫了。

訪問控制測試

Servlet測試

訪問 http://localhost:8080/index/IndexHttpServlet,響應頁面內容如下:

實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor

訪問 http://localhost:8080/index/IndexHttpServlet?filter1=filter1,響應頁面內容如下:

實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor

訪問 http://localhost:8080/index/IndexHttpServlet?filter1=filter1&filter2=filter2,響應頁面內容如下:

實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor

觀察控制檯輸出日誌,驗證 過濾器 的過濾順序正確。

2018-06-23 10:19:47.944  INFO 13150 --- [nio-8080-exec-1] i.o.s.sample.filter.FirstIndexFilter     : FirstIndexFilter pre filter the request
2018-06-23 10:19:47.944  INFO 13150 --- [nio-8080-exec-1] i.o.s.sample.filter.SecondIndexFilter    : SecondIndexFilter pre filter the request
2018-06-23 10:19:47.944  INFO 13150 --- [nio-8080-exec-1] i.o.s.sample.filter.SecondIndexFilter    : SecondIndexFilter post filter the response
2018-06-23 10:19:47.944  INFO 13150 --- [nio-8080-exec-1] i.o.s.sample.filter.FirstIndexFilter     : FirstIndexFilter post filter the response
複製程式碼

結論: 自定義的 過濾器IndexHttpServlet 生效, 而 自定義 的攔截器生效。

controller測試

訪問 http://localhost:8080/index/IndexController,響應頁面內容如下:

實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor

訪問 http://localhost:8080/index/IndexController?filter1=filter1,響應頁面內容如下:

實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor

訪問 http://localhost:8080/index/IndexController?filter1=filter1&filter2=filter2,響應頁面內容如下:

實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor

訪問 http://localhost:8080/index/IndexController?filter1=filter1&filter2=filter2&interceptor1=interceptor1,響應頁面內容如下:

實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor

訪問 http://localhost:8080/index/IndexController?filter1=filter1&filter2=filter2&interceptor1=interceptor1&interceptor2=interceptor2,響應頁面內容如下:

實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor

2018-06-23 10:21:42.533  INFO 13150 --- [nio-8080-exec-4] i.o.s.sample.filter.FirstIndexFilter     : FirstIndexFilter pre filter the request
2018-06-23 10:21:42.533  INFO 13150 --- [nio-8080-exec-4] i.o.s.sample.filter.SecondIndexFilter    : SecondIndexFilter pre filter the request
2018-06-23 10:21:42.534  INFO 13150 --- [nio-8080-exec-4] i.o.s.s.i.FirstIndexInterceptor          : FirstIndexInterceptor pre intercepted the request
2018-06-23 10:21:42.534  INFO 13150 --- [nio-8080-exec-4] i.o.s.s.i.SecondIndexInterceptor         : SecondIndexInterceptor pre intercepted the request
2018-06-23 10:21:42.535  INFO 13150 --- [nio-8080-exec-4] i.o.s.s.i.SecondIndexInterceptor         : SecondIndexInterceptor post intercepted the response
2018-06-23 10:21:42.535  INFO 13150 --- [nio-8080-exec-4] i.o.s.s.i.FirstIndexInterceptor          : FirstIndexInterceptor post intercepted the response
2018-06-23 10:21:42.535  INFO 13150 --- [nio-8080-exec-4] i.o.s.s.i.SecondIndexInterceptor         : SecondIndexInterceptor do something after request completed
2018-06-23 10:21:42.535  INFO 13150 --- [nio-8080-exec-4] i.o.s.s.i.FirstIndexInterceptor          : FirstIndexInterceptor do something after request completed
2018-06-23 10:21:42.535  INFO 13150 --- [nio-8080-exec-4] i.o.s.sample.filter.SecondIndexFilter    : SecondIndexFilter post filter the response
2018-06-23 10:21:42.535  INFO 13150 --- [nio-8080-exec-4] i.o.s.sample.filter.FirstIndexFilter     : FirstIndexFilter post filter the response
複製程式碼

結論: 自定義的 過濾器攔截器控制器 Controller 生效。而 過濾器 的優先順序高於 攔截器

小結

本文詳細介紹了 ListenerServletFilterControllerInterceptorWeb 多種元件的功能、方法、順序、作用域和生命週期。給出了詳細的示例程式碼,結合 原始碼 分析了流程,結合 測試 驗證了結論。長篇大論,希望大家對 Servlet 元件和 Spring MVC 的框架元件有了更清晰的認識。


歡迎關注技術公眾號: 零壹技術棧

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章