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

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

過濾器Filter

過濾器概念

Filter是J2E中來的,能夠看作是Servlet的一種「增強版」,它主要用於對用戶請求進行預處理和後處理,擁有一個典型的處理鏈。Filter也能夠對用戶請求生成響應,這一點與Servlet相同,但實際上不多會使用Filter向用戶請求生成響應。使用Filter完整的流程是:Filter對用戶請求進行預處理,接着將請求交給Servlet進行預處理並生成響應,最後Filter再對服務器響應進行後處理。java

過濾器做用

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

* 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中取出)做對比,起到權限過濾的做用。spring

過濾器實現方式

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

  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框架中定義的。bash

在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。cookie

攔截器Interceptor

攔截器概念

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

攔截器做用

  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修飾後,在SpringBoot中還須要經過實現WebMvcConfigurer手動註冊:

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

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

若是是在SpringMVC中,則須要經過xml文件配置<mvc:interceptors>節點信息。

切片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…

相關文章
相關標籤/搜索