浅谈 | 过滤器、监听器、拦截器和AOP

Java
327
0
0
2023-04-29

写在前面

最近在进行系统的日志模块重构工作,在选择技术实现的时候在过滤器和拦截器之间飘忽不定,于是决定抽点时间将过滤器和拦截器进行深度分析,顺便把监听器和AOP等内容也复习一下。

当我们需要基于全局实现某些功能时,在传统的Servlet容器中,可以使用过滤器和监听器,在Java框架中还可以使用拦截器。本篇将从过滤器(Filter)、监听器(Listener)、拦截器(Interceptor)和面向切面编程(AOP)这四个方面入手,分别介绍它们的概念以及如何使用。

为了后续学习需要,新建一个SpringBoot项目,并在pom文件中新增web、lombok依赖。之后新建一个名为controller的包,并在该包内新建一个名为IndexController的类,该类用于提供一个访问首页的接口:

@RestController
@Slf4j
public class IndexController {
    @RequestMapping("/index")
    public String index(){
        log.info("这是首页");
        return "the index page";
    }
}

过滤器(Filter)

过滤器简说

过滤器,这里指的是Servlet过滤器,它是在Java Servlet中定义的,能够对Servlet容器中的请求和响应对象进行检查和修改,只起到过滤作用,不会生成Request和Response对象。

过滤器特点:(1)过滤器是基于回调函数实现;(2)过滤器是Servlet规范规定的,只能用于Web程序中;(3)过滤器只在Servlet启动前后起作用,作用范围较窄。

过滤器的配置非常简单,开发者只需自定义类并实现Filter接口,查看一下这个Filter接口的源码:

public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {
    }
}

可以看到里面只有三个方法,它们的作用分别如下所示:

(1)init(),该方法在Servlet容器启动初始化过滤器时被调用,即它在Filter整个的生命周期中只会被调用一次。「此方法必须调用成功,否则Filter无法初始化,那么后续将无法提供服务。」

(2)doFilter(),Servlet容器中每一次请求都会调用该方法,FilterChain用于调用下一个过滤器。

(3)destroy(),当Servlet容器销毁该Filter实例时被调用,通常在该方法中书写销毁或者关闭资源的逻辑。同样在Filter整个的生命周期中只会被调用一次。

SpringBoot实现过滤器的三种方式

在SpringBoot中使用过滤器有三种方式,分别对应不同的业务场景:(1)无路径无顺序;(2)有路径无顺序;(3)有路径有顺序。

无路径无顺序

无路径无顺序,即默认过滤所有的路径,且多个过滤器之间不存在执行顺序。这种方式最简单,只需自定义类并实现Filter接口,并在自定义类上使用@Component注解将其交由Spring管理。

第一步,新建filter包,并在该包内新建一个名为IndexFilter的类,该类需要实现Filter接口并重写其中的三个方法:

@Component
@Slf4j
public class IndexFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
       log.info("进入了Filter的doFilter方法");
       filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("进入了Filter的init方法");
    }

    @Override
    public void destroy() {
        log.info("进入了Filter的destroy方法");
    }
}

第二步,启动项目,访问/index接口,可以看到IDEA输出如下信息:

img

可以看到每访问该接口,doFilter方法就会被调用,而init和destory方法则在Filter实例的整个生命周期中只执行一次。

有路径无顺序

有路径无顺序,即指定过滤的路径,但是多个过滤器之间不存在执行顺序。这种方式配置稍微复杂些,开发者需要自定义类并实现Filter接口,然后在自定义类上使用@WebFilter注解,通过@WebFilter注解来设置过滤器的匹配路径,同时还需要在启动类中添加@ServletComponentScan注解,@ServletComponentScan注解用于扫描添加@WebFilter@WebServlet@WebListener注解的Bean,并在使用的时候自动注入:

@WebFilter(filterName = "indexFilter2",urlPatterns = "/hello")
@Slf4j
public class IndexFilter2 implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("进入了Filter2的doFilter方法");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("进入了Filter2的init方法");
    }

    @Override
    public void destroy() {
        log.info("进入了Filter2的destroy方法");
    }
}


