《SpringBoot 3:入门与应用实战》第 9 章 使用 WebMvc 开发进阶 阅读笔记 24

《SpringBoot 3:入门与应用实战》第 9 章 使用 WebMvc 开发进阶 阅读笔记 24

10.1 拦截器

基于普通的 Spring Framework 或 Spring Boot 可以借助 AOP 模块的特性编写切面来进行通用逻辑抽取与增强,而在 Web 项目中用于体现 AOP的机制是拦截器,这个机制与 Servlet 中的过滤器有些类似,需要先对这两者进行区分。

10.1.1 区分拦截器与过滤器

首先需要读者明确:拦截器是 WebMvc 的概念,而过滤器是 Servlet 的概念。Servlet、Filter、Listener 共同称为 Servlet 三大组件,它们都需要依赖 Servlet 的 API;而拦截器则不同,拦截器由 WebMvc 提出,它只是 WebMvc 中设计的一个 API 而已。由此可以总结出第一个区别:拦截器是框架的概念,而过滤器是 Servlet 的概念。

既然概念来源不同,那么它们的拦截范围也就不同。过滤器是在 web.xml 中注册,任何来自 Servlet 容器 (Tomcat) 的请求都会经过这些过滤器;拦截器是框架的概念,而 WebMvc 的核心是一个 DispatcherServlet,所以拦截器实际上是在 DispatcherServlet 接收到请求后才有机会发挥作用的,对于 DispatcherServlet 没有接收到的请求,拦截器无法发挥作用。所以总结出第二个区别:过滤器可以拦截几乎所有请求,而拦截器只能拦截被 DispatcherServlet 接收处理的请求。

继续往下分析,请求的来源不同还会造成一个不同的现象:过滤器由 Servlet 容器创建,与 Spring Framework 的 IOC 没有任何关系,所以无法借助依赖注入机制给过滤器注入属性和组件;而拦截器被 Spring Framework 的 IOC 容器统一管理,它的本质也是一个普通的 Bean,所以拦截器中可以任意注入需要的 bean 对象。因此二者的第三个区别是:拦截器可以借助依赖注入获取所需的 bean 对象,而过滤器无法获取。

10.1.2 拦截器的拦截时机

不同于 Servlet 的过滤器,WebMvc 设计的拦截器切入时机更多,具体的核心接口是 HandlerInterceptor,其中定义了 3 个方法。

可以发现,3 个方法的切入时机由先到后依次如下。

  • preHandle:在执行 Controller 中的 Handler 方法之前触发,可用于编码、权限校验拦截等。
  • postHandle:在执行完 Handler 方法后,跳转页面视图/返回 JSON 数据之前触发。
  • afterCompletion:在完全执行完 Handler 方法后触发,可用于异常处理、性能监控、资源释放等。

注意 postHandle 和 afterCompletion 方法的区别,postHandle 方法的参数上有一个 ModelAndView,证明该时机下还没有确定好数据的封装和视图的返回(页面跳转)​,此时可以对数据和视图进行修改;而后边的 afterCompletion 方法的参数上没有 ModelAndView 对象,而是多出来一个异常对象 Exception,这说明视图跳转或者数据返回已经确定,所以无法干预,另外 Handler 方法的执行过程中可能出现了异常,可以在此处进行异常处理。

10.1.3 使用拦截器

1.定义拦截器

首先定义一个简单的拦截器 DemoInterceptor,使其实现 HandlerInterceptor 接口并实现其中的 3 个方法,暂时不过多编写方法的逻辑,仅打印控制台内容即可。注意一个细节,preHandle 方法的返回值需要设置为true,如果设置为 false,则 Controller 方法以及下面的 postHandle 和afterCompletion 方法都不会执行。

java 复制代码
package com.yangjunbo.springboot.webmvc.exampleh;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
public class DemoInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
Object handler)throws Exception {
        System.out.println("DemoInterceptor preHandle ......");
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("DemoInterceptor postHandle ......");
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {
        System.out.println("DemoInterceptor afterCompletion ......");
    }
}

2.配置拦截器

如果仅将拦截器注册到 IOC 容器,则该拦截器不会工作,原因是没有配置拦截器的拦截请求路径规则,为此需要编写一个配置类来配置拦截器。配置拦截器的方法是 WebMvcConfigurer 接口中的 addInterceptors 方法,在调用时需先声明要注册的拦截器,再指定该拦截器匹配的请求路径。除了使用 addPathPatterns 方法声明匹配的路径规则,还可以使用 excludePathPatterns 方法排除不需要匹配的路径。

java 复制代码
package com.yangjunbo.springboot.webmvc.exampleh;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfigurer implements WebMvcConfigurer {
    
    @Autowired
    private DemoInterceptor demoInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(demoInterceptor).addPathPatterns("/department/**");
    }

}

