Q:使用过滤器、拦截器与切片实现每个请求耗时的统计,并比较三者的区别与联系
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
接口,并重写接口中定义的三个方法:
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框架中定义的。
对于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。
拦截器,在AOP(Aspect-Oriented Programming)中用于在某个方法或字段被访问之前,进行拦截,然后在之前或之后加入某些操作。拦截是AOP的一种实现策略。
通过实现 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
修饰后,还需要通过实现 WebMvcConfigurer
手动注册:
// java配置类 @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private TimeInterceptor timeInterceptor; @Override public void addInterceptors(InterceptorRegistry registry){ registry.addInterceptor(timeInterceptor); } } 复制代码
相比过滤器,拦截器能够知道用户发出的请求最终被哪个控制器处理,但是拦截器还有一个明显的不足,即不能够获取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框架的哪个控制器所处理。