SpringBoot - 拦截器(Interceptor)、过滤器(Filter)、监听器(Listener)及AOP
传统基于Servlet容器的程序中,我们可以使用过滤器和监听器,在Java 框架中还可以使用拦截器,而面向切面编程AOP更是作为Spring框架中的核心思想
1. 拦截器
应用场景:
- 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算 PV(Page View)等;
- 权限检查:登陆检测,进入处理器检测是否登陆,如果没有则直接返回登陆界面等;
- 性能监控:通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。(反向代理,如 Apache 也可以自动记录);
- 通用行为:读取 Cookie 得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取 Locale、Theme 信息等,只要是多个处理器都需要的即可使用拦截器实现。
1 创建拦截器
创建自定义拦截器并实现
org.springframework.web.servlet.HandlerInterceptor
接口或者继承org.springframework.web.servlet.handler.HandlerInterceptorAdapter
拦截器HandlerInterceptor,共有三个方法:
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
方法在请求处理之前被调用,该方法在 Interceptor 类中最先执行,用来进行一些前置初始化操作或是对当前请求做预处理,也可以进行一些判断来决定请求是否要继续进行下去;postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView)
方法在当前请求处理完成后,也就是Controller方法调用后执行,但是它会在 DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对 Controller 处理之后的 ModelAndView 对象进行操作;afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex)
方法需要在当前对应的 Interceptor 类的 preHandle 方法返回值为 true 时才会执行。顾名思义,该方法将在整个请求结束之后,也就是在 DispatcherServlet 渲染了对应的视图之后执行。此方法主要用来进行资源清理。如://结束后threadLocal.remove()
;
HandlerInterceptor
1 | public interface HandlerInterceptor { |
拦截器适配器HandlerInterceptorAdapter,HandlerInterceptorAdapter继承了抽象接口HandlerInterceptor,建议使用HandlerInterceptorAdapter,因为可以按需进行方法的覆盖。
1 | public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor { |
2. 配置拦截器
1 |
|
3. 拦截器执行流程
- 拦截器执行顺序是按照Spring配置文件中定义的顺序而定的,需要先注册定义的拦截器;
- 先按照顺序执行所有拦截器的preHandle方法,当遇到
return false
为止,如果第二个preHandle方法是return false
,则第三个及以后的拦截器都不会执行,如果return ture
,则会按照顺序加载完preHandle方法; - 再执行主方法(自己的Controller接口),若中间抛出异常,则跟
return false
效果一样,不会继续执行postHandle,只会倒序执行afterCompletion方法; - 在主方法执行完业务逻辑(页面还未渲染数据)时,按照倒序执行postHandle方法,若第三个拦截器的preHandle方法
return false
,则会执行第二个和第一个的postHandle方法和afterCompletion(postHandle都执行完才会执行这个方法,也就是页面渲染完数据后,执行after进行清理工作),postHandle和afterCompletion都是倒序执行。
2. 过滤器
应用场景:
- 数据压缩
- 记录日志
- 数据统计
- 数据格式转换
- 数据设置默认值
- 权限认证、黑白名单
- 数据加密/解密、签名校验
创建过滤器及配置
自定义的过滤器需要实现
javax.servlet.Filter
,Filter接口中有三个方法:init(FilterConfig filterConfig)
:过滤器初始化的被调用;doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
:在doFilter()方法中,chain.doFilter()前的一般是对request执行的过滤操作,chain.doFilter后面的代码一般是对response执行的操作,chain.doFiter()执行下一个过滤器或者业务处理器;destory()
:过滤器销毁的时候被调用。
Filter的工作原理:
Filter接口中有一个doFilter方法,当我们编写好Filter,并配置对哪个web资源进行拦截后,WEB服务器每次在调用web资源的service方法之前,
都会先调用一下filter的doFilter方法,在该方法内编写代码可达到如下目的:- 调用目标资源之前,让一段代码执行;
- 调用目标资源(即是否让用户访问web资源);
- 调用目标资源之后,让一段代码执行。
创建步骤:
方法一(推荐使用)
- 创建过滤器类实现 Filter 接口,并在类的上面添加 @WebFilter 注解;
- 过滤器类上添加 @Order() 注解来制定过滤器的顺序;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29/**
* 自定义过滤器(注解方式)
*/
public class myFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("过滤器初始化");
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 对request、response进行一些预处理
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
System.out.println("----调用service之前执行一段代码----");
filterChain.doFilter(request, response); // 执行目标资源,放行
System.out.println("----调用service之后执行一段代码----");
}
public void destroy() {
System.err.println("过滤器销毁");
}
}- SpringBoot 启动类上添加
@ServletComponentScan
注解。
1
2
3
4
5
6
7
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}方法二
- 创建过滤器类实现 Filter 接口;
1
2
3
4
5
6
7
8
9
10
11
12
13public class MyFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//执行
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {}
}- 创建过滤器配置类,里面创建一个过滤器注册 Bean,将之前创建的过滤器注册到其中。
1
2
3
4
5
6
7
8
9
10
11
public class FilterConfig {
public FilterRegistrationBean myFilter(){
FilterRegistrationBean<MyFilter> filterBean = new FilterRegistrationBean<>();
filterBean.setFilter(new MyFilter());
filterBean.setName("FilterController");
filterBean.addUrlPatterns("/*");
return filterBean;
}
}
总结:Filter接口定义在javax.servlet
包中,是Servlet规范定义的,作用于Request/Response
前后,被Servlet容器调用,当Filter被Spring管理后可以使用Spring容器资源。,在一个 web 应用中,可以开发编写多个 Filter,这些 Filter 组合起来称之为一个 Filter链。在用户发起请求后,请求信息会根据过滤器链中过滤器的顺序进入各个过滤器,每经过一层过滤器时,需要通过这些滤器验证逻辑通过后放行,才能进入下一个过滤器,直至到服务器获取资源。等到服务器获取资源成功后进行响应给过滤器,然后再倒序的方式,经过一层层过滤器,最后对用户进行响应。
3. 监听器
Servlet的监听器Listener,它是实现了javax.servlet.ServletContextListener
接口的服务器端程序,它也是随web应用的启动而启动,只初始化一次,随web应用的停止而销毁,Listener是Servlet规范中定义的一种特殊类。用于监听以下等等域对象的创建和销毁事件。
- ServletContext:对应application,实现接口ServletContextListener。在整个Web服务中只有一个,在Web服务关闭时销毁。可用于做数据缓存,例如结合redis,在Web服务创建时从数据库拉取数据到缓存服务器;
- HttpSession:对应session会话,实现接口HttpSessionListener。在会话起始时创建,一端关闭会话后销毁。可用作获取在线用户数量;
- ServletRequest:对应request,实现接口ServletRequestListener。request对象是客户发送请求时创建的,用于封装请求数据,请求处理完毕后销毁。可用作封装用户信息;
监听域对象的属性发生修改的事件。用于在事件发生前、发生后做一些必要的处理。Filter是Servlet技术中最实用的技术,Web开发人员通过Filter技术,对web服务器管理的所有web资源。过滤器是在请求进入tomcat容器后,但请求进入servlet之前进行预处理的。请求结束返回也是,是在servlet处理完后,返回给前端之前
应用场景:
- 统计在线人数和在线用户;
- 系统启动时加载初始化信息;
- 实现URL级别的权限访问控制、过滤敏感词汇、压缩响应信息等;
- 统计网站访问量;
- 记录用户访问路径。
3.1 监听器支持的事件类型
ApplicationFailedEvent:该事件为spring boot启动失败时的操作
可以通过ApplicationFailedEvent 获取Throwable实例对象获取异常信息并处理
ApplicationPreparedEvent:上下文context准备时触发
上下文context已经准备完毕 ,可以通过ApplicationPreparedEvent获取到ConfigurableApplicationContext实例对象。ConfigurableApplicationContext类继承ApplicationContext类,但需要注意这个时候spring容器中的bean还没有被完全的加载,因此如果通过ConfigurableApplicationContext获取bean会报错的。比如报错:
Exception in thread "main" java.lang.IllegalStateException: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@69b0fd6f has not been refreshed yet
获取到上下文之后,可以将其注入到其他类中,毕竟ConfigurableApplicationContext为引用类型
ApplicationReadyEvent:上下文已经准备完毕的时候触发
这个时候就可以通过ApplicationReadyEvent获取ConfigurableApplicationContext,然后通过ConfigurableApplicationContext 获取bean的信息
ApplicationStartedEvent:spring boot 启动监听类
可以在SpringApplication启动之前做一些手脚,比如修改SpringApplication实例对象中的属性值
SpringApplicationEvent:获取SpringApplication
ApplicationEnvironmentPreparedEvent:环境事先准备,spring boot中的环境已经准备完毕
可以通过ApplicationEnvironmentPreparedEvent获取到SpringApplication、ConfigurableEnvironment等等信息, 可以通过ConfigurableEnvironment实例对象来修改以及获取默认的环境信息。
3.2 监听器的配置使用
配置监听器步骤
自定义事件,一般是继承ApplicationEvent抽象类 ;
1
2
3
4
5
6
7
8public class MyEvent extends ApplicationEvent {
// 第一个参数是不可缺少的,后面的参数可以是任意多个(或零个,自定义)
public MyEvent(Object source, String info) {
super(source);
//内容是自定义的
System.out.println("事件已经发生了:" + info);
}
}定义事件监听器,一般是实现ApplicationListener的接口 ;
启动时候,需要把监听器加入到spring容器中 ;
发布事件,使用ApplicationEventPublisher.publishEvent 发布事件;
配置方法
纯Java方式:在启动入口中,通过addListeners增加添加到spring容器和发布事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class MyListener implements ApplicationListener<MyEvent> {
//一旦springboot 发布了MyEvent事件,就会触发执行此方法,方法内容自定义
public void onApplicationEvent(MyEvent myEvent) {
System.out.println("接受到事件============" + event.getClass());
System.out.println("触发执行了监听器");
}
}
public class Application {
public static void main(String[] args){
SpringApplication springApplication = new SpringApplication(Application.class);
springApplication.addListeners(new MyListener());
ConfigurableApplicationContext context = springApplication.run(args);
//发布事件
context.publishEvent(new MyEvent(new Object(), "hello"));
context.close();
// context.stop();
}
}注解方式:使用@Component 把监听器纳入到spring容器中管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyListener2 implements ApplicationListener<MyEvent> {
public void onApplicationEvent(MyEvent myEvent) {
System.out.println("接受到事件============" + event.getClass());
System.out.println("触发执行了监听器");
}
}
public class Application {
public static void main(String[] args){
SpringApplication springApplication = new SpringApplication(Application.class);
//用Component注入了监听器后,就不用再addListener了
ConfigurableApplicationContext context = springApplication.run(args);
context.publishEvent(new MyEvent(new Object(), "hello"));
context.close();
// context.stop();
}
}配置文件形式:在application.yml中配置context.listener.classes属性
1
2
3context:
listener:
classes: com.xxx.test.listener.MyListener1
2
3
4
5
6
7
8
9
10
11
public class Application {
public static void main(String[] args){
SpringApplication springApplication = new SpringApplication(Application.class);
//在配置文件中配置了监听器,就不用再addListener了
ConfigurableApplicationContext context = springApplication.run(args);
context.publishEvent(new MyEvent(new Object(), "hello"));
context.close();
// context.stop();
}
}使用@EventListener注解(EventListenerMethodProcessor),不用实现ApplicationListener接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyListener3 {
public void listenerMethod(MyEvent myEvent){
System.out.println("接受到事件============" + event.getClass());
System.out.println("触发执行了监听器");
}
}
public class Application {
public static void main(String[] args){
SpringApplication springApplication = new SpringApplication(Application.class);
//在配置文件中配置了监听器,就不用再addListener了
ConfigurableApplicationContext context = springApplication.run(args);
context.publishEvent(new MyEvent(new Object(), "hello"));
context.close();
// context.stop();
}
}
总结:如果改成将MyEvent
改成Object
,就表示任意参数,所有该参数事件,或者其子事件都可以接收到;第一二种方式都是启动时候,把监听器加入到spring容器中,ConfigurableApplicationContext
拿到这个对象再发布就可使用监听器了。第三四中最终也是吧监听器ConfigurableApplicationContext
,只不过实现有了2个特别的类,第二种和第四种方式要好一些,因为简单明了,代码可读性好,对于第二种,一看实现了ApplicationListener
接口就知道是个监听器,对于第四种,一看@EventListener
注解就知道是个监听器。
AOP
相比较于拦截器,Spring 的aop则功能更强大,可以进行日志记录,性能统计,安全控制,事务处理,异常处理,解决代码复用等等,将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改 变这些行为的时候不影响业务逻辑的代码。
AOP封装的更细致,需要单独引用 jar包,AOP只支持方法层的切入点,也就是说你只能在方法上面定义Pointcut。
1 | <dependency> |
一个切入点声明有两部分:一个包含一个名称和任何参数的签名,一个能精确地确定我们感兴趣的执行方法的切入点表达式。在aop的@Aspectj注解样式中,通过常规方法定义提供切入点签名,并使用@Pointcut注解指示切入点表达式(作为切入点签名的方法必须具有void返回类型)
在定义AOP的类时,不需要和前面拦截器一样麻烦了,只需要通过注解,底层实现逻辑都通过IOC框架实现好了,涉及到的注解如下:
@Aspect:将一个 java 类定义为切面类。
@Pointcut:定义一个切入点,可以是一个规则表达式,比如下例中某个 package 下的所有函数,也可以是一个注解等,以下为切入点指令符。
- execution:用于匹配方法执行连接点。这是使用SpringAOP时要使用的主要切入点指示符。
- within:特定类型中的连接点。
- this:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;
- args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;参数是给定类型的实例。
- @args:用于匹配当前执行的方法传入的参数持有指定注解的执行;传递的实际参数的运行时类型具有给定类型的注解。
- target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;目标对象(要代理的应用程序对象)是给定类型的实例。
- @target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;执行对象的类具有给定类型的注解。
- @within:用于匹配指定类型内的方法执行;与具有给定注解的类型中的联接点匹配。
- @Annotation:用于匹配当前执行方法持有指定注解的方法;在SpringAOP中执行的方法具有给定注解的连接点。
五种通知(Advice):
- @Before:前置通知,放在方法头上,在切入点开始处切入内容。
- @After:后置finally通知,放在方法头上,在切入点结尾处切入内容,无论是否有异常,都会执行,类似于finally。
- @AfterReturning:后置try通知,放在方法头上,使用returning来引用方法返回值,在切入点 return 内容之后处理逻辑,只有执行成功会执行,异常不会。
- @AfterThrowing:后置catch通知,放在方法头上,使用throwing来引用抛出的异常,用来处理当切入内容部分抛出异常之后的处理逻辑。
- @Around:环绕通知,放在方法头上,这个方法要决定真实的方法是否执行,而且必须有返回值,在切入点前后切入内容,并自己控制何时执行切入点自身的内容。原则上可以替代@Before和@After。
@Order(100):AOP 切面执行顺序, @Before 数值越小越先执行,@After 和 @AfterReturning 数值越大越先执行。
区别
它们的执行拦截顺序:ServletContextListener> Filter > Interception > AOP > 具体执行的方法 > AOP > @ControllerAdvice > Interception > Filter > ServletContextListener
根据实现原理可以分为两大类:
- Filter和Listener:依赖Servlet容器,基于函数回调实现。可以拦截所有请求,覆盖范围更广,但无法获取ioc容器中的bean;
- Interceptor和aop:依赖spring框架,基于java反射和动态代理实现。只能拦截controller的请求,可以获取ioc容器中的bean。
从 Filter -> Interceptor -> aop ,拦截的功能越来越细致、强大,尤其是Interceptor和aop可以更好的结合spring框架的上下文进行开发。但是拦截顺序也是越来越靠后,请求是先进入Servlet容器的,越早的过滤和拦截对系统性能的消耗越少。
Filter与Interceptor联系与区别:
- 拦截器是基于java的反射机制,使用代理模式,而过滤器是基于函数回调;
- 拦截器不依赖与servlet容器是SpringMVC自带的,过滤器依赖于Servlet容器;
- 拦截器只能对action起作用,而过滤器可以对几乎所有的请求起作用(可以保护资源);
- 拦截器可以访问controller上下文,堆栈里面的对象,而过滤器不可以。(拦截器的preHandle方法在进入controller前执行,而拦截器的postHandle方法在执行完controller业务流程后,在视图解析器解析ModelAndView之前执行,可以操控Controller的ModelAndView内容。而afterCompletion是在视图解析器解析渲染ModelAndView完成之后执行的),( 过滤器是在服务器启动时就会创建的,只会创建一个实例,常驻内存,也就是说服务器一启动就会执行Filter的init(FilterConfig config)方法.当Filter被移除或服务器正常关闭时,会执行destroy方法);
- 拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。(拦截器是SprinMVC自带的,而SpringMVC存在Controller层的,而controller层可以访问到service层,service层是不能访问service层的,而过滤器是客户端和服务端之间请求与响应的过滤);
- 过滤器和拦截器触发时机、时间、地方不一样,过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是在servlet处理完后,返回给前端之前;
- 执行顺序:过滤前-拦截前-Action处理-拦截后-过滤后,过滤器包裹住servlet,servlet包裹住拦截器。