传统基于Servlet容器的程序中,我们可以使用过滤器和监听器,在Java 框架中还可以使用拦截器,而面向切面编程AOP更是作为Spring框架中的核心思想

1. 拦截器

 应用场景:

  1. 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算 PV(Page View)等;
  2. 权限检查:登陆检测,进入处理器检测是否登陆,如果没有则直接返回登陆界面等;
  3. 性能监控:通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。(反向代理,如 Apache 也可以自动记录);
  4. 通用行为:读取 Cookie 得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取 Locale、Theme 信息等,只要是多个处理器都需要的即可使用拦截器实现。

1 创建拦截器

  1. 创建自定义拦截器并实现org.springframework.web.servlet.HandlerInterceptor接口或者继承org.springframework.web.servlet.handler.HandlerInterceptorAdapter

  2. 拦截器HandlerInterceptor,共有三个方法:

    1. preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)方法在请求处理之前被调用,该方法在 Interceptor 类中最先执行,用来进行一些前置初始化操作或是对当前请求做预处理,也可以进行一些判断来决定请求是否要继续进行下去;
    2. postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView)方法在当前请求处理完成后,也就是Controller方法调用后执行,但是它会在 DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对 Controller 处理之后的 ModelAndView 对象进行操作;
    3. afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex)方法需要在当前对应的 Interceptor 类的 preHandle 方法返回值为 true 时才会执行。顾名思义,该方法将在整个请求结束之后,也就是在 DispatcherServlet 渲染了对应的视图之后执行。此方法主要用来进行资源清理。如://结束后 threadLocal.remove()

HandlerInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
public interface HandlerInterceptor {

default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}

default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}

default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}

拦截器适配器HandlerInterceptorAdapterHandlerInterceptorAdapter继承了抽象接口HandlerInterceptor,建议使用HandlerInterceptorAdapter,因为可以按需进行方法的覆盖。

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
30
31
32
33
34
35
36
37
38
39
40
public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor {

/**
* 预处理回调方法,实现处理器的预处理(如检查登陆),第三个参数为响应的处理器,自定义Controller
* 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}

/**
* 后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
*/
@Override
public void postHandle(
HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
}

/**
* 整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
*/
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}

/**
* 不是HandlerInterceptor的接口实现,是AsyncHandlerInterceptor的,AsyncHandlerInterceptor实现了HandlerInterceptor
*/
@Override
public void afterConcurrentHandlingStarted(
HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
}

}

2. 配置拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class LoginWebMvcConfigurer implements WebMvcConfigurer {
/**
* 排除指定uri请求
*/
private static List<String> tokenPath = new ArrayList<>();

static {
tokenPath.add("/**/tth/xxxx");
tokenPath.add("/**/tth/xxxxxx");
}

@Bean
LoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor()).addPathPatterns(ApiURI.MODULE_URI_PREFIX + "/**").excludePathPatterns(tokenPath).order(1);
}

}

3. 拦截器执行流程

  1. 拦截器执行顺序是按照Spring配置文件中定义的顺序而定的,需要先注册定义的拦截器;
  2. 先按照顺序执行所有拦截器的preHandle方法,当遇到return false为止,如果第二个preHandle方法是return false,则第三个及以后的拦截器都不会执行,如果return ture,则会按照顺序加载完preHandle方法;
  3. 再执行主方法(自己的Controller接口),若中间抛出异常,则跟return false效果一样,不会继续执行postHandle,只会倒序执行afterCompletion方法;
  4. 在主方法执行完业务逻辑(页面还未渲染数据)时,按照倒序执行postHandle方法,若第三个拦截器的preHandle方法return false,则会执行第二个和第一个的postHandle方法和afterCompletionpostHandle都执行完才会执行这个方法,也就是页面渲染完数据后,执行after进行清理工作),postHandleafterCompletion都是倒序执行。

2. 过滤器

应用场景:

  1. 数据压缩
  2. 记录日志
  3. 数据统计
  4. 数据格式转换
  5. 数据设置默认值
  6. 权限认证、黑白名单
  7. 数据加密/解密、签名校验

