前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅谈 | 过滤器、监听器、拦截器和AOP

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

作者头像
啃饼思录
发布2023-03-18 16:32:31
8450
发布2023-03-18 16:32:31
举报

写在前面

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

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

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

代码语言:javascript
复制
@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接口的源码:

代码语言:javascript
复制
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接口并重写其中的三个方法:

代码语言:javascript
复制
@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输出如下信息:

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

有路径无顺序

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

代码语言:javascript
复制
@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容器,只通过配置类实现。

两个自定义过滤器类:

代码语言:javascript
复制
@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配置类:

代码语言:javascript
复制
@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):

代码语言:javascript
复制
public interface ServletContextListener extends EventListener {
    default void contextInitialized(ServletContextEvent sce) {
    }

    default void contextDestroyed(ServletContextEvent sce) {
    }
}

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

代码语言:javascript
复制
@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):

代码语言:javascript
复制
public interface HttpSessionListener extends EventListener {
    default void sessionCreated(HttpSessionEvent se) {
    }

    default void sessionDestroyed(HttpSessionEvent se) {
    }
}

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

代码语言:javascript
复制
@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):

代码语言:javascript
复制
public interface ServletRequestListener extends EventListener {
    default void requestDestroyed(ServletRequestEvent sre) {
    }

    default void requestInitialized(ServletRequestEvent sre) {
    }
}

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

代码语言:javascript
复制
@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接口的源码:

代码语言:javascript
复制
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接口并重写其中的三个方法:

代码语言:javascript
复制
@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方法:

代码语言:javascript
复制
@Configuration
public class IndexInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new IndexInterceptor()).addPathPatterns("/**");
    }
}

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

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

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

代码语言:javascript
复制
    @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则是数值越大越先执行。

举个例子,如下所示:

代码语言:javascript
复制
@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方法:

代码语言:javascript
复制
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

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

代码语言:javascript
复制
public interface FilterChain {
    void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}

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

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

代码语言:javascript
复制
    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这个内置的过滤器方法:

代码语言:javascript
复制
  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)方法,这样循环执行进而实现函数回调:

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

使用范围不同

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

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

触发时机不同

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

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

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

拦截的请求范围不同

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

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

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

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

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

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

注入Bean的时机不同

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

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

代码语言:javascript
复制
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方法:

代码语言:javascript
复制
    @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控制台输出如下信息:

代码语言:javascript
复制
进入了Filter的init方法
进入了Filter的doFilter方法
test for IndexServiceImpl is called
进入了Interceptor的preHandle方法
这是首页
进入了Interceptor的postHandle方法
进入了Interceptor的afterCompletion方法

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

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

代码语言:javascript
复制
    @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对象是空的:

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

代码语言:javascript
复制
@Configuration
public class IndexInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new IndexInterceptor()).addPathPatterns("/**");
    }
}

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

代码语言:javascript
复制
@Component
@Slf4j
public class IndexInterceptor implements HandlerInterceptor {
}

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

代码语言:javascript
复制
@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控制台输出如下信息:

代码语言:javascript
复制
进入了Filter的init方法
进入了Filter的doFilter方法
进入了Interceptor的preHandle方法
这是首页
进入了Interceptor的postHandle方法
test for IndexServiceImpl is called
进入了Interceptor的afterCompletion方法

执行流程不同

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

过滤器控制执行顺序

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

代码语言:javascript
复制
@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响应也会经过过滤器,即也是经过了过滤器处理:

拦截器控制执行顺序

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

代码语言:javascript
复制
   @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方法的执行顺序是相反的:

代码语言:javascript
复制
进入了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()方法。:

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

代码语言:javascript
复制
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方法的源码:

代码语言:javascript
复制
    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)的实现原理及代码示例。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-06-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 啃饼随笔 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 过滤器(Filter)
    • 过滤器简说
      • SpringBoot实现过滤器的三种方式
      • 监听器(Listener)
        • 监听器简说
          • 监听器分类
          • 拦截器(Interceptor)
            • 拦截器简说
              • 拦截器实现步骤
              • AOP
                • AOP简说
                  • AOP中的一概念
                    • 如何选择
                    • 过滤器和拦截器区别
                      • 实现原理不同
                        • 使用范围不同
                          • 触发时机不同
                            • 拦截的请求范围不同
                              • 注入Bean的时机不同
                                • 执行流程不同
                                  • 应用场景不同
                                  • 小结
                                  相关产品与服务
                                  容器服务
                                  腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                  领券
                                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档