跳到主要内容Spring Boot 拦截器详解与实战 | 极客日志Javajava
Spring Boot 拦截器详解与实战
Spring Boot 拦截器用于统一处理请求,如登录校验。通过实现 HandlerInterceptor 接口定义拦截逻辑,配置 WebMvcConfigurer 注册拦截器。核心流程涉及 preHandle、postHandle 和 afterCompletion 方法。DispatcherServlet 作为调度中心,利用适配器模式兼容不同处理器。拦截器路径配置、执行流程及源码解析。
苹果系统2.6K 浏览 Spring Boot 拦截器
之前我们完成的图书管理系统完成了强制登录的功能,实现原理是后端在用户登录成功后会把用户信息存储到了 Session 中,后端程序根据 Session 来判断用户是否登录,但是这样是比较麻烦的:
• 需要修改每个接口的处理逻辑
• 需要修改每个接口的返回结果
• 接口定义修改,前端代码也需要跟着修改
有没有更简单的办法,统一拦截所有的请求,并进行 Session 校验呢?
这里我们学习一种新的解决办法:拦截器。
拦截器是 Spring 框架提供的核心功能之一,主要用来拦截用户的请求,在指定方法前后,根据业务需要执行预先设定的代码。

拦截器的使用
拦截器的使用分为两步:
- 定义拦截器;
- 注册配置拦截器。
自定义拦截器
实现 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 Exception {
log.info();
}
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws
"LoginInterceptor 视图渲染完毕后执行,最后执行"
• preHandle() 方法:目标方法执行前执行。返回 true 表示可以继续执行后续操作;返回 false 表示中断后续操作。
• postHandle() 方法:目标方法执行后执行。
• afterCompletion() 方法:视图渲染完毕后执行,最后执行。
注册配置拦截器
实现 WebMvcConfigurer 接口,并重写 addInterceptors 方法。
@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() 指定不拦截哪些请求。
上述代码中,我们配置的是 /**,表示拦截所有的请求。
比如用户登录校验,我们希望可以对除了登录之外所有的路径生效。
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;
import springbook.interceptor.LoginInterceptor;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login")
;
}
}
在拦截器中除了可以设置 /** 拦截所有资源外,还有一些常见拦截路径设置:
| 拦截路径 | 含义 | 举例 |
|---|
| /* | 一级路径 | 能匹配/user,/book,/login,不能匹配 /user/login |
| /** | 任意级路径 | 能匹配/user,/user/login,/user/reg |
| /book/* | /book 下的一级路径 | 能匹配/book/addBook,不能匹配/book/addBook/1,/book |
| /book/** | /book 下的任意级路径 | 能匹配/book,/book/addBook,/book/addBook/2,不能匹配/user/login |
拦截器执行流程
有了拦截器之后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图:
- 添加拦截器后,执行 Controller 的方法之前,请求会先被拦截器拦截住。执行 preHandle() 方法,这个方法需要返回一个布尔类型的值。如果返回 true,就表示放行本次操作,继续访问 controller 中的方法。如果返回 false,则不会放行 (controller 中的方法也不会执行)。
- 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")
;
}
}
DispatcherServlet 源码解析(重要)
当 Tomcat 启动之后,有一个核心的类 DispatcherServlet,它来控制程序的执行顺序。
所有请求都会先进到 DispatcherServlet,执行 doDispatch 调度方法。如果有拦截器,会先执行拦截器 preHandle() 方法的代码,如果 preHandle() 返回 true,继续访问 controller 中的方法。controller 当中的方法执行完毕后,再回过来执行 postHandle() 和 afterCompletion(),返回给 DispatcherServlet,最终给浏览器响应数据。
初始化
DispatcherServlet 的初始化方法 init() 在其父类 HttpServletBean 中实现的。
主要作用是加载 web.xml 中 DispatcherServlet 的配置,并调用子类的初始化。
在 HttpServletBean 的 init() 中调用了 initServletBean(),它是在 FrameworkServlet 类中实现的,主要作用是建立 WebApplicationContext 容器 (有时也称上下文),并加载 SpringMVC 配置文件中定义的 Bean 到该容器中,最后将该容器添加到 ServletContext 中。下面是 initServletBean() 的具体代码:
初始化 web 容器的过程中,会通过 onRefresh 来初始化 SpringMVC 的容器:
在 initStrategies() 中进行 9 大组件的初始化,如果没有配置相应的组件,就使用默认定义的组件 (在 DispatcherServlet.properties 中有配置默认的策略)。关于 9 大组件的解释
- 初始化文件上传解析器 MultipartResolver:从应用上下文中获取名称为 multipartResolver 的 Bean,如果没有名为 multipartResolver 的 Bean,则没有提供上传文件的解析器;
- 初始化区域解析器 LocaleResolver:从应用上下文中获取名称为 localeResolver 的 Bean,如果没有这个 Bean,则默认使用 AcceptHeaderLocaleResolver 作为区域解析器;
- 初始化主题解析器 ThemeResolver:从应用上下文中获取名称为 themeResolver 的 Bean,如果没有这个 Bean,则默认使用 FixedThemeResolver 作为主题解析器;
- 初始化处理器映射器 HandlerMappings:处理器映射器作用,1)通过处理器映射器找到对应的处理器适配器,将请求交给适配器处理;2)缓存每个请求地址 URL 对应的位置(Controller.xxx 方法);如果在 ApplicationContext 发现有 HandlerMappings,则从 ApplicationContext 中获取到所有的 HandlerMappings,并进行排序;如果在 ApplicationContext 中没有发现有处理器映射器,则默认 BeanNameUrlHandlerMapping 作为处理器映射器;
- 初始化处理器适配器 HandlerAdapter:作用是通过调用具体的方法来处理具体的请求;如果在 ApplicationContext 发现有 handlerAdapter,则从 ApplicationContext 中获取到所有的 HandlerAdapter,并进行排序;如果在 ApplicationContext 中没有发现处理器适配器,则默认 SimpleControllerHandlerAdapter 作为处理器适配器;
- 初始化异常处理器解析器 HandlerExceptionResolver:如果在 ApplicationContext 发现有 handlerExceptionResolver,则从 ApplicationContext 中获取到所有的 HandlerExceptionResolver,并进行排序;如果在 ApplicationContext 中没有发现异常处理器解析器,则不设置异常处理器;
- 初始化 RequestToViewNameTranslator:其作用是从 Request 中获取 viewName,从 ApplicationContext 发现有 viewNameTranslator 的 Bean,如果没有,则默认使用 DefaultRequestToViewNameTranslator;
- 初始化视图解析器 ViewResolvers:先从 ApplicationContext 中获取名为 viewResolver 的 Bean,如果没有,则默认 InternalResourceViewResolver 作为视图解析器;
- 初始化 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 就行了。
public class Log4j {
public void log4jPrint(String message){
System.out.println("我是 Log4j, 打印日志内容为:"+message);
}
}
public class Log4jAdapter implements Slf4jLog {
private Log4j log4j;
public Log4jAdapter(Log4j log4j) {
this.log4j = log4j;
}
@Override
public void log(String message) {
log4j.log4jPrint("我是是适配器,打印日志为:"+ message);
}
}
public interface Slf4jLog {
void log(String message);
}
public class Main {
public static void main(String[] args) {
Slf4jLog slf4jLog = new Log4jAdapter(new Log4j());
slf4jLog.log("我是客户端");
}
}
运行结果:
可以看出,我们不需要改变 log4j 的 api,只需要通过适配器转换下,就可以更换日志框架,保障系统的平稳运行。
适配器模式的优缺点
提高系统的灵活性和可扩展性:通过适配器模式,可以很方便地增加新的适配器来支持新的接口,而不需要修改原有的代码。
解耦:客户端通过适配器与需要适配的类进行交互,降低了客户端与需要适配的类之间的耦合度。
过多使用适配器会使系统变得复杂:由于引入了适配器,系统的类数量会增加,这可能会使系统的结构变得复杂。
可能隐藏了需要适配的类的真正功能:客户端通过适配器与需要适配的类进行交互,可能会忽略需要适配的类的某些功能。
适配器模式的应用场景
当系统需要使用现有的类,而这些类的接口不符合系统的需要时。
想要建立一个可以重复使用的类库,以便与一些彼此之间没有太大关联甚至完全不兼容的类一起工作。
在开发过程中,由于某些原因(如框架升级、第三方库变更等)导致原有的接口发生变化,而客户端代码依赖于旧的接口时。