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

七印miss發表於2019-02-17

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

過濾器Filter

過濾器概念

Filter是J2E中來的,可以看做是Servlet的一種“加強版”,它主要用於對使用者請求進行預處理和後處理,擁有一個典型的處理鏈。Filter也可以對使用者請求生成響應,這一點與Servlet相同,但實際上很少會使用Filter向使用者請求生成響應。使用Filter完整的流程是:Filter對使用者請求進行預處理,接著將請求交給Servlet進行預處理並生成響應,最後Filter再對伺服器響應進行後處理。

過濾器作用

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

 * Examples that have been identified for this design are<br>
 * 1) Authentication Filters, 即使用者訪問許可權過濾
 * 2) Logging and Auditing Filters, 日誌過濾,可以記錄特殊使用者的特殊請求的記錄等
 * 3) Image conversion Filters
 * 4) Data compression Filters <br>
 * 5) Encryption Filters <br>
 * 6) Tokenizing Filters <br>
 * 7) Filters that trigger resource access events <br>
 * 8) XSL/T filters <br>
 * 9) Mime-type chain Filter <br>
複製程式碼

對於第一條,即使用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()方法,則使用者的請求將得不到處理。
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 "";
    }
}
複製程式碼

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

在SpringBoot中註冊第三方過濾器

對於SpringMvc,可以通過在web.xml中註冊過濾器。但在SpringBoot中不存在web.xml,此時如果引用的某個jar包中的過濾器,且這個過濾器在實現時沒有使用@Component標識為Spring Bean,則這個過濾器將不會生效。此時需要通過java程式碼去註冊這個過濾器。以上面定義的TimeFilter為例,當去掉類註解@Component時,註冊方式為:

@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;
    }
}
複製程式碼

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

攔截器Interceptor

攔截器概念

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

攔截器作用

  1. 日誌記錄:記錄請求資訊的日誌,以便進行資訊監控、資訊統計、計算PV(Page View)等
  2. 許可權檢查:如登入檢測,進入處理器檢測檢測是否登入
  3. 效能監控:通過攔截器在進入處理器之前記錄開始時間,在處理完後記錄結束時間,從而得到該請求的處理時間。(反向代理,如apache也可以自動記錄);
  4. 通用行為:讀取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 渲染了對應的檢視之後執行。這個方法的主要作用是用於進行資源清理工作的。
@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);
    }
複製程式碼

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

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

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(timeInterceptor);
    }
}
複製程式碼

切片Aspect

切片概述

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

切片實現

切片的實現需要注意@Aspect,@Component以及@Around這三個註解的使用,詳細檢視官方文件:傳送門

@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;
    }
}
複製程式碼

過濾器、攔截器以及切片的呼叫順序

如下圖,展示了三者的呼叫順序Filter->Intercepto->Aspect->Controller。相反的是,當Controller丟擲的異常的處理順序則是從內到外的。因此我們總是定義一個註解@ControllerAdvice去統一處理控制器丟擲的異常。如果一旦異常被@ControllerAdvice處理了,則呼叫攔截器的afterCompletion方法的引數Exception ex就為空了。

過濾器、攔截器以及切片的呼叫順序
實際執行的呼叫棧也說明了這一點:
呼叫棧順序
而對於過濾器和攔截器詳細的呼叫順序如下圖:
過濾器和攔截器詳細的呼叫順序

過濾器和攔截器的區別

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

Filter Interceptor
實現方式 過濾器是基於函式回撥 基於Java的反射機制的
規範 Servlet規範 Spring規範
作用範圍 對幾乎所有的請求起作用 只對action請求起作用

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

參考

blog.csdn.net/xiaodanjava…

相關文章