农银一面:Filter、Interceptor、Spring AOP 的执行顺序

文摘   2024-12-28 10:00   山东  

引言

在我们的日常开发工作中,Filter(过滤器)、Interceptor(拦截器)和 AOP(面向切面编程)是非常常用的 3 种请求处理技术。在不同的应用场景中,使用它们都可以在不影响主业务逻辑的前提下为系统增加额外的功能。面试官去问这个问题的时候,一般是想考察求职者的技术深度和对框架机制的理解。本篇我们从 3 者的基本概念及使用来分析解答下这道面试题。

Filter

什么是 Filter

Filter 是 Java Servlet 规范的一部分,定义在 javax.servlet 包中,Filter 可以对 Servlet 容器的所有 HTTP 请求(HttpServletRequest)和响应(HttpServletResponse)进行预处理或后处理操作。例如,在请求到达目标资源之前执行身份验证或设置字符编码,或者在响应返回给客户端前修改其响应内容格式。

Filter 接口

package javax.servlet;

import java.io.IOException;

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

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;

    default public void destroy() {}
}

项目中自定义过滤器需实现该接口,接口中的 3 个方法就是 Filter 的整个生命周期。init()-初始化、doFilter()-执行过滤逻辑、destroy()-销毁。

  • init方法:Web 容器在启动时,会触发每个 Filter 实例的 init 方法调用并传递一个 FilterConfig 对象,该配置允许过滤器获取初始化参数以及 ServletContext 上下文对象,从而加载任何所需的资源。该方法在 Filter 的整个生命周期中仅会在初始化时被调用一次。

    该方法如果抛出异常,Web 容器就会认为这个过滤器无法正常工作,因此不会将它加入到过滤器链中,无法提供后续的请求过滤工作。

  • doFilter方法:该方法为 Filter 的核心工作方法,每一次请求都会调用该方法。

    FilterChain 接口参数由具体的 Servlet 容器实现并提供。每个过滤器的 doFilter 方法都会接收一个 FilterChain 对象作为参数。在这个方法内部,过滤器可以选择:

    • 直接处理请求/响应。
    • 调用 chain.doFilter(request, response) 将请求传递给下一个过滤器或目标资源。
  • destroy方法:Web 容器在销毁时,会触发每个 Filter 实例的 destroy 方法调用,清理过滤器所有持有的资源(如内存、文件句柄、线程等)。该方法在 Filter 的整个生命周期中也仅会执行一次。

Filter 的配置使用

在 SpringBoot 项目中可以使用如下几种配置方式:

  • 使用 @WebFilter 注解 + @ServletComponentScan 注解

在过滤器类上使用 @WebFilter 注解来定义 URL 模式和其他属性

package com.example.filter;

@WebFilter(urlPatterns = "/*", filterName = "exampleFilter")
public class ExampleFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化逻辑...
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 传递给下一个过滤器或目标资源
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        // 清理资源...
    }
}

在启动类或者任意配置类上加上 @ServletComponentScan 注解来让 Spring Boot 自动扫描并注册这些过滤器。

@SpringBootApplication
@ServletComponentScan(basePackages = "com.example.filter"// 指定扫描包路径
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.classargs);
    }
}
  • 使用 FilterRegistrationBean 进行注册
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<ExampleFilter> customFilterRegistration() {
        FilterRegistrationBean<ExampleFilter> registrationBean = new FilterRegistrationBean<>();

        ExampleFilter customFilter = new ExampleFilter();
        registrationBean.setFilter(customFilter);
        registrationBean.addUrlPatterns("/*");
        registrationBean.setOrder(1); // 设置过滤器的执行顺序

        // 添加初始化参数
        registrationBean.addInitParameter("encoding""UTF-8");
        return registrationBean;
    }
}
  • 直接使用 @Component 注解:这种方式可以让 Spring 自动将过滤器组件化,默认会应用到所有请求路径。
@Component
public class ExampleFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {}
}

Interceptor

什么是 Interceptor

Interceptor 是 Spring MVC 框架的一部分,是位于 org.springframework.web.servlet 包中的 HandlerInterceptor 接口,用于在请求处理之前或之后执行特定逻辑。与 Filter 不同的是,Interceptor 不依赖于 Servlet 容器,它是 Spring 框架独有的。

HandlerInterceptor 接口

