开篇图书系统那些年我们手动“锁门”的日子还记得我们之前一起做的图书管理系统吗从登录、增删改查到分页、批量删除咱们一步步把它搭起来了。功能齐全页面漂亮但有一个问题一直让人心里不踏实只要知道网址不登录也能访问图书列表甚至还能添加、修改、删除图书。于是我们赶紧给系统加上了“强制登录”功能在每个接口里手动检查 Sessionif(session.getAttribute(session_user_key)null){returnResult.unlogin();}UserInfouserInfo(UserInfo)session.getAttribute(session_user_key);if(userInfonull||userInfo.getId()0||.equals(userInfo.getUserName())){returnResult.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(拦截器处理者)接口并重写它的三个方法。这个类就是我们的“保安”我们要在这里告诉他该干什么活。importlombok.extern.slf4j.Slf4j;importorg.springframework.stereotype.Component;importorg.springframework.web.servlet.HandlerInterceptor;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjavax.servlet.http.HttpSession;Slf4jComponentpublicclassLoginInterceptorimplementsHandlerInterceptor{OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{// 从 Session 中获取用户信息不创建新的 SessionHttpSessionsessionrequest.getSession(false);if(session!nullsession.getAttribute(session_user_key)!null){log.info(用户已登录放行);returntrue;}log.warn(用户未登录拦截请求);response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);// 401returnfalse;}OverridepublicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{// 这里可以修改 Controller 返回的数据比如给返回结果统一加个字段// 但现在我们不需要先留着}OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{// 这里可以记录请求耗时、清理资源// 但现在我们不需要先留着}}我们在拦截器中验证之后就不需要在控制层接口中验证了注意我们重点关注preHandle方法因为它是在请求进入 Controller 之前执行的。我们在这里检查 Session 里有没有用户信息有就返回true放行没有就返回false拦截并返回 401 状态码。这就是保安的“查证”工作。那么我们定义了保安告诉他干什么活而这些活在谁身上干他不知道那么我们怎么告诉他得签合同呀合同上有说明。也就是说呀你保安会对进学校的人进行拦截操作基本流程都是刷卡检查但是是针对每个人吗肯定不是呀比如校长你拦他干什么对吧。2.2 第二步签合同上岗注册拦截器保安定义好了不能光站在那儿还得让他正式上岗。怎么上岗签合同。就像学校跟保安公司签合同合同里写明保安在哪儿执勤、拦截什么人、放行什么人等等。签了合同保安才能名正言顺地干活。不同的地方合同内容不一样但流程都是“先定义再注册”。在我们代码里这个“签合同”的过程就是把写好的拦截器注册到一个配置类中这个配置类要实现WebMvcConfigurer接口Spring 给我们规定好的“合同模板”。你只需要在config包下新建一个类把下面的代码复制进去改改拦截的路径就行了不用纠结原理。在里面新建一个类webconfigpackagecom.zhongge.config;importcom.zhongge.interceptor.LoginInterceptor;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.servlet.config.annotation.InterceptorRegistry;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;/** * ClassName WebConfig * Description TODO 注册拦截器类 * Author 笨忠 * Date 2026-04-01 20:20 * Version 1.0 */Configuration//将这个类交给Spring管理publicclassWebConfigimplementsWebMvcConfigurer{AutowiredprivateLoginInterceptorloginInterceptor;// 你定义的保安OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){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方法。publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){HttpSessionsessionrequest.getSession(false);if(session!nullsession.getAttribute(session_user_key)!null){returntrue;// 有证放行}response.setStatus(401);// 没证返回 401 状态码returnfalse;// 拦截不让进}如果某个保安说“不行”我就立刻被拦下后面的保安和 Controller 都见不到直接被赶回浏览器。浏览器收到 401 状态码就会跳转到登录页。如果所有保安都放行我才能继续往前走。解释返回false和返回true的作用3.3 第二关见到 Controller办理业务穿过保安队伍我终于见到了 Controller 大哥。他帮我执行真正的业务代码比如从数据库查图书列表然后把数据打包好交给我。RequestMapping(/getListByPage)publicResultgetListByPage(PageRequestpageRequest){PageResultBookInfopageResultbookService.getListByPage(pageRequest);returnResult.success(pageResult);}Controller 执行完后我就带着数据往回走。3.4 第三关返回路上保安再次出现往回走时我又遇到了保安们。这次他们执行postHandle可以在我返回之前做点事情。最重要的是他们可以修改我手里的数据就像银行柜员在给你回执单之前可以加盖公章、添加备注。这就是为什么postHandle能修改 Controller 返回的结果。publicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView){log.info(业务处理完毕准备返回数据);// 这里可以修改 modelAndView改变最终返回的内容}然后数据被发送到浏览器。3.5 第四关客人走后保安打扫卫生最后当整个响应已经发送给浏览器后保安们执行afterCompletion可以做最后的清理工作比如关闭资源、记录总耗时。这一步无论前面是否出错都会执行。publicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){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 校验代码删掉了。改造前图书列表接口RequestMapping(/getListByPage)publicResultgetListByPage(PageRequestpageRequest,HttpSessionsession){if(session.getAttribute(Constants.SESSION_USER_KEY)null){returnResult.unlogin();}UserInfouserInfo(UserInfo)session.getAttribute(Constants.SESSION_USER_KEY);if(userInfonull||userInfo.getId()0||.equals(userInfo.getUserName())){returnResult.unlogin();}PageResultBookInfopageResultbookService.getListByPage(pageRequest);returnResult.success(pageResult);}改造后RequestMapping(/getListByPage)publicResultgetListByPage(PageRequestpageRequest){PageResultBookInfopageResultbookService.getListByPage(pageRequest);returnResult.success(pageResult);}六、前端配合统一处理 401 状态码保安拦截时返回 401 状态码前端可以统一处理比如用 jQuery 的全局 AJAX 配置$.ajaxSetup({statusCode:{401:function(){location.hreflogin.html;}}});或者单独弄error:function(){//如果请求连接失败就会走这里console.error(加载图书数据失败);alert(未登录请先登录);// 没登录直接撵到登录页location.hreflogin.html;return;}这样任何请求被拦截都会自动跳转到登录页用户完全无感知。你不需要在每个 AJAX 回调里重复写跳转逻辑。七、源码浅析Spring 是如何调用保安的可能有人会好奇Spring 到底是怎么让保安们按顺序工作的我们来简单看一下核心类DispatcherServlet的doDispatch方法简化版。看不懂也没关系有个印象就行。protectedvoiddoDispatch(HttpServletRequestrequest,HttpServletResponseresponse){// 1. 拿到执行链里面包含了拦截器列表HandlerExecutionChainmappedHandlergetHandler(request);// 2. 执行所有拦截器的 preHandleif(!mappedHandler.applyPreHandle(request,response)){return;// 如果有一个拦截器返回 false就停止后面的都不执行}// 3. 执行 Controller 方法HandlerAdapterhagetHandlerAdapter(mappedHandler.getHandler());ModelAndViewmvha.handle(request,response,mappedHandler.getHandler());// 4. 执行所有拦截器的 postHandlemappedHandler.applyPostHandle(request,response,mv);// 5. 处理视图渲染processDispatchResult(request,response,mappedHandler,mv,dispatchException);}在applyPreHandle中会依次调用每个拦截器的preHandlebooleanapplyPreHandle(HttpServletRequestrequest,HttpServletResponseresponse){for(inti0;ithis.interceptorList.size();i){HandlerInterceptorinterceptorthis.interceptorList.get(i);if(!interceptor.preHandle(request,response,this.handler)){triggerAfterCompletion(request,response,null);returnfalse;}}returntrue;}所以请求的路径是拦截器 preHandle → Controller → 拦截器 postHandle → 返回。如果某个 preHandle 返回 false后面的所有步骤都不会执行。这正是我们想要的登录校验效果。1、 Tomcat里面可以部署多个web项目。我们访问项目的路径是 context path/servlet pathcontext path 上下文路径它用于决定项目比如它是用来区分你是博客项目还是我们的图书管理系统项目servlet path他是决定当前项目中的路径的。2、观察日志可知context path 是/原因是因为当前的Tomcat它只部署了一个项目3、访问我们的登录页面然后再观察日志我们本次的主角就是dispatcherServlet4、 看dispatcherServlet源码看下述的继承体系5、Servlet的生命周期服务器启动的时候执行init的方法[只执行一次]用接口的时候走service方法[可能执行很多遍]关闭服务的时候执行destroy方法[只执行一次]。我们的DispatcherServlet中没有Iinit方法我的初始化方法在哪呢在我们的父类中你会发现这个方法是空的然后由它的子类来实现这样的设计模式叫做模板方法模式。就是说父类定义了没有实现由子类来实现。然后你看他子类实现这个方法的时候那有一些日志这些日志不就是我们控制台上的日志吗阅读源码的时候ctrlalt←退回方法。接下来看Service方法DispatcherServlet中的Service方法接下来我们核心看doDispatch这个方法protectedvoiddoDispatch(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{HttpServletRequestprocessedRequestrequest;HandlerExecutionChainmappedHandlernull;booleanmultipartRequestParsedfalse;WebAsyncManagerasyncManagerWebAsyncUtils.getAsyncManager(request);try{ModelAndViewmvnull;ExceptiondispatchExceptionnull;try{processedRequestcheckMultipart(request);multipartRequestParsed(processedRequest!request);// Determine handler for the current request.mappedHandlergetHandler(processedRequest);if(mappedHandlernull){noHandlerFound(processedRequest,response);return;}if(!mappedHandler.applyPreHandle(processedRequest,response)){return;}// Determine handler adapter and invoke the handler.HandlerAdapterhagetHandlerAdapter(mappedHandler.getHandler());mvha.handle(processedRequest,response,mappedHandler.getHandler());if(asyncManager.isConcurrentHandlingStarted()){return;}applyDefaultViewName(processedRequest,mv);mappedHandler.applyPostHandle(processedRequest,response,mv);}catch(Exceptionex){dispatchExceptionex;}catch(Throwableerr){// As of 4.3, were processing Errors thrown from handler methods as well,// making them available for ExceptionHandler methods and other scenarios.dispatchExceptionnewServletException(Handler dispatch failed: err,err);}processDispatchResult(processedRequest,response,mappedHandler,mv,dispatchException);}catch(Exceptionex){triggerAfterCompletion(processedRequest,response,mappedHandler,ex);}catch(Throwableerr){triggerAfterCompletion(processedRequest,response,mappedHandler,newServletException(Handler processing failed: err,err));}finally{if(asyncManager.isConcurrentHandlingStarted()){// Instead of postHandle and afterCompletionif(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和排除路径excludePathPatternsexclude翻译为不包括就像给保安规定执勤范围和特殊通道。拦截器按顺序执行一旦preHandle返回false请求立即终止。用拦截器改造图书系统的强制登录删除了大量重复代码前端用全局配置处理 401 状态码。现在我们的图书系统终于有了一个专业的保安再也不用自己一个个锁门了十、伏笔统一返回格式和统一异常处理虽然拦截器解决了登录校验的统一问题但你可能还会问我们每个接口返回的数据格式不统一怎么办异常处理也要每个方法写一遍吗别急接下来我们就学习 Spring Boot 的统一返回格式和统一异常处理让我们的代码更规范、更优雅。敬请期待下一期最后老铁们如果你觉得这篇文章对你有帮助别忘了点赞⭐ 收藏 关注各位老铁的支持~~