@ServletComponentScan
@SpringBootApplication
public class FilterbookApplication {
    public static void main(String[] args) {
        SpringApplication.run(FilterbookApplication.class, args);
    }
}

可以看到我们在IndexFilter2的类上添加了@WebFilter(filterName = "IndexFilter2",urlPatterns = "/hello")配置项,目的就是实现对特定URL的拦截,此处是访问/hello才会拦截,因此当开发者启动项目访问/index接口时IndexFilter2是不会拦截的。

有路径有顺序

有路径无顺序,即指定过滤的路径,同时多个过滤器之间存在执行顺序。这种方式配置比较复杂,只能通过配置类来实现,且只针对实现过滤器的接口,它不需要通过注解来注入Spring容器,只通过配置类实现。

两个自定义过滤器类:

@Slf4j
public class FilterA implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
       log.info("进入了FilterA的doFilter方法");
       filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("进入了FilterA的init方法");
    }

    @Override
    public void destroy() {
        log.info("进入了FilterA的destroy方法");
    }
}


@Slf4j
public class FilterB implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
       log.info("进入了FilterB的doFilter方法");
       filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("进入了FilterB的init方法");
    }

    @Override
    public void destroy() {
        log.info("进入了FilterB的destroy方法");
    }
}

IndexFilterConfig配置类:

@Configuration
public class IndexFilterConfig {
    @Bean
    public FilterRegistrationBean filterA(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        FilterA filterA = new FilterA();
        filterRegistrationBean.setFilter(filterA);
        filterRegistrationBean.addUrlPatterns("*");
        filterRegistrationBean.setName("filterA");
        filterRegistrationBean.setOrder(1);
        return filterRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean filterB(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        FilterB filterB = new FilterB();
        filterRegistrationBean.setFilter(filterB);
        filterRegistrationBean.addUrlPatterns("*");
        filterRegistrationBean.setName("filterB");
        filterRegistrationBean.setOrder(2);
        return filterRegistrationBean;
    }
}

监听器(Listener)

监听器简说

监听器,它也是Servlet层面的内容,可用于监听Web应用中某些对象或者信息的创建、修改和销毁等动作发生,并作出相应的响应处理。

监听器分类

根据监听的对象,监听器可分为三类:

「(1)ServletContext对象监听器。」ServletContext对象监听器。对应于application,需要实现ServletContextListener接口。在整个Web服务中只有一个,在Web服务关闭时销毁。可用于做数据缓存,如结合redis可在Web服务创建时从数据库加载数据到缓存服务器中,提升系统响应速度。

「(2)HttpSession对象监听器。」HttpSession对象监听器。对应session会话,需要实现HttpSessionListener接口。在会话起始时创建,当一端关闭会话后销毁。可用于获取在线的用户数量。

「(3)ServletRequest对象监听器。」ServletRequest对象监听器。对应request,需要实现ServletRequestListener接口。request对象是客户发送请求时创建的,用于封装请求数据,当请求处理完毕时,才销毁。可用于封装用户信息。

SpringBoot实现监听器有两种方式,一种是直接在实现了对应接口的类上添加@Component注解;另一种则是先在实现类上添加@WebListener注解,再在项目启动类上添加@ServletComponentScan注解,此处采用第二种方式。

ServletContext对象监听器

ServletContextListener接口,用于监控ServletContext对象的生命周期,实际上就是监听Web应用的生命周期。ServletContextListener接口主要有两个方法,一个在当Servlet容器启动web应用时调用(contextInitialized),另一个是在Servlet容器终止web应用时调用(contextDestroyed):

public interface ServletContextListener extends EventListener {
    default void contextInitialized(ServletContextEvent sce) {
    }