配置完毕后启动应用,在浏览器中访问 /department/list 时可以发现控制台上可以打印 DemoInterceptor 的日志,而访问 /user/list 时,拦截器不会向控制台打印任何信息,说明拦截器配置成功。

10.1.4 多个拦截器的执行机制

1.编写两个拦截器

直接将代码中的 DemoInterceptor 复制两份,分别重命名为 DemoInterceptor1 和 DemoInterceptor2,并在内部的控制台打印中添加 1 和 2 的后缀予以区分。

2.配置拦截器

拦截器的配置是有先后顺序的,先配置的拦截器会在实际执行时优先执行,代码中的配置效果就是 DemoInterceptor1 在前,DemoInterceptor2在后。有的读者可能会联想到维护拦截器的结构应该是一个 List,毕竟 List 本身是有顺序的。

java 复制代码
package com.yangjunbo.springboot.webmvc.exampleh;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfigurer implements WebMvcConfigurer {
    
//    @Autowired
//    private DemoInterceptor demoInterceptor;
//
//    @Override
//    public void addInterceptors(InterceptorRegistry registry) {
//        registry.addInterceptor(demoInterceptor).addPathPatterns("/department/**");
//    }

    @Autowired
    private DemoInterceptor1 demoInterceptor1;
    @Autowired
    private DemoInterceptor2 demoInterceptor2;

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

}

3.拦截器执行机制

下面验证拦截器的执行顺序是否与预想的一样,重启应用后访问 /department/list 路径,观察控制台中打印的内容:

可以发现虽然前置拦截 preHandle 的逻辑的确与预想的一样,DemoInterceptor1 在前,DemoInterceptor2 在后,但是 postHandle 和afterCompletion 方法的执行顺序却是 DemoInterceptor2 在前,DemoInterceptor1 在后。

其实这个执行结果就是 "回旋镖" 式的体现,preHandle 方法是顺序执行,postHandle 和 afterCompletion 方法均是逆序执行。根据测试的结果可以得到一张执行流程图。

4.测试 preHandle

在10.1.3节中提到了DemoInterceptor中 preHandle 方法的一个细节:如果该方法返回 false 则不会向下执行,包括 Controller 和拦截器的后续逻辑都会被跳过。下面举两个例子来演示 preHandle 方法返回 true 和 false 引发的效果。

首先将 DemoInterceptor1 的 preHandle 返回值改为 false,重启应用后访问 /department/list 路径,可以发现浏览器中变成了空白,与此同时控制台中只打印了一行 DemoInterceptor preHandle 1,这就印证了上面的结论,由于第一个拦截器的 preHandle 方法返回 false,后续的所有方法都不会继续执行,Controller 中的 Handler 方法自然也被跳过,视图跳转也就无法生效。

接下来把 DemoInterceptor1 的 preHandle 返回值改为 true,把 DemoInterceptor2 的 preHandle 返回值改为 false,重新测试后发现浏览器中依然得到空白页面,但是这次控制台打印了 3 行信息。

引发这个结果的原因是 DemoInterceptor2 在 DemoInterceptor1 之后执行,而 DemoInterceptor1 的返回值为 true,导致 DemoInterceptor2 返回 false 后 DemoInterceptor1 仍然能够调用 afterCompletion 成功,但两个拦截器的 postHandle 方法都没有调用。

下面总结出最终结论。

  • 只有 preHandle 方法返回 true 时,afterCompletion 方法才会被调用。
  • 只有所有preHandle 方法的返回值全部为 true 时,Controller 方法和 postHandle 方法才会被调用。
相关推荐
摇滚侠24 分钟前
《SpringBoot 3:入门与应用实战》第 9 章 使用 WebMvc 开发应用 阅读笔记 22
javascript·spring boot·笔记
元界metalite30 分钟前
SpringBoot开发企业后台-操作日志记录的最佳实践
后端
用户2986985301430 分钟前
PDF 转纯文本(TXT)免费攻略:轻松提取文字内容
人工智能·后端·c#
程序边界31 分钟前
从RAG落地到MCP实战:向量数据库融合架构的工程化体验
后端
Rain的Java大神之路42 分钟前
如何保证接口幂等
java·经验分享·后端·面试·架构
木木爱研究42 分钟前
elpis-里程碑四-基于Vue完成动态组件库建设
前端·后端
一水行42 分钟前
博客评论系统搭建回顾
javascript·后端
Jesse_EC44 分钟前
一个看不见的字符——CRLF 如何让我的 Docker 初始化脚本"静默失踪"
java·后端
一条破秋裤1 小时前
STM32 学习笔记:OLED 调试工具与 Keil 在线调试
笔记·stm32·学习