开篇:图书系统那些年,我们手动"锁门"的日子
还记得我们之前一起做的图书管理系统吗?从登录、增删改查到分页、批量删除,咱们一步步把它搭起来了。功能齐全,页面漂亮,但有一个问题一直让人心里不踏实:只要知道网址,不登录也能访问图书列表,甚至还能添加、修改、删除图书。
于是我们赶紧给系统加上了"强制登录"功能,在每个接口里手动检查 Session:

java
if (session.getAttribute("session_user_key") == null) {
return Result.unlogin();
}
UserInfo userInfo = (UserInfo) session.getAttribute("session_user_key");
if (userInfo == null || userInfo.getId() < 0 || "".equals(userInfo.getUserName())) {
return Result.unlogin();
}
结果呢?每个接口都要写这么一大段。如果有几十个接口,岂不是要复制粘贴几十遍?要是以后登录校验规则变了,又得满世界改。这就像家里的每一扇门都要单独上锁,每次出门都得一个个检查,累不累?
有没有一种办法,只在门口设一个保安,让他检查所有人的证件,有证就放行,没证就拦住?这就是我们今天要学的------Spring Boot 拦截器。
一、保安来了:什么是拦截器?

拦截器是 Spring 框架提供的一个功能,它可以在请求到达 Controller 之前 、Controller 执行之后 、整个请求结束之后,执行一些预先定义的代码。
用生活例子来理解:你去银行办业务。
- 进门前(preHandle) :保安会检查你有没有带身份证,没带?对不起,不能进去。这一步可以直接拦下请求。
- 办理业务(Controller):柜员帮你处理核心业务,比如存钱、转账。
- 业务办完,还没给你回执单(postHandle) :柜员在给你的回执单上加盖公章、添加备注 。这一步能修改 Controller 返回的结果,然后再把最终结果给你。
- 你彻底离开银行(afterCompletion) :大堂经理清理窗口、记录日志。无论前面是否出错,这一步最后一定执行。
拦截器就是这样的"保安",它在请求处理的不同阶段介入,完成一些通用任务。三个方法各司其职:
- preHandle:能不能进?(有权拒绝)
- postHandle:返回前改结果(能修改返回值)
- afterCompletion:最后扫尾(一定执行)
二、两步走:定义保安 + 签合同上岗
使用拦截器只需要做两件事:定义 和注册。
什么意思呢?假如我学校要找保安,那么首先你得有保安呀,你得有这个人呀,这个就叫做定义,那么定义完了之后就直接不管了吗?肯定不是呀,你得签合同上岗呀,这个就叫做注册,所以拦截器其实就很简单,就两个操作:定义+注册。
1. 定义拦截器 = 写好保安的工作手册
你写一个类实现 HandlerInterceptor,重写那三个方法:
- 进门查什么
- 办完改什么
- 走后清理什么
这就叫:告诉保安要干什么。
2. 注册拦截器 = 给保安安排岗位
在配置类里加 addInterceptors,写:
- 哪些路径要拦(/、/admin/)
- 哪些路径放行(/login、/static)
这就叫:告诉保安,你的技能用在谁身上。
一句话总结
- 定义:造保安、教保安干活
- 注册:安排保安站哪个门、查哪些人
2.1 第一步:定义保安(写拦截器类)
我们通常将拦截器类(保安这个人)放在interceptor包下:

然后比如我们将他命名为:LoginInterceptor,然后让它实现 HandlerInterceptor (拦截器处理者)接口,并重写它的三个方法。这个类就是我们的"保安",我们要在这里告诉他该干什么活。
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从 Session 中获取用户信息(不创建新的 Session)
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("session_user_key") != null) {
log.info("用户已登录,放行");
return true;
}
log.warn("用户未登录,拦截请求");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 这里可以修改 Controller 返回的数据(比如给返回结果统一加个字段)
// 但现在我们不需要,先留着
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 这里可以记录请求耗时、清理资源
// 但现在我们不需要,先留着
}
}
我们在拦截器中验证之后就不需要在控制层接口中验证了

注意 :我们重点关注 preHandle 方法,因为它是在请求进入 Controller 之前执行的。我们在这里检查 Session 里有没有用户信息,有就返回 true 放行,没有就返回 false 拦截,并返回 401 状态码。这就是保安的"查证"工作。
那么我们定义了保安告诉他干什么活,而这些活在谁身上干?他不知道,那么我们怎么告诉他?得签合同呀,合同上有说明。也就是说呀,你保安会对进学校的人进行拦截操作,基本流程都是刷卡检查,但是是针对每个人吗?肯定不是呀,比如校长你拦他干什么,对吧。
2.2 第二步:签合同上岗(注册拦截器)
保安定义好了,不能光站在那儿,还得让他正式上岗。怎么上岗?签合同。就像学校跟保安公司签合同,合同里写明:保安在哪儿执勤、拦截什么人、放行什么人等等。签了合同,保安才能名正言顺地干活。不同的地方合同内容不一样,但流程都是"先定义,再注册"。
在我们代码里,这个"签合同"的过程就是把写好的拦截器注册到一个配置类 中,这个配置类要实现 WebMvcConfigurer 接口(Spring 给我们规定好的"合同模板")。你只需要在 config 包下新建一个类,把下面的代码复制进去,改改拦截的路径就行了,不用纠结原理。

在里面新建一个类webconfig

java
package com.zhongge.config;
import com.zhongge.interceptor.LoginInterceptor;
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;
/**
* @ClassName WebConfig
* @Description TODO 注册拦截器类
* @Author 笨忠
* @Date 2026-04-01 20:20
* @Version 1.0
*/
@Configuration//将这个类交给Spring管理
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor; // 你定义的保安
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor) // 让这个保安上岗(添加拦截器)
.addPathPatterns("/**") // 让他守所有门
.excludePathPatterns("/user/login") // 但登录这个门例外
.excludePathPatterns("/**/*.js") // 静态资源也不拦
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.png")
.excludePathPatterns("/**/*.html"); // 所有HTML页面都放行
}
}
addPathPatterns("/**")就是告诉保安:你要拦截所有请求。/**是通配符,匹配所有路径。excludePathPatterns("/user/login")是告诉保安:这个请求不要拦截,因为登录时还没有 Session,不能拦。- 静态资源(JS、CSS、图片)也不需要拦截,所以也排除。
瞧,就像签了合同,保安正式上岗!我们再也不用在每个 Controller 里手动写 Session 校验了。
那么此时我们先不返回状态码,然后看拦截之后返回的状态码是什么?

使用postman请求图书列表之后 看他的响应效果:

那么我们再使用fiddler抓一下包 看他返回的状态码:

我们返回的状态码是200呀,那么此时你都被拦截了,代表你这个请求没有连接上,此时为了方便前端判断,我们应该手动将状态码设置为401:


三、请求的"闯关之旅":从进门到出门,保安都在做什么?
让我们把自己想象成一个请求,从浏览器出发,看看我们到底经历了什么。
3.1 出发:我(请求)要去找图书数据
我在浏览器里被创建,目标地址是 /book/getListByPage。我带着你的指令,飞向服务器。服务器里有个总调度员,叫 DispatcherServlet,它负责接待所有请求。
3.2 第一关:保安们排成一排,检查我的证件
我一到服务器,总调度员就把我交给了一排保安。保安们按顺序检查我,每个保安都会执行 preHandle 方法。
java
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("session_user_key") != null) {
return true; // 有证,放行
}
response.setStatus(401); // 没证,返回 401 状态码
return false; // 拦截,不让进
}
如果某个保安说"不行",我就立刻被拦下,后面的保安和 Controller 都见不到,直接被赶回浏览器。浏览器收到 401 状态码,就会跳转到登录页。
如果所有保安都放行,我才能继续往前走。
解释返回false和返回true的作用:

3.3 第二关:见到 Controller,办理业务
穿过保安队伍,我终于见到了 Controller 大哥。他帮我执行真正的业务代码,比如从数据库查图书列表,然后把数据打包好交给我。
java
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest) {
PageResult<BookInfo> pageResult = bookService.getListByPage(pageRequest);
return Result.success(pageResult);
}
Controller 执行完后,我就带着数据往回走。
3.4 第三关:返回路上,保安再次出现
往回走时,我又遇到了保安们。这次他们执行 postHandle,可以在我返回之前做点事情。最重要的是,他们可以修改我手里的数据 ,就像银行柜员在给你回执单之前,可以加盖公章、添加备注。这就是为什么 postHandle 能修改 Controller 返回的结果。
java
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
log.info("业务处理完毕,准备返回数据");
// 这里可以修改 modelAndView,改变最终返回的内容
}
然后,数据被发送到浏览器。
3.5 第四关:客人走后,保安打扫卫生
最后,当整个响应已经发送给浏览器后,保安们执行 afterCompletion,可以做最后的清理工作,比如关闭资源、记录总耗时。这一步无论前面是否出错,都会执行。
java
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
log.info("整个请求处理完毕,收工");
}
就这样,我的"闯关之旅"结束了。
四、保安的"执勤范围":拦截路径详解
刚才我们用了 /**,它代表拦截所有请求。Spring 支持多种路径匹配规则,你可以根据需要灵活配置。这就好比保安的合同里规定了他负责哪些区域:
| 路径 | 含义 | 例子 |
|---|---|---|
/* |
一级路径 | 匹配 /user、/book,不匹配 /user/login |
/** |
任意级路径 | 匹配 /user、/user/login、/book/addBook |
/book/* |
/book 下的一级路径 | 匹配 /book/addBook,不匹配 /book/addBook/1 |
/book/** |
/book 下的任意级路径 | 匹配 /book、/book/addBook、/book/addBook/1 |
比如,你只想拦截 /book 开头的请求,可以写成 addPathPatterns("/book/**")。如果你只想拦截一级路径,可以写成 addPathPatterns("/*")。
排除路径也一样灵活。你可以把登录接口、注册接口、静态资源等都排除掉,就像保安可以放行一些特殊人员。
五、改造图书系统:删除重复代码,清爽回归
现在,我们可以把之前图书系统里所有接口的 Session 校验代码删掉了。
改造前(图书列表接口):
java
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest, HttpSession session) {
if (session.getAttribute(Constants.SESSION_USER_KEY) == null) {
return Result.unlogin();
}
UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
if (userInfo == null || userInfo.getId() < 0 || "".equals(userInfo.getUserName())) {
return Result.unlogin();
}
PageResult<BookInfo> pageResult = bookService.getListByPage(pageRequest);
return Result.success(pageResult);
}
改造后:
java
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest) {
PageResult<BookInfo> pageResult = bookService.getListByPage(pageRequest);
return Result.success(pageResult);
}

六、前端配合:统一处理 401 状态码
保安拦截时返回 401 状态码,前端可以统一处理,比如用 jQuery 的全局 AJAX 配置:
javascript
$.ajaxSetup({
statusCode: {
401: function() {
location.href = "login.html";
}
}
});
或者单独弄
js
error: function () {
//如果请求连接失败就会走这里
console.error("加载图书数据失败");
alert("未登录,请先登录!");
// 没登录,直接撵到登录页
location.href = "login.html";
return;
}

这样,任何请求被拦截,都会自动跳转到登录页,用户完全无感知。你不需要在每个 AJAX 回调里重复写跳转逻辑。
七、源码浅析:Spring 是如何调用保安的?
可能有人会好奇,Spring 到底是怎么让保安们按顺序工作的?我们来简单看一下核心类 DispatcherServlet 的 doDispatch 方法(简化版)。看不懂也没关系,有个印象就行。
java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
// 1. 拿到执行链,里面包含了拦截器列表
HandlerExecutionChain mappedHandler = getHandler(request);
// 2. 执行所有拦截器的 preHandle
if (!mappedHandler.applyPreHandle(request, response)) {
return; // 如果有一个拦截器返回 false,就停止,后面的都不执行
}
// 3. 执行 Controller 方法
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());
// 4. 执行所有拦截器的 postHandle
mappedHandler.applyPostHandle(request, response, mv);
// 5. 处理视图渲染
processDispatchResult(request, response, mappedHandler, mv, dispatchException);
}
在 applyPreHandle 中,会依次调用每个拦截器的 preHandle:
java
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) {
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;
}
}
return true;
}
所以,请求的路径是:拦截器 preHandle → Controller → 拦截器 postHandle → 返回。如果某个 preHandle 返回 false,后面的所有步骤都不会执行。这正是我们想要的登录校验效果。
1、 Tomcat里面可以部署多个web项目。我们访问项目的路径是: context path/servlet path
- context path 上下文路径,它用于决定项目,比如它是用来区分你是博客项目,还是我们的图书管理系统项目?
- servlet path他是决定当前项目中的路径的。
2、观察日志可知:context path 是 /,原因是因为当前的Tomcat它只部署了一个项目

3、访问我们的登录页面,然后再观察日志:

我们本次的主角就是dispatcherServlet
4、 看dispatcherServlet源码

看下述的继承体系






5、Servlet的生命周期

服务器启动的时候执行init的方法[只执行一次],用接口的时候走service方法[可能执行很多遍],关闭服务的时候执行destroy方法[只执行一次]。


我们的DispatcherServlet中没有Iinit方法,我的初始化方法在哪呢?在我们的父类中:

你会发现这个方法是空的,然后由它的子类来实现,这样的设计模式叫做模板方法模式。

就是说父类定义了,没有实现,由子类来实现。
然后你看他子类实现这个方法的时候,那有一些日志,这些日志不就是我们控制台上的日志吗?


阅读源码的时候ctrl+alt+←退回方法。

接下来看Service方法


DispatcherServlet中的Service方法:


接下来我们核心看doDispatch这个方法
java
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);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Determine handler adapter and invoke the handler.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
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 ServletException("Handler dispatch failed: " + err, err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new ServletException("Handler processing failed: " + err, err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
asyncManager.setMultipartRequestParsed(multipartRequestParsed);
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
cleanupMultipart(processedRequest);
}
}
}
}
通过debug的方式去看

请求:

处理器的链接执行了这个就执行下一个。







执行拦截器,我们就是在下述这里执行的,由我们的适配器来执行目标方法。

这个就是我们拦截器的执行原理逻辑。
八、小彩蛋:适配器模式------保安与不同客户的沟通
接下来我们就看下面这行代码,它设计了一个的设计模式叫做适配器模式:

在看源码时,你可能注意到 HandlerAdapter 这个词。它其实是适配器模式的一个应用。简单说,就是让不同种类的 Controller 都能被统一处理。
比如,有的 Controller 是普通的 @Controller,有的可能是实现 HttpRequestHandler 接口的类,有的可能是 Servlet。它们长得不一样,但 Spring 想用统一的方式调用它们。HandlerAdapter 就像个万能转接头,把不同的接口转成统一的调用方式。
生活中,适配器也很常见:手机充电器、插头转换器、网线转接头......都是让不兼容的东西能一起工作。
九、总结:有了保安,代码更清爽
今天,我们跟随一个请求的视角,完整了解了 Spring Boot 拦截器的工作流程。我们用保安类比拦截器,用银行办业务类比请求处理过程,轻松理解了拦截器的概念、使用步骤和执行流程。
我们学会了:
- 拦截器可以在请求进入 Controller 之前、之后、完成后介入。
- 使用拦截器只需要两步:定义 (写一个类实现 HandlerInterceptor)和注册(在配置类中注册并配置拦截路径)。
- 配置拦截路径
addPathPatterns和排除路径excludePathPatterns,exclude翻译为不包括,就像给保安规定执勤范围和特殊通道。 - 拦截器按顺序执行,一旦
preHandle返回false,请求立即终止。 - 用拦截器改造图书系统的强制登录,删除了大量重复代码,前端用全局配置处理 401 状态码。
现在,我们的图书系统终于有了一个专业的保安,再也不用自己一个个锁门了!
十、伏笔:统一返回格式和统一异常处理
虽然拦截器解决了登录校验的统一问题,但你可能还会问:我们每个接口返回的数据格式不统一怎么办?异常处理也要每个方法写一遍吗?
别急,接下来我们就学习 Spring Boot 的统一返回格式 和统一异常处理,让我们的代码更规范、更优雅。敬请期待下一期!
最后,老铁们,如果你觉得这篇文章对你有帮助,别忘了👍点赞⭐ 收藏👀 关注,🦀🦀各位老铁的支持~~