    default void contextDestroyed(ServletContextEvent sce) {
    }
}

如果开发者想自定义ServletContext对象监听器,只需实现ServletContextListener接口,并重写其中的两个方法:

@WebListener
@Slf4j
public class MyServletContextListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.info("MyServletContextListener contextInitialized has called");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        log.info("MyServletContextListener contextInitialized has called");
    }
}
HttpSession对象监听器

HttpSessionListener接口,用于监控HttpSession对象的生命周期,实际上就是监听Session会话的生命周期。HttpSessionListener接口主要有两个方法,一个在Session会话创建时调用(sessionCreated),另一个在Session会话销毁时调用(sessionDestroyed):

public interface HttpSessionListener extends EventListener {
    default void sessionCreated(HttpSessionEvent se) {
    }

    default void sessionDestroyed(HttpSessionEvent se) {
    }
}

如果开发者想自定义HttpSession对象监听器,只需实现HttpSessionListener接口,并重写其中的两个方法:

@WebListener
@Slf4j
public class MyHttpSessionListener implements HttpSessionListener {
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        log.info("MyHttpSessionListener sessionCreated has called");
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        log.info("MyHttpSessionListener sessionDestroyed has called");
    }
}
ServletRequest对象监听器

ServletRequestListener接口,用于监控ServletRequest对象的生命周期。ServletRequestListener接口主要有两个方法,一个在ServletRequest对象初始化时调用(requestInitialized),另一个在ServletRequest对象销毁时调用(requestDestroyed):

public interface ServletRequestListener extends EventListener {
    default void requestDestroyed(ServletRequestEvent sre) {
    }

    default void requestInitialized(ServletRequestEvent sre) {
    }
}

如果开发者想自定义ServletRequest对象监听器,只需实现ServletRequestListener接口,并重写其中的两个方法:

@WebListener
@Slf4j
public class MyServletRequestListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        log.info("MyServletRequestListener requestDestroyed has called");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        log.info("MyServletRequestListener requestInitialized has called");
    }
}

拦截器(Interceptor)

拦截器简说

拦截器,这里指的是Spring中的拦截器,主要用于拦截用户请求并做相应的处理。拦截器和过滤器、监听器不同,它不依赖于Servlet容器,依赖于Spring框架,是AOP的一种体现,底层基于Java的动态代理实现。

拦截器特点:(1)拦截器基于Java反射机制(动态代理)实现;(2)拦截器是Spring特有的,能使用Spring中的任何资源;(3)拦截器可以用于Web程序,也可以用于Application和Swing程序汇总;(4)拦截器能够深入到方法前后,异常抛出前后,伸缩性较大,作用范围较广。

拦截器是链式调用,一个应用中可以同时存在多个拦截器(Interceptor),一个请求也可以触发多个拦截器,每个拦截器会根据它被声明的顺序依次被调用。拦截器的配置稍微复杂些,用户首先也要自定义一个类并实现HandlerInterceptor接口,查看一下这个HandlerInterceptor接口的源码:

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 {
    }
}

可以看到里面也只有三个方法,它们的作用分别如下所示:

(1)preHandle(),该方法将在请求处理之前调用。「注意,如果该方法返回值为false,那么就认为当前请求结束,这不仅导致当前的拦截器失效,同时其他的拦截器也不再执行。」

(2)postHandle(),该方法只有在preHandle()方法返回true的时候才会执行,且会在Controller中方法调用之后,DispatcherServlet返回渲染视图之前被调用。「注意,postHandle()方法被调用的顺序与preHandle()是相反的,即先声明的拦截器的preHandle()会先执行,但是它的postHandle()方法反而会在后执行。」

(3)afterCompletion(),该方法只有在preHandle()方法返回true的时候才会执行,且会在整个请求结束之后,DispatcherServlet渲染了对应的视图之后执行。可以在此方法中进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中。

拦截器实现步骤

SpringBoot实现拦截器分为两个步骤:(1)自定义需要拦截类并实现HandlerInterceptor接口重写其中的三个方法;(2)通过配置类来注册拦截器,配置类需要实现WebMvcConfigurer接口,并重写其中的addInterceptors方法。