/**
 * A HandlerInterceptor gets called before the appropriate HandlerAdapter triggers the execution of the handler itself.
 * This mechanism can be used for a large field of preprocessing aspects, e.g. for authorization checks,
 * or common handler behavior like locale or theme changes.
 * Its main purpose is to allow for factoring out repetitive handler code.
 */

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

接口注释意思大致是说,HandlerInterceptor 是在通过 HandlerAdapter 执行查找到的 handler 之前被调用,这种机制主要目的是为了减少重复代码,用于大量的程序预处理工作,比如授权检查等。

  • preHandle方法:在 controller 方法调用之前,按照 Interceptor 链顺序执行,进行权限检查等请求前处理操作。

    如果该方法返回了 false,那么不仅当前 Interceptor 会终止执行,整个拦截器链都会被终止。

  • postHandle方法:在 controller 方法调用之后返回 ModelAndView 之前执行,与 preHandle 不同的是,postHandle 是按照Interceptor 链逆序执行的。
  • afterCompletion方法:在整个请求完成后调用,通常用于资源清理或日志记录。

执行顺序:

下面我们通过 Spring MVC 在实际分发处理请求时的源码具体看下 Interceptor 的执行情况(源码出自 spring-framework-5.0.x):

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exceptio {
    // 此处用 processedRequest 接收了用户的 request 请求
    HttpServletRequest processedRequest = request;
    // HandlerExecutionChain 局部变量
    HandlerExecutionChain mappedHandler = null;
    // 标记一下是否解析了文件类型的数据,如果有最终需要清理操作
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        // ModelAndView 局部变量
        ModelAndView mv = null;
        // 处理异常局部变量
        Exception dispatchException = null;

        try {
            /**
             * 判断一下是否是文件上传请求。
             * 如果请求是 POST 请求,并且 Context-Type 是以 multipart/ 开头的就认为是文件上传的请求。
             * 需要注意的是,若是这里被认定为文件上传请求,processedRequest 和 request 将不再指向同一对象
             * 这里返回的是 MultipartHttpServletRequest。
             */

            processedRequest = checkMultipart(request);
            // 两个请求不再相同,进行文件上传标记,用于后续清理操作
            multipartRequestParsed = (processedRequest != request);

            /**
             * 向 HandlerMapping 请求查找 HandlerExecutionChain
             * 找到一个处理器,如果没有找到对应的处理类的话,这里通常会返回404。
             */

            mappedHandler = getHandler(processedRequest);
            // 如果没找到对应的处理器,则抛出异常
            // 相信大家都见过 'No mapping for GET /xxxx',就是这里抛出的
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // 根据查找到的 Handler 请求查找能够进行处理的 HandlerAdapter
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // 判断自上次请求后是否有修改,没有修改直接返回响应
            // 如果是GET请求,且内容没有变化的话,就直接返回
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (log.isDebugEnabled()) {
                    log.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                }
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }

            /**
             * <<<<<<<<<这里到了我们本节内容,拦截器的执行了>>>>>>>>>
             * 这里通过 applyPreHandle 方法,按顺序依次执行 HandlerInterceptor 的 preHandle 方法
             * 可以看到,如果任一 HandlerInterceptor 的 preHandle 方法返回了 false, 则整个拦截器连
             * 不再继续进行处理。
             */

            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            /**
             * 通过 HandlerAdapter 执行查找到的 handler
             * 这里真正执行我们 controller 中的方法逻辑,返回一个 ModelAndView
             */

            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            /**
             * 检查是否已经开始处理并发请求,如果并发处理已经开始,那么当前的请求线程就可以返回了,而不会等待异步操              * 作的结果,也就不会再执行拦截器 PostHandle 之类的操作了。
             */

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            // 如果我们没有设置 viewName,就采用默认的,否则采用我们自己的
            applyDefaultViewName(processedRequest, mv);
            // <<<<<<<<< 这里,通过 applyPostHandle 逆序执行 HandlerInterceptor 的 postHandle 方法>>>>>>>>>
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            // As of 4.3, we're processing Errors thrown from handler methods as well,
            // making them available for @ExceptionHandler methods and other scenarios.
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        // 渲染视图填充 Model,如果有异常渲染异常页面
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        // 如果有异常按倒序执行所有 HandlerInterceptor 的 afterCompletion 方法
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        // 如果有异常按倒序执行所有 HandlerInterceptor 的 afterCompletion 方法
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                new NestedServletException("Handler processing failed", err));
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) {
                // 倒序执行所有 HandlerInterceptor 的 afterCompletion 方法
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) {
                // 如果请求包含文件类型的数据则进行相关清理工作
                cleanupMultipart(processedRequest);
            }
        }
    }
}

