實戰Spring Boot 2.0之過濾器和攔截器

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

正文

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 類似,區別就是 攔截器 只能對 Controller 的 HTTP 請求進行攔截。

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-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>

原理剖析

我們通過實現 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()

小結

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

發佈了64 篇原創文章 · 獲贊 118 · 訪問量 42萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章