第一步,新建interceptor包,并在该包内新建一个名为IndexInterception的类,该类需要实现HandlerInterceptor接口并重写其中的三个方法:

@Component
@Slf4j
public class IndexInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("进入了Interceptor的preHandle方法");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("进入了Interceptor的postHandle方法");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("进入了Interceptor的afterCompletion方法");
    }
}

第二步,新建config包,并在该包内新建一个名为IndexInterceptorConfig的类,该类需要实现WebMvcConfigurer接口并重写其中的addInterceptors方法:

@Configuration
public class IndexInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new IndexInterceptor()).addPathPatterns("/**");
    }
}

可以看到这里我们将自定义的拦截器处理类注册到InterceptorRegistry中并通过addPathPatterns("/**")方法设置了需要拦截的URL。

第三步,启动项目,访问/index接口,可以看到IDEA输出如下信息:

img

可以看到每访问该接口,这三个方法就会被调用。一般我们会在preHandle中获取到拦截的方法:

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("进入了Interceptor的preHandle方法");
        HandlerMethod handlerMethod = (HandlerMethod)handler;
        Method method = handlerMethod.getMethod();
        String methodName = method.getName();
        log.info("拦截到的方法名称为:{}",methodName);
        return true;
    }

AOP

AOP简说

AOP(面向切面编程)是一种编程思想,并不是某种具体实现。一般在提到AOP实现时,会有过滤器和代理模式这两种,过滤器基于函数回调实现,而代理模式基于java反射机制。代理模式分为静态代理和动态代理,动态代理就是拦截器的简单实现。

AOP中的一概念

(1)通知(Advice),AOP框架中的增强处理,用于描述切面何时执行以及如何增强处理。

(2)连接点(Join Point),即应用程序执行过程中能够插入切面的一个点,这个点可以是方法的调用,也可以是异常的抛出。需要注意的是,在Spring AOP中,这个点只能是方法的调用。

(3)切点(Point Cut),可以插入且用于增强处理的连接点。

(4)切面(Aspect),里面通常会定义通知和切点。

(5)引入(Introduction),引入允许开发者向现有的类中添加新方法或者属性。

(6)织入(Weaving),它是一个过程,即将增强处理添加到目标对象中并创建一个被增强的对象的过程。

相比于拦截器,Spring AOP封装性更好,且功能更强大,使用的时候需要单独引入spring-boot-starter-aop这一jar包。

在使用AOP的时候,由于底层都通过Spring容器来管理,因此可直接通过注解来使用它,以下是经常会使用到的注解:

(1)@Aspect,使用在类上,用于将普通Java类定义为切面类;

(2)@Pointcut,用于定义一个切入点,可以是一个规则表达式,也可以是注解;

(3)@Before,用于在切入点开始处切入内容;

(4)@After,用于在切入点结尾处切入内容;

(5)@AfterReturning,用于在切入点返回内容之后处理逻辑;

(6)@Around,用于在切入点前后切入内容,并控制何时执行切入点的内容。可以理解是@Before@After注解的组合使用;

(7)@AfterThrowing,用于当切入内容部分抛出异常之后的处理逻辑。(8)@Order(1),AOP切面的执行顺序。注意@Before数值越小越先执行,@After@AfterReturning则是数值越大越先执行。

举个例子,如下所示:

@Component
@Aspect
@Slf4j
public class MyAspect {
    @Pointcut("execution(public * com.envy.filterbook.controller.IndexController.*(..))")
    public void myPointCut(){}

    @Before("myPointCut()")
    public void myBefore(){
        log.info("myBefore...");
    }

    @After("myPointCut()")
    public void myAfter(){
        log.info("myAfter...");
    }

    @Around("myPointCut()")
    public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("myAround...before...");
        Object proceed = joinPoint.proceed();
        log.info("myAround...after...");
        return proceed;
    }

    @AfterReturning("myPointCut()")
    public void myAfterReturning(){
        log.info("myAfterReturning...");
    }
}