上述源码中,其实不仅仅是拦截器的执行顺序了,而是 Spring MVC 处理客户端请求的整个过程。如下图,可以很直观的看出拦截器的执行时机与顺序。

图片来源于网络,侵权请联系删除

Interceptor 的配置使用

自定义拦截器,实现 HandlerInterceptor 接口

public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 在这里添加拦截逻辑,例如身份验证或日志记录
        System.out.println("MyInterceptor preHandle: " + request.getRequestURI());
        return true// 返回 true 继续处理请求,返回 false 中断请求
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 在视图渲染之前执行的逻辑
        System.out.println("MyInterceptor postHandle: " + request.getRequestURI());
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 请求完成后的逻辑,比如资源清理
        System.out.println("MyInterceptor afterCompletion: " + request.getRequestURI());
    }
}

实现 WebMvcConfigurer 接口并重写 addInterceptors 方法,将拦截器注册到 Spring MVC 的拦截器链中

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加拦截器,并指定拦截路径模式
        registry.addInterceptor(new MyInterceptor())
                .addPathPatterns("/**"// 拦截所有路径
                .excludePathPatterns("/static/**""/login""/register"); // 排除静态资源和某些 URL
    }
}

AOP(Aspect-Oriented Programming)

什么是 AOP

AOP(Aspect-Oriented Programming),即面向切面编程,是一种编程范式,目的是通过分离横切关注点(如事务管理、日志记录)来提高代码的模块化程度。AOP 允许开发者定义“切面”(Aspects),通过这些切面可以在不改变业务逻辑的情况下增强现有业务功能。

AOP 是一种编程思想,Spring AOP 是 Spring 框架提供的 AOP 实现。

AOP 核心概念

  • 切面(Aspect):一个模块化的特殊类,包含通知和切入点,用来实现特定的横切逻辑。
  • 通知(Advice):在特定连接点(Join Point)执行的动作,例如前置通知(在目标方法调用之前执行的通知)。
  • 连接点(Join Point):程程序执行过程中的一个点,例如方法调用或异常抛出的地方。在 Spring AOP 中,连接点指的是应用程序中所有可能被拦截的方法执行点。
  • 切入点(Pointcut):用于匹配连接点的表达式,决定了哪些连接点会应用切面的通知。Spring AOP 使用 AspectJ 的切入点表达式语言。例如:@Pointcut("execution(* com.example.service..*.*(..))")
  • 引入(Introduction):为现有的类添加新方法或属性的能力。
  • 目标对象(Target Object):目标对象是指被一个或多个切面所通知的对象,也就是需要对其方法调用进行增强的对象。使用 AOP 时,这些对象会被代理,以便可以在它们的方法调用前后插入额外的行为。
  • 代理(Proxy):由 AOP 框架创建的对象,用来实现对目标对象的增强。有两种主要类型的 AOP 代理:JDK 动态代理和 CGLIB 代理。

Spring AOP 如何创建代理:

  • 默认情况下,如果目标对象实现了至少一个接口,Spring AOP 将优先选择 JDK 动态代理。
  • 如果目标对象没有实现任何接口,则会自动切换到 CGLIB 代理。
  • 如果要强制使用 CGLIB 代理,可以在启动类或配置类上添加 @EnableAspectJAutoProxy(proxyTargetClass = true) 注解即可。

Spring AOP 的配置使用

Spring AOP 的使用步骤

  1. 启用 AOP 支持:在启动类或任意配置类上添加 @EnableAspectJAutoProxy 注解。
  2. 定义切面(Aspect):创建一个类,使用 @Aspect 注解来标记这个类为一个切面,并使用 @Component 注解让 Spring 管理这个 Bean。
  3. 指定切入点(Pointcut):使用 @Pointcut 注解加 AspectJ 表达式定义切入点。
  4. 定义通知(Advice):定义一个通知,在特定连接点时执行特定逻辑。

