Q:使用過濾器、攔截器與切片實現每一個請求耗時的統計,並比較三者的區別與聯繫html
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
void init(FilterConfig config)
void destory()
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
對於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
攔截器,在AOP(Aspect-Oriented Programming)中用於在某個方法或字段被訪問以前,進行攔截,而後在以前或以後加入某些操做。攔截是AOP的一種實現策略。mvc
經過實現HandlerInterceptor
接口,並重寫該接口的三個方法來實現攔截器的自定義:框架
preHandler(HttpServletRequest request, HttpServletResponse response, Object handler)
Interceptor
同Filter同樣都是鏈式調用。每一個Interceptor的調用會依據它的聲明順序依次執行,並且最早執行的都是Interceptor中的preHandle方法,因此能夠在這個方法中進行一些前置初始化操做或者是對當前請求的一個預處理,也能夠在這個方法中進行一些判斷來決定請求是否要繼續進行下去。該方法的返回值是布爾值Boolean 類型的,當它返回爲false時,表示請求結束,後續的Interceptor和Controller都不會再執行;當返回值爲true時就會繼續調用下一個Interceptor 的preHandle 方法,若是已是最後一個Interceptor 的時候就會是調用當前請求的Controller 方法。postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)
@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>
節點信息。
相比過濾器,攔截器可以知道用戶發出的請求最終被哪一個控制器處理,可是攔截器還有一個明顯的不足,即不可以獲取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框架的哪一個控制器所處理。