如何选择

以上介绍的FilterServletContextListenerInterceptorAOP以及经常用到的@ControllerAdvice注解,它们的执行顺序如下所示:

ServletContextListener-->Filter-->Interceptor-->AOP-->具体执行的方法-->AOP-->@ControllerAdvice-->Interceptor-->Filter-->ServletContextListener

上述对象根据实现原理可分为两大类:

(1)FilterListener:依赖Servlet容器,基于函数回调实现,几乎可拦截所有请求,但无法获取Spring IOC容器中的Bean对象。

(2)InterceptorAOP:依赖Spring框架,基于Java反射和动态代理实现,只能拦截Controller中的请求,可以获取Spring IOC容器中的Bean对象。

可以看到从Filter-->Interceptor-->AOP,拦截功能越来越强大,尤其是InterceptorAOP可以结合Spring框架来进行实现,但是拦截顺序确是越来越往后,所以如果有些请求可以在Servlet容器中过滤,那么这是最好的,毕竟越早的过滤和拦截就消耗越少的系统资源。当然这个需要结合实际业务来进行确定具体选用哪种方式。

过滤器和拦截器区别

其实过滤器和拦截器都是AOP(面向切面编程)的具体实现,都可以实现诸如登录鉴权、日志记录等功能,但是也存在一些不同点,下面将进行分析。

实现原理不同

「(1)过滤器(Filter)基于函数回调。」前面说过自定义的过滤器需要实现Filter接口并重写其中的三个方法,注意其中的doFilter方法:

void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

里面有三个参数,分别是ServletRequestServletResponseFilterChain,前面两个没什么说的,这个FilterChain其实是一个回调接口:

public interface FilterChain {
    void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}

可以看到这个类里面也有一个doFilter方法,这其实就是一个回调方法,该接口常用的实现类是ApplicationFilterChain,这里以ApplicationFilterChain为例进行介绍。

查看一下ApplicationFilterChain类的doFilter方法逻辑:

    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        if (Globals.IS_SECURITY_ENABLED) {
            ServletRequest req = request;
            ServletResponse res = response;

            try {
                AccessController.doPrivileged(() -> {
                    this.internalDoFilter(req, res);
                    return null;
                });
            } catch (PrivilegedActionException var7) {
               //异常处理
            }
        } else {
            this.internalDoFilter(request, response);
        }
    }

可以看到无论是否开启了全局安全,都会调用internalDoFilter这个内置的过滤器方法:

  private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        //如果当前所在的过滤器不是最后一个,那么获取当前过滤器信息
        if (this.pos < this.n) {
            ApplicationFilterConfig filterConfig = this.filters[this.pos++];

            try {
                Filter filter = filterConfig.getFilter();
                if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
                    request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
                }

                if (Globals.IS_SECURITY_ENABLED) {
                    Principal principal = ((HttpServletRequest)request).getUserPrincipal();
                    Object[] args = new Object[]{request, response, this};
                    SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
                } else {
                    filter.doFilter(request, response, this);
                }
            } catch (ServletException | RuntimeException | IOException var15) {
                 //异常处理
            }
        } else {
            try {
                if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                    lastServicedRequest.set(request);
                    lastServicedResponse.set(response);
                }

                if (request.isAsyncSupported() && !this.servletSupportsAsync) {
                    request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
                }
                if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && Globals.IS_SECURITY_ENABLED) {
                    Principal principal = ((HttpServletRequest)request).getUserPrincipal();
                    Object[] args = new Object[]{request, response};
                    SecurityUtil.doAsPrivilege("service", this.servlet, classTypeUsedInService, args, principal);
                } else {
                    this.servlet.service(request, response);
                }
            } catch (ServletException | RuntimeException | IOException var17) {
                 //异常处理
            } finally {
                if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                    lastServicedRequest.set((Object)null);
                    lastServicedResponse.set((Object)null);
                }
            }
        }
    }