Spring 提供的通知类型有如下几种:

  • @Before(前置通知):前置通知是在目标方法调用 之前 执行的通知。无论目标方法是否抛出异常或正常返回,前置通知都会被执行。
  • @AfterReturning(后置返回通知):后置返回通知是在目标方法 成功返回后 执行的通知。只有当目标方法没有抛出异常时,才会触发该通知。
  • @AfterThrowing(抛出异常通知):抛出异常通知是在目标方法 抛出异常后 执行的通知。它只会在方法抛出指定类型的异常时触发。
  • @Around(环绕通知):环绕通知是最强大的一种通知类型,它包围了目标方法的调用。你可以完全控制方法的执行,包括决定是否继续执行方法以及如何处理返回值或异常。
  • @After(后置最终通知):后置最终通知是在目标方法 完成之后 执行的通知,不论方法是正常结束还是因为异常而终止。

Spring AOP 使用示例

启用 AOP 支持

@SpringBootApplication
@EnableAspectJAutoProxy
public class MyApp {
    public static void main(String[] args) {
        SpringApplication.run(MyApp.classargs);
    }
}

定义切面类

@Slf4j
@Aspect
@Component
public class AllAdviceAspect {

    // 定义切入点,匹配 service 包下的所有方法
    @Pointcut("execution(* com.example.service..*.*(..))")
    public void serviceLayerExecution() {}

    // 前置通知,在方法调用前打印日志
    @Before("serviceLayerExecution()")
    public void logBefore(JoinPoint joinPoint) {
        log.info("Before method execution: {}", joinPoint.getSignature().getName());
    }

    // 后置返回通知,在方法成功返回后打印日志
    @AfterReturning(pointcut = "serviceLayerExecution()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        log.info("Method returned successfully: {}, Result: {}", joinPoint.getSignature().getName(), result);
    }

    // 抛出异常通知,在方法抛出异常后打印日志
    @AfterThrowing(pointcut = "serviceLayerExecution()", throwing = "ex")
    public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
        log.error("An exception was thrown in {}: {}", joinPoint.getSignature().getName(), ex.getMessage());
    }

    // 后置最终通知,在方法完成之后打印日志,无论是否抛出异常
    @After("serviceLayerExecution()")
    public void logAfter(JoinPoint joinPoint) {
        log.info("After method execution: {}", joinPoint.getSignature().getName());
    }

    // 环绕通知,包围方法调用,控制方法执行流程
    @Around("serviceLayerExecution()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        log.info("Starting around advice for method: {}", pjp.getSignature().getName());

        long start = System.currentTimeMillis();
        try {
            // 执行目标方法
            Object result = pjp.proceed();
            log.info("Method {} took {} ms to execute", pjp.getSignature().getName(), System.currentTimeMillis() - start);
            return result;
        } catch (Throwable e) {
            log.error("Error during method execution: {}", e.getMessage());
            throw e; // 重新抛出异常以便后续处理
        } finally {
            log.info("Ending around advice for method: {}", pjp.getSignature().getName());
        }
    }
}

三者之间对比

  • Filter 是 Java Servlet 规范的一部分,它工作在 Servlet 容器层面,是 Servlet 容器级别的,适用于所有进入应用的 HTTP 请求。
  • Interceptor 是 Spring MVC 框架提供的一种请求处理机制,是属于框架级别的。通过 Interceptor 章节的源码可以看出,Interceptor 工作在 Spring MVC 分发处理请求时,而分发请求的类是 DispatcherServlet,它是一个 Servlet,根据 Servlet 规范,Filter 是先于 Servlet 执行的。所以 Filter 要比 Interceptor 优先执行。
  • Spring AOP 是 Spring 框架提供的面向切面编程的支持,允许我们在不改变原有业务逻辑的前提下,集中处理横切关注点。它的执行时机是在特定的连接点,也就是请求已经到了我们 controller 中的某个方法时才会触发,而 Interceptor 在执行查找到的 handler 之前就已经被调用了,所以 Interceptor 要先于 Spring AOP 执行。

执行顺序如下图:

本篇主要基于 SpringBoot 介绍了过滤器、拦截器和 Spring AOP,通过学习其基本知识了解到了它们工作时的执行顺序。实际上,其实无论是过滤器还是拦截器,都可以被视为 AOP 思想的具体实现形式,尽管它们各自工作在不同的层次上。

您的鼓励对我持续创作非常关键,如果本文对您有帮助,请记得点赞、分享、在看哦~~~谢谢!

Java驿站
这里是【Java驿站】,一个Java编程学习与交流平台。
 最新文章