03.07 SpringBoot實現過濾器、攔截器與切片

Q:使用過濾器、攔截器與切片實現每個請求耗時的統計,並比較三者的區別與聯繫

過濾器Filter

過濾器概念

Filter是J2E中來的,可以看做是Servlet的一種“加強版”,它主要用於對用戶請求進行預處理和後處理,擁有一個典型的處理鏈。Filter也可以對用戶請求生成響應,這一點與Servlet相同,但實際上很少會使用Filter向用戶請求生成響應。

使用Filter完整的流程是:Filter對用戶請求進行預處理,接著將請求交給Servlet進行預處理並生成響應,最後Filter再對服務器響應進行後處理。

過濾器作用

在JavaDoc中給出了幾種過濾器的作用

Examples that have been identified for this design are

1) Authentication Filters, 即用戶訪問權限過濾

2) Logging and Auditing Filters, 日誌過濾,可以記錄特殊用戶的特殊請求的記錄等

3) Image conversion Filters

4) Data compression Filters

5) Encryption Filters

6) Tokenizing Filters

7) Filters that trigger resource access events

8) XSL/T filters

9) Mime-type chain Filter

對於第一條,即使用Filter作權限過濾,其可以這麼實現:定義一個Filter,獲取每個客戶端發起的請求URL,與當前用戶無權限訪問的URL列表(可以是從DB中取出)作對比,起到權限過濾的作用。

過濾器實現方式

自定義的過濾器都必須實現javax.Servlet.Filter接口,並重寫接口中定義的三個方法:

1.void init(FilterConfig config)

用於完成Filter的初始化。

2.void destory()

用於Filter銷燬前,完成某些資源的回收。

3.void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)

實現過濾功能,即對每個請求及響應增加的額外的預處理和後處理。,執行該方法之前,即對用戶請求進行預處理;執行該方法之後,即對服務器響應進行後處理。

值得注意的是,chain.doFilter()方法執行之前為預處理階段,該方法執行結束即代表用戶的請求已經得到控制器處理。因此,如果在doFilter中忘記調用chain.doFilter()方法,則用戶的請求將得不到處理。

<code>import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

// 必須添加註解,springmvc通過web.xml配置
@Component
public class TimeFilter implements Filter {
    private static final Logger LOG = LoggerFactory.getLogger(TimeFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        LOG.info("初始化過濾器:{}", filterConfig.getFilterName());
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        LOG.info("start to doFilter");
        long startTime = System.currentTimeMillis();
        chain.doFilter(request, response);
        long endTime = System.currentTimeMillis();
        LOG.info("the request of {} consumes {}ms.", getUrlFrom(request), (endTime - startTime));
        LOG.info("end to doFilter");
    }

    @Override
    public void destroy() {
        LOG.info("銷燬過濾器");
    }

    private String getUrlFrom(ServletRequest servletRequest){
        if (servletRequest instanceof HttpServletRequest){
            return ((HttpServletRequest) servletRequest).getRequestURL().toString();
        }

        return "";
    }
}/<code>

從代碼中可看出,類Filter是在javax.servlet.*中,因此可以看出,過濾器的一個很大的侷限性在於,其不能夠知道當前用戶的請求是被哪個控制器(Controller)處理的,因為後者是spring框架中定義的。

在SpringBoot中註冊第三方過濾器

對於SpringMvc,可以通過在web.xml中註冊過濾器。但在SpringBoot中不存在web.xml,此時如果引用的某個jar包中的過濾器,且這個過濾器在實現時沒有使用@Component標識為Spring Bean,則這個過濾器將不會生效。

此時需要通過java代碼去註冊這個過濾器。以上面定義的TimeFilter為例,當去掉類註解@Component時,註冊方式為:

<code>@Configuration
public class WebConfig {
    /**
     * 註冊第三方過濾器
     * 功能與spring mvc中通過配置web.xml相同
     * @return
     */
    @Bean
    public FilterRegistrationBean thirdFilter(){
        ThirdPartFilter thirdPartFilter = new ThirdPartFilter();
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean() ;

        filterRegistrationBean.setFilter(thirdPartFilter);
        List<string> urls = new ArrayList<>();
        // 匹配所有請求路徑
        urls.add("/*");
        filterRegistrationBean.setUrlPatterns(urls);

        return filterRegistrationBean;
    }
}/<string>/<code>

相比使用@Component註解,這種配置方式有個優點,即可以自由配置攔截的URL。

攔截器Interceptor

攔截器概念

攔截器,在AOP(Aspect-Oriented Programming)中用於在某個方法或字段被訪問之前,進行攔截,然後在之前或之後加入某些操作。攔截是AOP的一種實現策略。

攔截器作用

  • 日誌記錄:記錄請求信息的日誌,以便進行信息監控、信息統計、計算PV(Page View)等
  • 權限檢查:如登錄檢測,進入處理器檢測檢測是否登錄
  • 性能監控:通過攔截器在進入處理器之前記錄開始時間,在處理完後記錄結束時間,從而得到該請求的處理時間。(反向代理,如apache也可以自動記錄);
  • 通用行為:讀取cookie得到用戶信息並將用戶對象放入請求,從而方便後續流程使用,還有如提取Locale、Theme信息等,只要是多個處理器都需要的即可使用攔截器實現。

