在工作中如何选择拦截机制去处理我们的业务请求,过滤器,拦截器,还是切面的选择一直比较模糊,今天花时间整理一下。
Filter(过滤器)
拦截所有客户端对Web资源(静态资源、动态资源)的访问。Filter是服务端的一个组件,是基于Servlet实现的从客户端访问服务端Web资源的一种拦截机制,对请求request和响应response都进行过滤,可以用来完成设置字符编码,鉴权操作等。
严格意义上讲Filter只是适用于web中,依赖于Servlet容器,利用Java的回调机制进行实现。使用时实现Filter接口,在Web.xml里配置对应的class还有mapping-url,SpringBoot工程可以通FilterRegisteration配置设置要过滤的URL, *两种配置方式过滤器都是有序的,谁在前就先调用谁!定义过滤器后会重写三个方法,分别是init(),doFilter(),和destory()。
-
init方法是过滤器的初始化方法,当web容器创建这个bean的时候就会执行,这个方法可以读取web.xml里面的参数。
-
doFilter方法是执行过滤的请求的核心,当客户端请求访问web资源时,这个时候我们可以拿到request里面的参数,对数据进行处理后,通过filterChain方法将请求将请求放行,放行后我们也可以通过response对响应进行处理(比如压缩响应),然后会传递到下一个过滤器。
-
destory方法是当web容器中的过滤器实例被销毁时会被执行,主要作用是释放资源。
Filter代码
//@Component
public class TimeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("过滤器初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("过滤器执行了");
long start2 = System.currentTimeMillis();
filterChain.doFilter(servletRequest, servletResponse);
long time = System.currentTimeMillis() - start2;
System.out.println("过滤器执行的时间是 :" + time);
System.out.println("过滤器执行结束");
}
@Override
public void destroy() {
System.out.println("过滤器销毁了");
}
}</pre>
Web.xml配置
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<servlet-name>/*</servlet-name>
</filter-mapping>
SpringBoot工程可以通过加@Component注解添加进Spring管理,也可以通过下面注册的方式去执行,推荐用下方的方式:
@Bean
public FilterRegistrationBean timeFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
TimeFilter filter = new TimeFilter();
filterRegistrationBean.setFilter(filter);
filterRegistrationBean.addUrlPatterns("/user","/users");
return filterRegistrationBean;
}
如下图所示过滤器执行流程和生命周期。
执行流程
![img](https://jsnds.oss-cn-shanghai.aliyuncs.com/uploads/202208080007036.webp!c25)
生命周期
![img](https://jsnds.oss-cn-shanghai.aliyuncs.com/uploads/202208080017071.webp!c25)
Interceptor(拦截器)
拦截以 .action结尾的url拦截Action的访问。过滤器依赖serverlet容器,获取request和response处理,是基于函数回调,简单说就是“去取你想取的”;Interfactor是基于Java的反射机制(AOP思想)进行实现,不依赖Servlet容器。如下图所示拦截器执行过程:
拦截器可以在方法执行之前(preHandle)和方法执行之后(afterCompletion)进行操作。回调操作(postHandle)可以获取执行的方法的名称,请求(HttpServletRequest)
Interceptor,可以控制请求的控制器和方法,但控制不了请求方法里的参数(只能获取参数的名称,不能获取到参数的值,用于处理页面提交的请求响应并进行处理,例如做国际化,做主题更换,过滤等)。如下代码所示:
@Component
public class FirstHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object hanlder) {
out.println("拦截器.preHandle 开始执行。。。");
out.println(hanlder.getClass().getSimpleName());
out.println(((HandlerMethod) hanlder).getBean().getClass().getName());
httpServletRequest.setAttribute("start", currentTimeMillis());
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object hanlder, ModelAndView modelAndView) {
out.println("拦截器.postHandle 开始执行。。。");
long start = (long) httpServletRequest.getAttribute("start");
out.println("postHandle执行时间为:" + (currentTimeMillis() - start));
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object hanlder, Exception e) {
//会打印两次 spring里面的basic error 也会被拦截
out.println("拦截器.afterCompletion 开始执行。。。");
long start = (long) httpServletRequest.getAttribute("start");
out.println("afterCompletion执行时间为:" + (currentTimeMillis() - start));
out.println("\n ex is :" + e+"\n");
}
}
再来看看拦截器再spring boot里面的配置:
@Configuration
public class InterceptorConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new FirstHandlerInterceptor()).addPathPatterns("/**").order(0);
}
}
HandlerInterceptor三个方法:preHandle、postHandle和afterCompletion的执行时间点如下:
preHandle:在HandlerMapping确定使用哪个Handler处理请求之后,HandlerAdapter调用Handler之前,在该阶段HandlerInterceptor可以修改Request和Response。
postHandle:在HandlerAdapter调用Handler之后,DispatcherServlet渲染视图之前。实际上在调用postHandler之前HandlerAdapter已经完成Response写并提交,因此postHandler无法修改Response。如果有修改Response的场景,可以使用ResponseBodyAdvice接口。
afterCompletion:请求处理完成之后调用,适合做一些资源清理工作。
与业务相关的细粒度任务适合首选HandlerInterceptor,下面是一些可以选择使用HandlerInterceptor的场景:
- 鉴权,可以使用HandlerInterceptor但是Filter是更佳的选择。
- 审计日志,记录每一个请求。
- Token解析或校验。
- Handler执行时间统计。
如果在没有显示的指定Interceptor的顺序时,Interceptor的执行顺序将以registry时的顺序执行。可以通过.order()方法指定Interceptor的执行顺序,如下所示:
@Configuration
public class InterceptorConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new FirstHandlerInterceptor()).addPathPatterns("/**").order(0);
registry.addInterceptor(new SecondHandlerInterceptor()).addPathPatterns("/**").order(1);
}
}
如下图所示多个Interceptor的执行顺序说明,如下:
总结一下Filter和Interceptor的执行顺序说明,如下:
Filter与Interceptor区别
-
拦截器是基于java的反射机制,使用代理模式,而过滤器是基于函数回调。
-
拦截器不依赖servlet容器,过滤器依赖于servlet容器。
-
拦截器只能对action起作用,而过滤器可以对几乎所有的请求起作用(可以保护资源)。
-
拦截器可以访问action上下文,堆栈里面的对象,而过滤器不可以。
-
执行顺序:过滤前-拦截前-Action处理-拦截后-过滤后。
作用域不同
- 过滤器依赖于servlet容器,只能在 servlet容器,web环境下使用
- 拦截器依赖于spring容器,可以在spring容器中调用,不管此时Spring处于什么环境
细粒度的不同
- 过滤器的控制比较粗,只能在请求进来时进行处理,对请求和响应进行包装
- 拦截器提供更精细的控制,可以在controller对请求处理之前或之后被调用,也可以在渲染视图呈现给用户之后调用
中断链执行的难易程度不同
- 拦截器可以 preHandle方法内返回 false 进行中断
- 过滤器就比较复杂,需要处理请求和响应对象来引发中断,需要额外的动作,比如将用户重定向到错误页面
Spring AOP
在使用Filter的时能获取request和response对象,对请求和响应进行处理。使用Interfactor时我们可以通过handler来获取当前请求控制器的方法名称。但是这两者都有一个弊端,我们拿不到控制器要接收的参数,先看下servlet源码的执行顺序,如下:
从DispatherServlet分发请求时,进入doService()方法内部,在方法参数封装之前,添加了判断,applyPreHandle()方法就时判断拦截器里面的preHandler()方法,根据返回的true或者false,判断是否执行真正的handler,所以我们在拦截器的handler参数里面是获取不到请求的参数的,因此,我们要引入Spring AOP,也就是切片编程,它可以在控制器的执行之前,执行之后,抛出异常等等,进行控制!
Spring声明式事务管理(切面)
AOP操作可以对Spring管理的Bean的访问(业务层Controller、Service等)进行横向的拦截,最大的优势在于它可以获取执行方法的参数(ProceedingJoinPoint.getArgs() ),并对方法进行统一的处理。常用于日志记录、事务管理、请求参数安全验证等。
如下是SpringAOP提供的常用注解:
@Aspect:作用是把当前类标识为一个切面供容器读取
@Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为 此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。
@Around:环绕增强,相当于MethodInterceptor
@AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行
@Before:标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有
@AfterThrowing:异常抛出增强,相当于ThrowsAdvice
@After: final增强,不管是抛出异常或者正常退出都会执行
如下是一个SpringAOP参考案例:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAspect {
@Pointcut("execution(public * com.zq..controller..*Controller.*(..) ) && @annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void pointcut() {
}
@Around("pointcut()")
public Object function(ProceedingJoinPoint joinPoint) throws Throwable {
// do something
// 获取增强的类
// 获取增强的方法名
// 获取增强方法的参数
// 继续执行被拦截方法
return joinPoint.proceed();
}
}
总结
filter和interceptor和aspect对比如下:
- | filter | interceptor | aspect |
---|---|---|---|
入参 | ServletRequest, ServletResponse | HttpServletRequest , HttpServletResponse ,Object handler | ProceedingJoinPoint |
原理 | 依赖于servlet容器,与框架无关 | Spring框架拦截器,基于Java反射机制 | 动态代理(jdk动态代理/cglib) |
范围 | RESTful api | RESTful api | Spring Bean |
filter和interceptor和aspect执行顺序如下:
三者功能类似,但各有优势,从过滤器–》拦截器–》切面,拦截规则越来越细致,执行顺序依次是过滤器、拦截器、切面。
一般情况下数据被过滤的时机越早对服务的性能影响越小,因此我们在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是aop。
比如权限校验,一般情况下所有的请求都需要做登录校验,此时就应该使用过滤器在最顶层做校验;日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。
完!
参考资料:
https://www.jianshu.com/p/2ec6a5f24a33
https://www.jianshu.com/p/10c468cfb671
https://blog.csdn.net/weixin_48052161/article/details/111025970
https://www.jianshu.com/p/a7f5707b72a4