整理自:https://mp.weixin.qq.com/s/OaMaT-QF20P65tKOjC5_oA
Q:使用過(guò)濾器秒梳、攔截器與切片實(shí)現(xiàn)每個(gè)請(qǐng)求耗時(shí)的統(tǒng)計(jì),并比較三者的區(qū)別與聯(lián)系
過(guò)濾器Filter
過(guò)濾器概念
Filter是J2E中來(lái)的剧防,可以看做是Servlet的一種“加強(qiáng)版”,它主要用于對(duì)用戶請(qǐng)求進(jìn)行預(yù)處理和后處理辫樱,擁有一個(gè)典型的處理鏈峭拘。Filter也可以對(duì)用戶請(qǐng)求生成響應(yīng),這一點(diǎn)與Servlet相同狮暑,但實(shí)際上很少會(huì)使用Filter向用戶請(qǐng)求生成響應(yīng)鸡挠。
使用Filter完整的流程是:Filter對(duì)用戶請(qǐng)求進(jìn)行預(yù)處理,接著將請(qǐng)求交給Servlet進(jìn)行預(yù)處理并生成響應(yīng)搬男,最后Filter再對(duì)服務(wù)器響應(yīng)進(jìn)行后處理拣展。
過(guò)濾器作用
在JavaDoc中給出了幾種過(guò)濾器的作用
- Examples that have been identified for this design are
- Authentication Filters, 即用戶訪問(wèn)權(quán)限過(guò)濾
- Logging and Auditing Filters, 日志過(guò)濾,可以記錄特殊用戶的特殊請(qǐng)求的記錄等
- Image conversion Filters
- Data compression Filters
- Encryption Filters
- Tokenizing Filters
- Filters that trigger resource access events
- XSL/T filters
- Mime-type chain Filter
對(duì)于第一條缔逛,即使用Filter作權(quán)限過(guò)濾备埃,其可以這么實(shí)現(xiàn):定義一個(gè)Filter,獲取每個(gè)客戶端發(fā)起的請(qǐng)求URL褐奴,與當(dāng)前用戶無(wú)權(quán)限訪問(wèn)的URL列表(可以是從DB中取出)作對(duì)比按脚,起到權(quán)限過(guò)濾的作用。
過(guò)濾器實(shí)現(xiàn)方式
自定義的過(guò)濾器都必須實(shí)現(xiàn)javax.Servlet.Filter接口敦冬,并重寫(xiě)接口中定義的三個(gè)方法:
1.void init(FilterConfig config)
用于完成Filter的初始化辅搬。
2.void destory()
用于Filter銷毀前,完成某些資源的回收脖旱。
3.void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
實(shí)現(xiàn)過(guò)濾功能堪遂,即對(duì)每個(gè)請(qǐng)求及響應(yīng)增加的額外的預(yù)處理和后處理。,執(zhí)行該方法之前萌庆,即對(duì)用戶請(qǐng)求進(jìn)行預(yù)處理溶褪;執(zhí)行該方法之后,即對(duì)服務(wù)器響應(yīng)進(jìn)行后處理践险。
值得注意的是猿妈,chain.doFilter()方法執(zhí)行之前為預(yù)處理階段,該方法執(zhí)行結(jié)束即代表用戶的請(qǐng)求已經(jīng)得到控制器處理捏境。因此于游,如果在doFilter中忘記調(diào)用chain.doFilter()方法,則用戶的請(qǐng)求將得不到處理垫言。
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通過(guò)web.xml配置@Componentpublic class TimeFilter implements Filter { private static final Logger LOG = LoggerFactory.getLogger(TimeFilter.class); @Override public void init(FilterConfig filterConfig) throws ServletException { LOG.info("初始化過(guò)濾器:{}", 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("銷毀過(guò)濾器"); } private String getUrlFrom(ServletRequest servletRequest){ if (servletRequest instanceof HttpServletRequest){ return ((HttpServletRequest) servletRequest).getRequestURL().toString(); } return ""; }}
從代碼中可看出,類Filter是在javax.servlet.*中筷频,因此可以看出蚌成,過(guò)濾器的一個(gè)很大的局限性在于前痘,其不能夠知道當(dāng)前用戶的請(qǐng)求是被哪個(gè)控制器(Controller)處理的,因?yàn)楹笳呤莝pring框架中定義的担忧。
在SpringBoot中注冊(cè)第三方過(guò)濾器
對(duì)于SpringMvc芹缔,可以通過(guò)在web.xml中注冊(cè)過(guò)濾器。但在SpringBoot中不存在web.xml瓶盛,此時(shí)如果引用的某個(gè)jar包中的過(guò)濾器最欠,且這個(gè)過(guò)濾器在實(shí)現(xiàn)時(shí)沒(méi)有使用@Component標(biāo)識(shí)為Spring Bean,則這個(gè)過(guò)濾器將不會(huì)生效惩猫。
此時(shí)需要通過(guò)java代碼去注冊(cè)這個(gè)過(guò)濾器芝硬。以上面定義的TimeFilter為例,當(dāng)去掉類注解@Component時(shí)轧房,注冊(cè)方式為:
@Configurationpublic
class WebConfig {
/**
* 注冊(cè)第三方過(guò)濾器
* 功能與spring mvc中通過(guò)配置web.xml相同
* @return
*/
@Bean
public FilterRegistrationBean thirdFilter(){
ThirdPartFilter thirdPartFilter = new ThirdPartFilter();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean() ;
filterRegistrationBean.setFilter(thirdPartFilter);
List<String > urls = new ArrayList<>();
// 匹配所有請(qǐng)求路徑
urls.add("/*");
filterRegistrationBean.setUrlPatterns(urls);
return filterRegistrationBean;
}}
相比使用@Component注解拌阴,這種配置方式有個(gè)優(yōu)點(diǎn),即可以自由配置攔截的URL奶镶。
攔截器Interceptor
攔截器概念
攔截器迟赃,在AOP(Aspect-Oriented Programming)中用于在某個(gè)方法或字段被訪問(wèn)之前,進(jìn)行攔截厂镇,然后在之前或之后加入某些操作纤壁。攔截是AOP的一種實(shí)現(xiàn)策略。
攔截器作用
日志記錄:記錄請(qǐng)求信息的日志剪撬,以便進(jìn)行信息監(jiān)控摄乒、信息統(tǒng)計(jì)、計(jì)算PV(Page View)等
權(quán)限檢查:如登錄檢測(cè)残黑,進(jìn)入處理器檢測(cè)檢測(cè)是否登錄
性能監(jiān)控:通過(guò)攔截器在進(jìn)入處理器之前記錄開(kāi)始時(shí)間,在處理完后記錄結(jié)束時(shí)間斋否,從而得到該請(qǐng)求的處理時(shí)間梨水。(反向代理,如apache也可以自動(dòng)記錄)茵臭;
通用行為:讀取cookie得到用戶信息并將用戶對(duì)象放入請(qǐng)求疫诽,從而方便后續(xù)流程使用,還有如提取Locale旦委、Theme信息等奇徒,只要是多個(gè)處理器都需要的即可使用攔截器實(shí)現(xiàn)。
攔截器實(shí)現(xiàn)
通過(guò)實(shí)現(xiàn)HandlerInterceptor接口缨硝,并重寫(xiě)該接口的三個(gè)方法來(lái)實(shí)現(xiàn)攔截器的自定義:
1.preHandler(HttpServletRequest request, HttpServletResponse response, Object handler)
方法將在請(qǐng)求處理之前進(jìn)行調(diào)用摩钙。SpringMVC中的Interceptor同F(xiàn)ilter一樣都是鏈?zhǔn)秸{(diào)用。每個(gè)Interceptor的調(diào)用會(huì)依據(jù)它的聲明順序依次執(zhí)行查辩,而且最先執(zhí)行的都是Interceptor中的preHandle方法胖笛,所以可以在這個(gè)方法中進(jìn)行一些前置初始化操作或者是對(duì)當(dāng)前請(qǐng)求的一個(gè)預(yù)處理网持,也可以在這個(gè)方法中進(jìn)行一些判斷來(lái)決定請(qǐng)求是否要繼續(xù)進(jìn)行下去。
該方法的返回值是布爾值Boolean 類型的长踊,當(dāng)它返回為false時(shí)功舀,表示請(qǐng)求結(jié)束,后續(xù)的Interceptor和Controller都不會(huì)再執(zhí)行身弊;當(dāng)返回值為true時(shí)就會(huì)繼續(xù)調(diào)用下一個(gè)Interceptor 的preHandle 方法辟汰,如果已經(jīng)是最后一個(gè)Interceptor 的時(shí)候就會(huì)是調(diào)用當(dāng)前請(qǐng)求的Controller 方法。
2.postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
在當(dāng)前請(qǐng)求進(jìn)行處理之后阱佛,也就是Controller 方法調(diào)用之后執(zhí)行帖汞,但是它會(huì)在DispatcherServlet 進(jìn)行視圖返回渲染之前被調(diào)用,所以我們可以在這個(gè)方法中對(duì)Controller 處理之后的ModelAndView 對(duì)象進(jìn)行操作瘫絮。
3.afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)
該方法也是需要當(dāng)前對(duì)應(yīng)的Interceptor的preHandle方法的返回值為true時(shí)才會(huì)執(zhí)行涨冀。顧名思義,該方法將在整個(gè)請(qǐng)求結(jié)束之后麦萤,也就是在DispatcherServlet 渲染了對(duì)應(yīng)的視圖之后執(zhí)行鹿鳖。這個(gè)方法的主要作用是用于進(jìn)行資源清理工作的。
@Componentpublic
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("在請(qǐng)求處理之前進(jìn)行調(diào)用(Controller方法調(diào)用之前)");
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壮莹,否則請(qǐng)求不會(huì)被控制器處理
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
LOG.info("請(qǐng)求處理之后進(jìn)行調(diào)用翅帜,但是在視圖被渲染之前(Controller方法調(diào)用之后),如果異常發(fā)生命满,則 該方法不會(huì)被調(diào)用");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
LOG.info("在整個(gè)請(qǐng)求結(jié)束之后被調(diào)用涝滴,也就是在DispatcherServlet 渲染了對(duì)應(yīng)的視圖之后執(zhí)行(主要是用于進(jìn)行資源清理工作)");
long startTime = (long) request.getAttribute("startTime");
LOG.info("time consume is {}", System.currentTimeMillis() - startTime);
}
與過(guò)濾器不同的是,攔截器使用@Component修飾后胶台,在SpringBoot中還需要通過(guò)實(shí)現(xiàn)WebMvcConfigurer手動(dòng)注冊(cè):
// java配置類@Configurationpublic
class WebConfig implements WebMvcConfigurer {
@Autowired
private TimeInterceptor timeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(timeInterceptor);
}}
如果是在SpringMVC中歼疮,則需要通過(guò)xml文件配置<mvc:interceptors>
節(jié)點(diǎn)信息。
切片Aspect
切片概述
相比過(guò)濾器诈唬,攔截器能夠知道用戶發(fā)出的請(qǐng)求最終被哪個(gè)控制器處理韩脏,但是攔截器還有一個(gè)明顯的不足,即不能夠獲取request的參數(shù)以及控制器處理之后的response铸磅。所以就有了切片的用武之地了赡矢。
切片實(shí)現(xiàn)
切片的實(shí)現(xiàn)需要注意@Aspect,@Component以及@Around這三個(gè)注解的使用,詳細(xì)查看官方文檔:
https://docs.spring.io/spring/docs/5.0.12.RELEASE/spring-framework-reference/core.html#aop
@Aspect
@Componentpublic
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("切片開(kāi)始阅仔。吹散。。");
long startTime = System.currentTimeMillis();
// 獲取請(qǐng)求入?yún)?
Object[] args = proceedingJoinPoint.getArgs();
Arrays.stream(args).forEach(arg -> LOG.info("arg is {}", arg));
// 獲取相應(yīng)
Object response = proceedingJoinPoint.proceed();
long endTime = System.currentTimeMillis();
LOG.info("請(qǐng)求:{}, 耗時(shí){}ms", proceedingJoinPoint.getSignature(), (endTime - startTime));
LOG.info("切片結(jié)束八酒。空民。。");
return null;
}}
過(guò)濾器丘跌、攔截器以及切片的調(diào)用順序
如下圖袭景,展示了三者的調(diào)用順序Filter->Intercepto->Aspect->Controller唁桩。相反的是,當(dāng)Controller拋出的異常的處理順序則是從內(nèi)到外的耸棒。因此我們總是定義一個(gè)注解@ControllerAdvice去統(tǒng)一處理控制器拋出的異常荒澡。
如果一旦異常被@ControllerAdvice處理了,則調(diào)用攔截器的afterCompletion方法的參數(shù)Exception ex就為空了与殃。
實(shí)際執(zhí)行的調(diào)用棧也說(shuō)明了這一點(diǎn):
而對(duì)于過(guò)濾器和攔截器詳細(xì)的調(diào)用順序如下圖:
過(guò)濾器和攔截器的區(qū)別
最后有必要再說(shuō)說(shuō)過(guò)濾器和攔截器二者之間的區(qū)別:
除此之外单山,相比過(guò)濾器,攔截器能夠“看到”用戶的請(qǐng)求具體是被Spring框架的哪個(gè)控制器所處理幅疼。