攔截器實現

通過實現HandlerInterceptor接口,並重寫該接口的三個方法來實現攔截器的自定義:

1.preHandler(HttpServletRequest request, HttpServletResponse response, Object handler)

方法將在請求處理之前進行調用。SpringMVC中的Interceptor同Filter一樣都是鏈式調用。每個Interceptor的調用會依據它的聲明順序依次執行,而且最先執行的都是Interceptor中的preHandle方法,所以可以在這個方法中進行一些前置初始化操作或者是對當前請求的一個預處理,也可以在這個方法中進行一些判斷來決定請求是否要繼續進行下去。

該方法的返回值是布爾值Boolean 類型的,當它返回為false時,表示請求結束,後續的Interceptor和Controller都不會再執行;當返回值為true時就會繼續調用下一個Interceptor 的preHandle 方法,如果已經是最後一個Interceptor 的時候就會是調用當前請求的Controller 方法。

2.postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)

在當前請求進行處理之後,也就是Controller 方法調用之後執行,但是它會在DispatcherServlet 進行視圖返回渲染之前被調用,所以我們可以在這個方法中對Controller 處理之後的ModelAndView 對象進行操作。

3.afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)

該方法也是需要當前對應的Interceptor的preHandle方法的返回值為true時才會執行。顧名思義,該方法將在整個請求結束之後,也就是在DispatcherServlet 渲染了對應的視圖之後執行。這個方法的主要作用是用於進行資源清理工作的。

<code>@Component
public class TimeInterceptor implements HandlerInterceptor {
    private static final Logger LOG = LoggerFactory.getLogger(TimeInterceptor.class);
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        LOG.info("在請求處理之前進行調用(Controller方法調用之前)");
        request.setAttribute("startTime", System.currentTimeMillis());
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        LOG.info("controller object is {}", handlerMethod.getBean().getClass().getName());
        LOG.info("controller method is {}", handlerMethod.getMethod());

        // 需要返回true,否則請求不會被控制器處理
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        LOG.info("請求處理之後進行調用,但是在視圖被渲染之前(Controller方法調用之後),如果異常發生,則該方法不會被調用");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LOG.info("在整個請求結束之後被調用,也就是在DispatcherServlet 渲染了對應的視圖之後執行(主要是用於進行資源清理工作)");
        long startTime = (long) request.getAttribute("startTime");
        LOG.info("time consume is {}", System.currentTimeMillis() - startTime);
    }/<code>

與過濾器不同的是,攔截器使用@Component修飾後,在SpringBoot中還需要通過實現WebMvcConfigurer手動註冊:

<code>// java配置類
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private TimeInterceptor timeInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(timeInterceptor);
    }
}/<code>

如果是在SpringMVC中,則需要通過xml文件配置<interceptors>節點信息。/<interceptors>

切片Aspect

切片概述

相比過濾器,攔截器能夠知道用戶發出的請求最終被哪個控制器處理,但是攔截器還有一個明顯的不足,即不能夠獲取request的參數以及控制器處理之後的response。所以就有了切片的用武之地了。

切片實現

切片的實現需要注意@Aspect,@Component以及@Around這三個註解的使用,詳細查看官方文檔:

https://docs.spring.io/spring/docs/5.0.12.RELEASE/spring-framework-reference/core.html#aop

<code>@Aspect
@Component
public class TimeAspect {
    private static final Logger LOG = LoggerFactory.getLogger(TimeAspect.class);


    @Around("execution(* me.ifight.controller.*.*(..))")
    public Object handleControllerMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        LOG.info("切片開始。。。");
        long startTime = System.currentTimeMillis();

        // 獲取請求入參
        Object[] args = proceedingJoinPoint.getArgs();
        Arrays.stream(args).forEach(arg -> LOG.info("arg is {}", arg));

        // 獲取相應
        Object response = proceedingJoinPoint.proceed();

        long endTime = System.currentTimeMillis();
        LOG.info("請求:{}, 耗時{}ms", proceedingJoinPoint.getSignature(), (endTime - startTime));
        LOG.info("切片結束。。。");
        return null;
    }
}/<code>

過濾器、攔截器以及切片的調用順序

如下圖,展示了三者的調用順序Filter->Intercepto->Aspect->Controller。相反的是,當Controller拋出的異常的處理順序則是從內到外的。因此我們總是定義一個註解@ControllerAdvice去統一處理控制器拋出的異常。

如果一旦異常被@ControllerAdvice處理了,則調用攔截器的afterCompletion方法的參數Exception ex就為空了。

SpringBoot實現過濾器、攔截器與切片

實際執行的調用棧也說明了這一點:

SpringBoot實現過濾器、攔截器與切片

而對於過濾器和攔截器詳細的調用順序如下圖:

SpringBoot實現過濾器、攔截器與切片

過濾器和攔截器的區別

最後有必要再說說過濾器和攔截器二者之間的區別:

SpringBoot實現過濾器、攔截器與切片

除此之外,相比過濾器,攔截器能夠“看到”用戶的請求具體是被Spring框架的哪個控制器所處理。


分享到:


相關文章: