SpringBoot中的拦截器使用详解
拦截器 还记得我们之前的图书管理系统吗?当时我们实现了强制登录的功能。原理其实很直观:用户登录成功后,后端就把用户信息存到 Session 里,然后程序根据 Session 来判断用户是否登录。听起来挺简单的,对吧?但实际操作起来,你可能会觉得有点麻烦: • 每个接口的处理逻辑都得改一遍 • 每个接
拦截器
还记得我们之前的图书管理系统吗?当时我们实现了强制登录的功能。原理其实很直观:用户登录成功后,后端就把用户信息存到 Session 里,然后程序根据 Session 来判断用户是否登录。听起来挺简单的,对吧?但实际操作起来,你可能会觉得有点麻烦:
• 每个接口的处理逻辑都得改一遍
• 每个接口的返回结果也得跟着动
• 接口定义一旦改了,前端代码也得跟着改
那么,有没有一种更省事的办法,能统一拦截所有请求,然后统一去做 Session 校验呢?
这个时候,就轮到我们今天的主角登场了——拦截器。
拦截器是 Spring 框架提供的一个核心功能。说白了,它的作用就是拦截用户的请求,在指定方法执行前后,根据业务需要,执行我们预先设定好的代码。

拦截器的使用
在 Spring Boot 里使用拦截器,其实就两步:
1. 定义一个拦截器;
2. 注册并配置这个拦截器。
自定义拦截器
说到定义,首先要做的,是实现 HandlerInterceptor 接口,并重写它里面的所有方法。下面就是一个典型的例子——LoginInterceptor:
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor 目标方法执行前执行");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("LoginInterceptor 目标方法执行后执行");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("LoginInterceptor 视图渲染完毕后执行,最后执行");
}
}
这三个方法,各有各的用处:
•
preHandle():目标方法执行前执行。返回true表示可以继续执行后续操作;返回false就表示中断后续操作。•
postHandle():目标方法执行后执行。•
afterCompletion():视图渲染完毕后执行,最后执行。
注册配置拦截器
定义好拦截器之后,还得把它注册到 Spring 的配置里。这就需要实现 WebMvcConfigurer 接口,并重写它的 addInterceptors 方法。看下面这个 WebConfig:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
;
}
}
启动服务,登录成功后,发送个根据图书 id 查询图书的请求,再观察一下后端的日志:

你看,preHandle 方法执行完之后,请求就被放行了,然后开始执行目标方法。目标方法执行完毕,再依次执行 postHandle 和 afterCompletion 方法。
那如果我们把 preHandle 方法的返回值改成 false 呢?再观察一下日志:


看到没?日志里只剩下“目标方法执行前执行”这一条了。原因很清楚——preHandle 方法直接把所有请求都给拦截了。
拦截器详解
拦截器路径
拦截路径,就是说,我们定义的拦截器到底对哪些请求生效。在注册配置拦截器的时候,通过 addPathPatterns() 方法来指定要拦截哪些请求。当然,如果想放行某些请求,也可以通过 excludePathPatterns() 来指定不拦截哪些。
上面代码里,我们配置的是 /**,意思就是拦截所有请求。
举个例子,用户登录校验这个功能,我们希望除了登录接口之外,对其他所有路径都生效。那就可以这样写:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login")
;
}
}
除了 /** 这种拦截所有资源的写法,还有一些常见的拦截路径设置:

拦截器执行流程
正常情况下,一个请求的调用顺序是这样的:

但有了拦截器之后,情况就变了。在调用 Controller 之前,请求会先被拦截器拦下来,进行相应的业务处理。执行流程就变成了下面这样:

1. 添加拦截器后,执行 Controller 方法之前,请求先被拦截器拦截住。执行
preHandle()方法,这个方法需要返回一个布尔值。如果返回true,表示放行本次操作,继续访问 Controller 中的方法;如果返回false,就不会放行,Controller 中的方法也不会执行。2. Controller 中的方法执行完毕后,再回来执行
postHandle()和afterCompletion()方法。全部执行完毕,最终给浏览器响应数据。
修改之前登录校验功能
现在,我们来看看怎么用拦截器来改造之前那个登录校验功能。
首先,定义拦截器。从 Session 中获取用户信息,如果 Session 中不存在,就返回 false,并设置 HTTP 状态码为 401;否则返回 true。
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor preHandle....");
HttpSession session = request.getSession();
UserInfo userInfo = (UserInfo) session.getAttribute(Constants.USER_SESSION_KEY);
if (userInfo==null || userInfo.getId()<=0){
response.setStatus(401);
return false;
}
return true;
}
}
接着,注册配置拦截器,把登录接口和静态资源统统排除在拦截范围之外:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login")
.excludePathPatterns("/css/**")
.excludePathPatterns("/js/**")
.excludePathPatterns("/pic/**")
.excludePathPatterns("/**/*.html")
;
}
}
现在,用浏览器直接访问 http://127.0.0.1:8080/book/queryBookById?bookId=1 看看效果:

用 Fiddler 抓个包看看:

然后,登录成功之后,再访问上面的链接,结果就完全不一样了:

DispatcherServlet源码解析(重要)
观察一下服务启动时的日志:

你会发现,Tomcat 启动之后,有一个核心类会登场——DispatcherServlet。它负责控制整个程序的执行顺序。
所有请求都会先进入 DispatcherServlet,执行 doDispatch 调度方法。如果有拦截器,那么会先执行拦截器的 preHandle() 方法。如果 preHandle() 返回 true,就继续访问 Controller 中的方法。Controller 中的方法执行完毕后,再回来执行 postHandle() 和 afterCompletion(),最后返回给 DispatcherServlet,最终给浏览器响应数据。

初始化
DispatcherServlet 的初始化方法 init() 是在它的父类 HttpServletBean 中实现的。它的主要作用就是加载 web.xml 中 DispatcherServlet 的配置,并调用子类的初始化方法。

在 HttpServletBean 的 init() 中,会调用 initServletBean()。这个方法是在 FrameworkServlet 类中实现的,主要作用是建立 WebApplicationContext 容器(有时也称为上下文),并加载 Spring MVC 配置文件中定义的 Bean 到该容器中,最后将该容器添加到 ServletContext 中。下面是 initServletBean() 的具体代码:

注意看,这里打印的日志,正是我们在控制台里看到的那条日志:

初始化 Web 容器的过程中,会通过 onRefresh 来初始化 Spring MVC 的容器:

在 initStrategies() 中,会进行 9 大组件的初始化。如果没有配置相应的组件,就会使用默认定义的组件(在 DispatcherServlet.properties 中有配置默认的策略)。
关于 9 大组件的解释
1. 初始化文件上传解析器
MultipartResolver:从应用上下文中获取名称为multipartResolver的 Bean,如果没有,则没有提供上传文件的解析器;2. 初始化区域解析器
LocaleResolver:从应用上下文中获取名称为localeResolver的 Bean,如果没有,则默认使用AcceptHeaderLocaleResolver作为区域解析器;3. 初始化主题解析器
ThemeResolver:从应用上下文中获取名称为themeResolver的 Bean,如果没有,则默认使用FixedThemeResolver作为主题解析器;4. 初始化处理器映射器
HandlerMappings:其作用有两个:1)通过处理器映射器找到对应的处理器适配器,将请求交给适配器处理;2)缓存每个请求地址 URL 对应的位置(Controller.xxx 方法)。如果在ApplicationContext发现有HandlerMappings,则从ApplicationContext中获取到所有的HandlerMappings,并进行排序;如果没有发现,则默认使用BeanNameUrlHandlerMapping作为处理器映射器;5. 初始化处理器适配器
HandlerAdapter:作用是通过调用具体的方法来处理具体的请求。如果在ApplicationContext发现有handlerAdapter,则从ApplicationContext中获取到所有的HandlerAdapter,并进行排序;如果没有发现,则默认使用SimpleControllerHandlerAdapter作为处理器适配器;6. 初始化异常处理器解析器
HandlerExceptionResolver:如果在ApplicationContext发现有handlerExceptionResolver,则从ApplicationContext中获取到所有的HandlerExceptionResolver,并进行排序;如果没有发现,则不设置异常处理器;7. 初始化
RequestToViewNameTranslator:其作用是从Request中获取viewName。从ApplicationContext发现是否有viewNameTranslator的 Bean,如果没有,则默认使用DefaultRequestToViewNameTranslator;8. 初始化视图解析器
ViewResolvers:先从ApplicationContext中获取名为viewResolver的 Bean,如果没有,则默认使用InternalResourceViewResolver作为视图解析器;9. 初始化
FlashMapManager:其作用是用于检索和保存FlashMap(保存从一个 URL 重定向到另一个 URL 时的参数信息)。从ApplicationContext发现有flashMapManager的 Bean,如果没有,则默认使用DefaultFlashMapManager。
处理请求(核心)
DispatcherServlet 接收到请求后,会执行 doDispatch 调度方法,再将请求转给 Controller。我们来仔细看看 doDispatch 方法的具体实现:
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());
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;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
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) {
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()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
代码中,有三个方法特别值得关注:


上面这三个方法,正好对应我们之前用到的 HandlerInterceptor 接口:

适配器模式
聊完了拦截器的源码,再来看看 HandlerAdapter。它在 Spring MVC 中使用了一种经典的设计模式——适配器模式。
适配器模式定义
适配器模式,也叫包装器模式。简单来说,就是将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。
说白了,就是目标类不能直接使用,需要通过一个新类包装一下,适配调用方来使用。把两个不兼容的接口通过一定的方式使之兼容。
比如下面这两个接口,本身是不兼容的(参数类型不一样,参数个数不一样等等):

但通过适配器的方式,就可以让它们变得兼容:

适配器模式角色
• Target:目标接口(可以是抽象类或接口),客户希望直接用的接口
• Adaptee:适配者,但是与 Target 不兼容
• Adapter:适配器类,此模式的核心。通过继承或者引用适配者的对象,把适配者转为目标接口
• Client:需要使用适配器的对象
适配器模式的实现
举个实际点的例子,我们之前学习的 slf4j 就使用了适配器模式。slf4j 提供了一系列打印日志的 API,但底层实际调用的是 log4j 或 logback 来打日志。作为调用者,我们只需要调用 slf4j 的 API 就行了。
Log4j
public class Log4j {
public void log4jPrint(String message){
System.out.println("我是Log4j, 打印日志内容为: "+message);
}
}
Log4jAdapter
public class Log4jAdapter implements Slf4jLog{
private Log4j log4j;
public Log4jAdapter(Log4j log4j) {
this.log4j = log4j;
}
@Override
public void log(String message) {
log4j.log4jPrint("我是是适配器, 打印日志为: "+ message);
}
}
Slf4jLog
public interface Slf4jLog {
void log(String message);
}
Main
public class Main {
public static void main(String[] args) {
Slf4jLog slf4jLog = new Log4jAdapter(new Log4j());
slf4jLog.log("我是客户端");
}
}
运行结果如下:

可以看到,我们不需要改变 log4j 的 API,只需要通过适配器转换一下,就可以轻松更换日志框架,保障系统的平稳运行。
适配器模式的优缺点
优点
提高系统的灵活性和可扩展性:通过适配器模式,可以很方便地增加新的适配器来支持新的接口,而不需要修改原有的代码。
解耦:客户端通过适配器与需要适配的类进行交互,降低了客户端与需要适配的类之间的耦合度。
缺点
过多使用适配器会使系统变得复杂:由于引入了适配器,系统的类数量会增加,这可能会使系统的结构变得复杂。
可能隐藏了需要适配的类的真正功能:客户端通过适配器与需要适配的类进行交互,可能会忽略需要适配的类的某些功能。
适配器模式的应用场景
当系统需要使用现有的类,而这些类的接口不符合系统的需要时。
想要建立一个可以重复使用的类库,以便与一些彼此之间没有太大关联甚至完全不兼容的类一起工作。
在开发过程中,由于某些原因(如框架升级、第三方库变更等)导致原有的接口发生变化,而客户端代码依赖于旧的接口时。
Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。
极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。
