可以看到在internalDoFilter方法中会获取到所有的过滤器,并在内部的doFilter方法中依次调用这些过滤器,每个过滤器都会先执行内部的doFilter方法,最后在执行结束前会调用FilterChain.doFilter(request, response)方法,其实就是调用ApplicationFilterChain.doFilter(request, response)方法,这样循环执行进而实现函数回调:

img

「(2)拦截器(Interceptor)基于Java反射机制(动态代理)实现。」

使用范围不同

(1)自定义的过滤器需要实现javax.servlet.Filter接口,而这个接口是在Servlet规范中定义的,或者说过滤器需要依赖于Servlet容器,因此它只能在Web程序中使用:

img

(2)自定义的拦截器需要实现org.springframework.web.servlet.HandlerInterceptor接口,它存在于SpringMVC中,由Spring容器来管理,不依赖于Servlet容器,是可以单独使用的,因此不仅可以用在Web程序中,也可以用在Application、Swing等程序中:

img

触发时机不同

下图是过滤器和拦截器的触发时机:

img

「(1)过滤器(Filter)是在请求进入容器后,但在进入Servlet之前进行预处理,请求结束是在Servlet处理完之后。」即过滤器对请求进行预处理,再交给Servlet处理并生成响应,最后再对服务器响应进行后处理。

「(2)拦截器(Interceptor)是请求进入Servlet后,但在进入Controller之前进行预处理,请求结束是在Controller中渲染了对应的视图之后。」拦截器可以在方法执行前调用(preHandle),方法执行后调用(postHandle),视图页面渲染后调用(afterCompletion)。

拦截的请求范围不同

这里我们通过一个实例在进行说明,在前面我们已经分别配置了过滤器和拦截器并提供了/index这一API,接下来我们尝试同时配置过滤器和拦截器。之后启动项目,可以看到IDEA控制台输出如下信息:

img

接着访问/index这一API,可以看到IDEA控制台输出如下信息:

img

然后再次访问/index这一API,可以看到IDEA控制台输出如下信息:

img

也就说执行顺序如下所示:

img

可以看到过滤器的init和destory方法只会调用一次,且分别在容器初始化和销毁时调用。而拦截器中的三个方法和过滤器中的doFilter方法则会在每次请求的时候都会调用。

实际上过滤器会对几乎所有进入容器的请求都起作用,但是拦截器只会对Controller中的请求或者访问static目录下的静态资源起作用(目标执行方法起作用)。

注入Bean的时机不同

前面我们使用的都是Controller层,但在实际开发过程中经常会遇到Service层,因此接下来我们尝试分别在过滤器和拦截器中都注入Service,看看这两者有什么区别。

第一步,新建service包,并在里面新建IndexService接口,并定义IndexServiceImpl类来实现这个接口:

public interface IndexService {
    void test();
}

@Service
public class IndexServiceImpl implements IndexService{
    @Override
    public void test() {
        System.out.println("test for IndexServiceImpl is called");
    }
}

第二步,在自定义的过滤器IndexFilter中注入这个IndexService对象并在doFilter方法中调用它的test方法:

    @Autowired
    private IndexService indexService;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
       log.info("进入了Filter的doFilter方法");
       indexService.test();
       filterChain.doFilter(servletRequest,servletResponse);
    }

第三步,启动项目,并访问/index这一API,可以看到IDEA控制台输出如下信息:

进入了Filter的init方法
进入了Filter的doFilter方法
test for IndexServiceImpl is called
进入了Interceptor的preHandle方法
这是首页
进入了Interceptor的postHandle方法
进入了Interceptor的afterCompletion方法

可以看到这个输出顺序与之前介绍的内容完全一致。

第四步,先注释掉第二步中的操作,然后在自定义的拦截器IndexInterceptor中注入这个IndexService对象并在postHandle方法中调用它的test方法:

    @Autowired
    private IndexService indexService;

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("进入了Interceptor的postHandle方法");
        indexService.test();
    }

第五步,启动项目,并访问/index这一API,可以看到IDEA控制台报错了,原因是自定义的拦截器IndexInterceptor中注入的IndexService对象是空的:

img

为什么是这样呢?还记得之前我们在IndexInterceptorConfig#addInterceptors()中的代码么,我们在调用addInterceptor方法的时候手动new了一个IndexInterceptor对象:

@Configuration
public class IndexInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new IndexInterceptor()).addPathPatterns("/**");
    }
}

而我们在IndexInterceptor类上又添加了@Component注解,表示让Spring容器来管理并生成IndexInterceptor对象:

@Component
@Slf4j
public class IndexInterceptor implements HandlerInterceptor {
}

很明显你注册的拦截器和Spring容器中存在的不是同一个,所以才会导致Spring无法管理进而出现空指针。解决办法就是去掉IndexInterceptor类上的@Component注解,同时在IndexInterceptorConfig类中手动定义一个IndexInterceptor对象,并将该拦截器注册一下:

@Configuration
public class IndexInterceptorConfig implements WebMvcConfigurer {
    @Bean
    public IndexInterceptor interceptor(){
        System.out.println("手动注入了IndexInterceptor");
        return new IndexInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor()).addPathPatterns("/**");
    }
}

之后再次重新启动项目,并访问/index这一API,可以看到IDEA控制台输出如下信息:

进入了Filter的init方法
进入了Filter的doFilter方法
进入了Interceptor的preHandle方法
这是首页
进入了Interceptor的postHandle方法
test for IndexServiceImpl is called
进入了Interceptor的afterCompletion方法

执行流程不同

在实际开发过程中经常出现同时存在多个拦截器或者过滤器,此时我们想控制它们执行的先后顺序,这都是可以的。

过滤器控制执行顺序

对于过滤器(Filter)来说,可以在类上使用@Order注解来控制多个过滤器的执行顺序,其中@Order注解中的数字越小,那么优先级越高,越先执行:

@Order(1)  //过滤器优先级
@Component
@WebFilter(filterName = "IndexFilter",urlPatterns = "/*")
@Slf4j
public class IndexFilter1 implements Filter {
}

@Order(2)  //过滤器优先级
@Component
@WebFilter(filterName = "IndexFilter",urlPatterns = "/*")
@Slf4j
public class IndexFilter2 implements Filter {
}

@Order(3)  //过滤器优先级
@Component
@WebFilter(filterName = "IndexFilter",urlPatterns = "/*")
@Slf4j
public class IndexFilter3 implements Filter {
}
过滤器执行流程

客户端发起的请求首先是经过了过滤器并处理了Request请求,之后去访问Servlet资源,当Servlet执行完毕后,Response响应也会经过过滤器,即也是经过了过滤器处理:

img

拦截器控制执行顺序

对于拦截器(Interceptor)来说,它们的注册顺序就是默认的执行顺序,当然也可以通过Order方法来进行控制,Order方法中的数字越小,那么优先级越高,越先执行:

   @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor2()).addPathPatterns("/**").order(2);
        registry.addInterceptor(interceptor3()).addPathPatterns("/**").order(3);
        registry.addInterceptor(interceptor1()).addPathPatterns("/**").order(1);
    }

需要注意的是对于多个拦截器来说,先声明的拦截器的preHandle方法先执行,但是它的postHandle方法后执行,也就是说postHandle方法的执行顺序和preHandle方法的执行顺序是相反的:

进入了Interceptor1的preHandle方法
进入了Interceptor2的preHandle方法
进入了Interceptor3的preHandle方法
这是首页
进入了Interceptor3的postHandle方法
进入了Interceptor2的postHandle方法
进入了Interceptor1的postHandle方法
进入了Interceptor3的afterCompletion方法
进入了Interceptor2的afterCompletion方法
进入了Interceptor1的afterCompletion方法
拦截器执行流程