创建过滤器及配置

  1. 自定义的过滤器需要实现javax.servlet.FilterFilter接口中有三个方法:

    1. init(FilterConfig filterConfig):过滤器初始化的被调用;
    2. doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain):在doFilter()方法中,chain.doFilter()前的一般是对request执行的过滤操作,chain.doFilter后面的代码一般是对response执行的操作,chain.doFiter()执行下一个过滤器或者业务处理器;
    3. destory():过滤器销毁的时候被调用。
  2. Filter的工作原理:

    Filter接口中有一个doFilter方法,当我们编写好Filter,并配置对哪个web资源进行拦截后,WEB服务器每次在调用web资源的service方法之前,
    都会先调用一下filter的doFilter方法,在该方法内编写代码可达到如下目的:

    • 调用目标资源之前,让一段代码执行;
    • 调用目标资源(即是否让用户访问web资源);
    • 调用目标资源之后,让一段代码执行。
  3. 创建步骤:

    方法一(推荐使用)

    1. 创建过滤器类实现 Filter 接口,并在类的上面添加 @WebFilter 注解;
    2. 过滤器类上添加 @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
    /**
    * 自定义过滤器(注解方式)
    */
    @Order(1)
    @WebFilter(filterName = "myFilter", urlPatterns = {"/aa/*", "/bb/*"}, description = "自定义过滤器")
    public class myFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    System.out.println("过滤器初始化");
    }

    @Override
    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之后执行一段代码----");
    }

    @Override
    public void destroy() {
    System.err.println("过滤器销毁");
    }

    }
    1. SpringBoot 启动类上添加 @ServletComponentScan 注解。
    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @ServletComponentScan("com.xxx.filter")
    public class Application {
    public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    }
    }

    方法二

    1. 创建过滤器类实现 Filter 接口;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    //执行
    filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {}
    }
    1. 创建过滤器配置类,里面创建一个过滤器注册 Bean,将之前创建的过滤器注册到其中。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Configuration
    public class FilterConfig {
    @Bean
    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容器调用,当FilterSpring管理后可以使用Spring容器资源。,在一个 web 应用中,可以开发编写多个 Filter,这些 Filter 组合起来称之为一个 Filter链。在用户发起请求后,请求信息会根据过滤器链中过滤器的顺序进入各个过滤器,每经过一层过滤器时,需要通过这些滤器验证逻辑通过后放行,才能进入下一个过滤器,直至到服务器获取资源。等到服务器获取资源成功后进行响应给过滤器,然后再倒序的方式,经过一层层过滤器,最后对用户进行响应。

3. 监听器

Servlet的监听器Listener,它是实现了javax.servlet.ServletContextListener接口的服务器端程序,它也是随web应用的启动而启动,只初始化一次,随web应用的停止而销毁,ListenerServlet规范中定义的一种特殊类。用于监听以下等等域对象的创建和销毁事件。

  1. ServletContext:对应application,实现接口ServletContextListener。在整个Web服务中只有一个,在Web服务关闭时销毁。可用于做数据缓存,例如结合redis,在Web服务创建时从数据库拉取数据到缓存服务器;
  2. HttpSession:对应session会话,实现接口HttpSessionListener。在会话起始时创建,一端关闭会话后销毁。可用作获取在线用户数量;
  3. ServletRequest:对应request,实现接口ServletRequestListener。request对象是客户发送请求时创建的,用于封装请求数据,请求处理完毕后销毁。可用作封装用户信息;

监听域对象的属性发生修改的事件。用于在事件发生前、发生后做一些必要的处理。Filter是Servlet技术中最实用的技术,Web开发人员通过Filter技术,对web服务器管理的所有web资源。过滤器是在请求进入tomcat容器后,但请求进入servlet之前进行预处理的。请求结束返回也是,是在servlet处理完后,返回给前端之前

应用场景:

  1. 统计在线人数和在线用户;
  2. 系统启动时加载初始化信息;
  3. 实现URL级别的权限访问控制、过滤敏感词汇、压缩响应信息等;
  4. 统计网站访问量;
  5. 记录用户访问路径。

3.1 监听器支持的事件类型

  1. ApplicationFailedEvent:该事件为spring boot启动失败时的操作

    可以通过ApplicationFailedEvent 获取Throwable实例对象获取异常信息并处理

  2. 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为引用类型

  3. ApplicationReadyEvent:上下文已经准备完毕的时候触发

    这个时候就可以通过ApplicationReadyEvent获取ConfigurableApplicationContext,然后通过ConfigurableApplicationContext 获取bean的信息

  4. ApplicationStartedEvent:spring boot 启动监听类

    可以在SpringApplication启动之前做一些手脚,比如修改SpringApplication实例对象中的属性值

  5. SpringApplicationEvent:获取SpringApplication

  6. ApplicationEnvironmentPreparedEvent:环境事先准备,spring boot中的环境已经准备完毕

    可以通过ApplicationEnvironmentPreparedEvent获取到SpringApplication、ConfigurableEnvironment等等信息, 可以通过ConfigurableEnvironment实例对象来修改以及获取默认的环境信息。

3.2 监听器的配置使用

配置监听器步骤

  1. 自定义事件,一般是继承ApplicationEvent抽象类 ;

    1
    2
    3
    4
    5
    6
    7
    8
    public class MyEvent extends ApplicationEvent {
    // 第一个参数是不可缺少的,后面的参数可以是任意多个(或零个,自定义)
    public MyEvent(Object source, String info) {
    super(source);
    //内容是自定义的
    System.out.println("事件已经发生了:" + info);
    }
    }
  2. 定义事件监听器,一般是实现ApplicationListener的接口 ;

  3. 启动时候,需要把监听器加入到spring容器中 ;

  4. 发布事件,使用ApplicationEventPublisher.publishEvent 发布事件;

配置方法

  1. 纯Java方式:在启动入口中,通过addListeners增加添加到spring容器和发布事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class MyListener implements ApplicationListener<MyEvent> {
    //一旦springboot 发布了MyEvent事件,就会触发执行此方法,方法内容自定义
    @Override
    public void onApplicationEvent(MyEvent myEvent) {
    System.out.println("接受到事件============" + event.getClass());
    System.out.println("触发执行了监听器");
    }
    }

    @SpringBootApplication
    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();
    }
    }
  2. 注解方式:使用@Component 把监听器纳入到spring容器中管理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Component
    public class MyListener2 implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent myEvent) {
    System.out.println("接受到事件============" + event.getClass());
    System.out.println("触发执行了监听器");
    }
    }

    @SpringBootApplication
    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();
    }
    }
  3. 配置文件形式:在application.yml中配置context.listener.classes属性

    1
    2
    3
    context:
    listener:
    classes: com.xxx.test.listener.MyListener
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @SpringBootApplication
    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();
    }
    }
  4. 使用@EventListener注解(EventListenerMethodProcessor),不用实现ApplicationListener接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Component
    public class MyListener3 {
    @EventListener
    public void listenerMethod(MyEvent myEvent){
    System.out.println("接受到事件============" + event.getClass());
    System.out.println("触发执行了监听器");
    }
    }

    @SpringBootApplication
    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
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