(1)程序先执行preHandle()方法,如果该方法返回值为true,那么程序将继续往下执行处理器中的方法,否则认为当前请求结束,不再往下执行;(2)在Controller类(业务处理器)处理完请求后会执行postHandle()方法,之后通过DispatcherServlet向客户端返回响应;

(3)DispatcherServlet处理完请求后执行afterCompletion()方法。:

img

接下来通过阅读源码加深印象,我们知道Controller中的所有请求都会经过核心组件DispatcherServlet并调用它的doDispatch()方法,查看该方法的源码:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
  HttpServletRequest processedRequest = request;
  HandlerExecutionChain mappedHandler = null;
  boolean multipartRequestParsed = false;

  WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

  try {
   ModelAndView mv = null;
   Exception dispatchException = null;

   try {
    processedRequest = checkMultipart(request);
    multipartRequestParsed = (processedRequest != request);

    // 确定处理当前请求的程序
    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null) {
     noHandlerFound(processedRequest, response);
     return;
    }

    // 确定处理当前请求的程序适配器
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

    // Process last-modified header, if supported by the handler.
    String method = request.getMethod();
    boolean isGet = HttpMethod.GET.matches(method);
    if (isGet || HttpMethod.HEAD.matches(method)) {
     long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
     if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
      return;
     }
    }
                //调用Interceptor拦截器中的PreHandle方法
    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
     return;
    }

    // 使用给定的处理程序来处理这个请求,即执行Handle(核心方法业务逻辑也在这个方法中)
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

    if (asyncManager.isConcurrentHandlingStarted()) {
     return;
    }
                //进行视图转换
    applyDefaultViewName(processedRequest, mv);
                //调用Interceptor拦截器中的PostHandle方法
    mappedHandler.applyPostHandle(processedRequest, response, mv);
   }
   catch (Exception ex) {
    //异常处理
   }
   processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
  }
  catch (Exception ex) {
   //异常处理
  }
  finally {
   if (asyncManager.isConcurrentHandlingStarted()) {
    // Instead of postHandle and afterCompletion
    if (mappedHandler != null) {
     mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
    }
   }
   else {
    // Clean up any resources used by a multipart request.
    if (multipartRequestParsed) {
     cleanupMultipart(processedRequest);
    }
   }
  }
 }

可以看到执行顺序总的来说就是applyPreHandle-->handle(实际业务逻辑)-->渲染视图-->applyPostHandle,查看一下上述提到的applyPreHandleapplyPostHandle方法的源码:

    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
  for (int i = 0; i < this.interceptorList.size(); i++) {
   HandlerInterceptor interceptor = this.interceptorList.get(i);
   if (!interceptor.preHandle(request, response, this.handler)) {
    triggerAfterCompletion(request, response, null);
    return false;
   }
   this.interceptorIndex = i;
  }
  return true;
 }

    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
   throws Exception {
  for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
   HandlerInterceptor interceptor = this.interceptorList.get(i);
   interceptor.postHandle(request, response, this.handler, mv);
  }
 }

可以发现它们在遍历interceptorList数组的时候一个是从左到右,另一个是从右到左,所以这两者的调用方式是相反的。

应用场景不同

过滤器(Filter)一般用于:字符编码转换、敏感词过滤、登录权限验证、资源访问控制等;

拦截器(Interceptor)一般用于:AOP以及需要配合一些业务逻辑的场景(注入Bean等)。

这里顺便提一下监听器的作用,一般用于统计在线人数、清除过期session等。

小结

本篇主要基于SpringBoot学习了如何使用过滤器、监听器、拦截器和AOP,其实无论是过滤器还是拦截器,它们都是AOP的一种实现,同时也学习了另一种更加灵活的AOP实现,基于Aspect切面的方式,这种方式似乎更加优雅。参考文章:一口气说出 过滤器 和 拦截器 6个区别,别再傻傻分不清了SpringBoot 过滤器、监听器、拦截器、AOP 比较Java三大器之拦截器(Interceptor)的实现原理及代码示例。