一个切入点声明有两部分:一个包含一个名称和任何参数的签名,一个能精确地确定我们感兴趣的执行方法的切入点表达式。在aop的@Aspectj注解样式中,通过常规方法定义提供切入点签名,并使用@Pointcut注解指示切入点表达式(作为切入点签名的方法必须具有void返回类型)

在定义AOP的类时,不需要和前面拦截器一样麻烦了,只需要通过注解,底层实现逻辑都通过IOC框架实现好了,涉及到的注解如下:

  1. @Aspect:将一个 java 类定义为切面类。

  2. @Pointcut:定义一个切入点,可以是一个规则表达式,比如下例中某个 package 下的所有函数,也可以是一个注解等,以下为切入点指令符。

    • execution:用于匹配方法执行连接点。这是使用SpringAOP时要使用的主要切入点指示符。
    • within:特定类型中的连接点。
    • this:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;
    • args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;参数是给定类型的实例。
    • @args:用于匹配当前执行的方法传入的参数持有指定注解的执行;传递的实际参数的运行时类型具有给定类型的注解。
    • target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;目标对象(要代理的应用程序对象)是给定类型的实例。
    • @target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;执行对象的类具有给定类型的注解。
    • @within:用于匹配指定类型内的方法执行;与具有给定注解的类型中的联接点匹配。
    • @Annotation:用于匹配当前执行方法持有指定注解的方法;在SpringAOP中执行的方法具有给定注解的连接点。
  3. 五种通知(Advice):

    • @Before:前置通知,放在方法头上,在切入点开始处切入内容。
    • @After:后置finally通知,放在方法头上,在切入点结尾处切入内容,无论是否有异常,都会执行,类似于finally。
    • @AfterReturning:后置try通知,放在方法头上,使用returning来引用方法返回值,在切入点 return 内容之后处理逻辑,只有执行成功会执行,异常不会。
    • @AfterThrowing:后置catch通知,放在方法头上,使用throwing来引用抛出的异常,用来处理当切入内容部分抛出异常之后的处理逻辑。
    • @Around:环绕通知,放在方法头上,这个方法要决定真实的方法是否执行,而且必须有返回值,在切入点前后切入内容,并自己控制何时执行切入点自身的内容。原则上可以替代@Before和@After。
  4. @Order(100):AOP 切面执行顺序, @Before 数值越小越先执行,@After 和 @AfterReturning 数值越大越先执行。

区别

它们的执行拦截顺序:ServletContextListener> Filter > Interception > AOP > 具体执行的方法 > AOP > @ControllerAdvice > Interception > Filter > ServletContextListener

根据实现原理可以分为两大类:

  1. Filter和Listener:依赖Servlet容器,基于函数回调实现。可以拦截所有请求,覆盖范围更广,但无法获取ioc容器中的bean;
  2. Interceptor和aop:依赖spring框架,基于java反射和动态代理实现。只能拦截controller的请求,可以获取ioc容器中的bean。

从 Filter -> Interceptor -> aop ,拦截的功能越来越细致、强大,尤其是Interceptor和aop可以更好的结合spring框架的上下文进行开发。但是拦截顺序也是越来越靠后,请求是先进入Servlet容器的,越早的过滤和拦截对系统性能的消耗越少。

Filter与Interceptor联系与区别:

  1. 拦截器是基于java的反射机制,使用代理模式,而过滤器是基于函数回调;
  2. 拦截器不依赖与servlet容器是SpringMVC自带的,过滤器依赖于Servlet容器;
  3. 拦截器只能对action起作用,而过滤器可以对几乎所有的请求起作用(可以保护资源);
  4. 拦截器可以访问controller上下文,堆栈里面的对象,而过滤器不可以。(拦截器的preHandle方法在进入controller前执行,而拦截器的postHandle方法在执行完controller业务流程后,在视图解析器解析ModelAndView之前执行,可以操控Controller的ModelAndView内容。而afterCompletion是在视图解析器解析渲染ModelAndView完成之后执行的),( 过滤器是在服务器启动时就会创建的,只会创建一个实例,常驻内存,也就是说服务器一启动就会执行Filter的init(FilterConfig config)方法.当Filter被移除或服务器正常关闭时,会执行destroy方法);
  5. 拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。(拦截器是SprinMVC自带的,而SpringMVC存在Controller层的,而controller层可以访问到service层,service层是不能访问service层的,而过滤器是客户端和服务端之间请求与响应的过滤);
  6. 过滤器和拦截器触发时机、时间、地方不一样,过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是在servlet处理完后,返回给前端之前;
  7. 执行顺序:过滤前-拦截前-Action处理-拦截后-过滤后,过滤器包裹住servlet,servlet包裹住拦截器。