
Spring Boot拦截器(Interceptor)详解
Spring Boot拦截器(Interceptor)详解
拦截器(Interceptor)同 Filter 过滤器一样,它俩都是面向切面编程——AOP 的具体实现(AOP切面编程只是一种编程思想而已)。你可以使用 Interceptor 来执行某些任务,例如在 Controller 处理请求之前编写日志,添加或更新配置…
在 Spring中,当请求发送到 Controller 时,在被Controller处理之前,它必须经过 Interceptors(0或多个)。Spring Interceptor是一个非常类似于Servlet Filter 的概念 。
Interceptor 作用
日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算 PV(Page View)等;
权限检查:如登录检测,进入处理器检测是否登录;
性能监控:通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。(反向代理,如 Apache 也可以自动记录)
通用行为:读取 Cookie 得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取 Locale、Theme 信息等,只要是多个处理器都需要的即可使用拦截器实现。
自定义 Interceptor
如果你需要自定义 Interceptor 的话必须实现 org.springframework.web.servlet.HandlerInterceptor
接口或继承 org.springframework.web.servlet.handler.WebRequestHandlerInterceptorAdapter
类,并且需要重写下面下面 3 个方法(注意一下:后者在Spring Framework 5.3 版本之后已经被弃用了(@Deprecated
),并且在 Spring Boot 3.0 中已经移除了):
preHandler(HttpServletRequest request, HttpServletResponse response, Object handler)
方法在请求处理之前被调用。该方法在 Interceptor 类中最先执行,用来进行一些前置初始化操作或是对当前请求做预处理,也可以进行一些判断来决定请求是否要继续进行下去。该方法的返回至是 Boolean 类型,当它返回 false 时,表示请求结束,后续的 Interceptor 和 Controller 都不会再执行;当它返回为 true 时会继续调用下一个 Interceptor 的 preHandle 方法,如果已经是最后一个 Interceptor 的时候就会调用当前请求的 Controller 方法。postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
方法在当前请求处理完成之后,也就是 Controller 方法调用之后执行,但是它会在 DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对 Controller 处理之后的 ModelAndView 对象进行操作。afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)
方法需要在当前对应的 Interceptor 类的 postHandler 方法返回值为 true 时才会执行。顾名思义,该方法将在整个请求结束之后,也就是在 DispatcherServlet 渲染了对应的视图之后执行。此方法主要用来进行资源清理。
三者的执行顺序如下图所示:
接下来我们结合实际代码来看一下:
这里我用的是Spring Boot3.5.5版本。
首先是LogInterceptor 类:
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
long startTime = System.currentTimeMillis();
System.out.println("\n-------- LogInterception.preHandle --- ");
System.out.println("Request URL: " + request.getRequestURL());
System.out.println("Start Time: " + startTime); // Use startTime variable directly
request.setAttribute("startTime", startTime);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("\n-------- LogInterception.postHandle --- ");
System.out.println("Request URL: " + request.getRequestURL());
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("\n-------- LogInterception.afterCompletion --- ");
long startTime = (Long) request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
System.out.println("Request URL: " + request.getRequestURL());
System.out.println("End Time: " + endTime);
System.out.println("Time Taken: " + (endTime - startTime));
}
}
然后是OldLoginInterceptor 类:
public class OldLoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("\n-------- OldLoginInterceptor.preHandle --- ");
System.out.println("Request URL: " + request.getRequestURL());
System.out.println("Sorry! This URL is no longer used, Redirect to /admin/login");
response.sendRedirect(request.getContextPath()+ "/admin/login");
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("\n-------- OldLoginInterceptor.postHandle --- ");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("\n-------- OldLoginInterceptor.afterCompletion --- ");
}
}
接着是AdminInterceptor类
public class AdminInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("\n-------- AdminInterceptor.preHandle --- ");
System.out.println("Request URL: " + request.getRequestURL());
// 这里是你的业务逻辑,比如检查Session中是否有用户信息
// 如果没有,可以重定向到登录页并返回false
// 例如:
// Object user = request.getSession().getAttribute("adminUser");
// if (user == null) {
// response.sendRedirect(request.getContextPath() + "/admin/login");
// return false;
// }
return true; // 继续执行请求
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("\n-------- AdminInterceptor.postHandle --- ");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("\n-------- AdminInterceptor.afterCompletion --- ");
}
}
编写配置类配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor());
registry.addInterceptor(new OldLoginInterceptor()).addPathPatterns("/admin/oldLogin");
registry.addInterceptor(new AdminInterceptor()).addPathPatterns("/admin/*").excludePathPatterns("/admin/oldLogin");
}
}
LogInterceptor
拦截器用于拦截所有请求; OldLoginInterceptor
用来拦截链接 “ / admin / oldLogin”,它将重定向到新的 “ / admin / login”。;AdminInterceptor
用来拦截链接 “/admin/*”,除了链接 “ / admin / oldLogin”。
自定义一个Controller来验证拦截器
@Controller
public class LoginController {
@RequestMapping("/index")
public String index(Model model){
return "index";
}
@RequestMapping(value = "/admin/login")
public String login(Model model){
return "login";
}
}
同时依赖 thymeleaf 模板构建两个页面。
index.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Spring Boot Mvc Interceptor example</title>
</head>
<body>
<div style="border: 1px solid #ccc;padding: 5px;margin-bottom:10px;">
<a th:href="@{/}">Home</a>
|
<a th:href="@{/admin/oldLogin}">/admin/oldLogin (OLD URL)</a>
</div>
<h3>Spring Boot Mvc Interceptor</h3>
<span style="color:blue;">Testing LogInterceptor</span>
<br/><br/>
See Log in Console..
</body>
</html>
login.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Spring Boot Mvc Interceptor example</title>
</head>
<body>
<div style="border: 1px solid #ccc;padding: 5px;margin-bottom:10px;">
<a th:href="@{/}">Home</a>
|
<a th:href="@{/admin/oldLogin}">/admin/oldLogin (OLD URL)</a>
</div>
<h3>This is Login Page</h3>
<span style="color:blue">Testing OldLoginInterceptor & AdminInterceptor</span>
<br/><br/>
See more info in the Console.
</body>
</html>
运行程序并测试效果
一切准备完毕,启动该项目。打开网址: http://localhost:8080/index
说实话,我也不知道那个thymeleaf是啥玩意,我只会前后端分离哈哈哈。不过不影响,上述代码我是叫AI写的,感觉还可以。
这里说一下多个拦截器的情况,首先,在配置拦截器的 WebMvcConfigurer
类中,调用了 InterceptorRegistry
的方法。这个注册中心(InterceptorRegistry
)的底层其实就是一个简单的 List
集合。当调用 registry.addInterceptor(new A()).addPathPatterns(...)
和 registry.addInterceptor(new B()).addPathPatterns(...)
时,Spring 内部实际上是把这些拦截器按顺序添加到了一个 List
中。这个 List
就保存了所有拦截器的引用,并且保持了你注册时的先后顺序。
如下图所示
关于该请求在后台的执行过程,用图解的方式进行展示:
如果此时点击 /admin/oldLogin (OLD URL)
或者在网址栏输入:http://localhost:8080/admin/oldLogin
控制台打印结果:
-------- LogInterception.preHandle ---
Request URL: http://127.0.0.1:8080/admin/oldLogin
Start Time: 1757077887099
-------- OldLoginInterceptor.preHandle ---
Request URL: http://127.0.0.1:8080/admin/oldLogin
Sorry! This URL is no longer used, Redirect to /admin/login
-------- LogInterception.afterCompletion ---
Request URL: http://127.0.0.1:8080/admin/oldLogin
End Time: 1757077887100
Time Taken: 1
-------- LogInterception.preHandle ---
Request URL: http://127.0.0.1:8080/admin/login
Start Time: 1757077887102
-------- AdminInterceptor.preHandle ---
Request URL: http://127.0.0.1:8080/admin/login
-------- AdminInterceptor.postHandle ---
-------- LogInterception.postHandle ---
Request URL: http://127.0.0.1:8080/admin/login
-------- AdminInterceptor.afterCompletion ---
-------- LogInterception.afterCompletion ---
Request URL: http://127.0.0.1:8080/admin/login
End Time: 1757077887105
Time Taken: 3
同样我们用图解的形式分析:
应用
性能监控
如记录一下请求的处理时间,得到一些慢请求(如处理时间超过500毫秒),从而进行性能改进,一般的反向代理服务器如 apache 都具有这个功能,但此处我们演示一下使用拦截器怎么实现。
实现分析:
1、在进入处理器之前记录开始时间,即在拦截器的 preHandle 记录开始时间;
2、在结束请求处理之后记录结束时间,即在拦截器的 afterCompletion 记录结束实现,并用结束时间-开始时间得到这次请求的处理时间。
问题:
我们的拦截器是单例,因此不管用户请求多少次都只有一个拦截器实现,即线程不安全,那我们应该怎么记录时间呢?
解决方案是使用 ThreadLocal,它是线程绑定的变量,提供线程局部变量(一个线程一个 ThreadLocal,A线程的ThreadLocal 只能看到A线程的 ThreadLocal,不能看到B线程的 ThreadLocal)。
代码实现:
public class StopWatchHandlerInterceptor implements HandlerInterceptor {
// 线程局部变量,用于存储每个请求的开始时间
private NamedThreadLocal<Long> startTimeThreadLocal = new NamedThreadLocal<>("StopWatch-StartTime");
private Logger logger = LoggerFactory.getLogger(StopWatchHandlerInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1、获取请求的开始时间
long beginTime = System.currentTimeMillis();
// 将开始时间绑定到当前线程,以便在请求处理完成后获取
startTimeThreadLocal.set(beginTime);
// 返回 true,继续执行后续的拦截器和Controller
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 2、获取请求的结束时间
long endTime = System.currentTimeMillis();
// 得到线程绑定的局部变量(开始时间)
long beginTime = startTimeThreadLocal.get();
// 3、计算请求消耗的总时间
long consumeTime = endTime - beginTime;
// 4、使用完毕后,清除线程局部变量,防止内存泄漏
startTimeThreadLocal.remove();
// 如果处理时间超过500毫秒,则认为是慢请求
if (consumeTime > 500) {
// TODO: 可以在这里记录到日志文件或监控系统
logger.info(String.format("%s consume %d millis", request.getRequestURI(), consumeTime));
}
// 测试时,可以启用此行来打印所有请求的耗时
// logger.info(String.format("%s consume %d millis", request.getRequestURI(), consumeTime));
}
}
NamedThreadLocal:Spring提供的一个命名的ThreadLocal实现。
在测试时需要把 stopWatchHandlerInterceptor 放在拦截器链的第一个,这样得到的时间才是比较准确的。
拦截器配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new StopWatchHandlerInterceptor());
registry.addInterceptor(new OldLoginInterceptor()).addPathPatterns("/admin/oldLogin");
}
}
和上述操作步骤一致,控制台打印结果为:
-------- OldLoginInterceptor.preHandle ---
Request URL: http://127.0.0.1:8080/admin/oldLogin
Sorry! This URL is no longer used, Redirect to /admin/login
2025-09-05T21:32:34.641+08:00 INFO 20230 --- [interceptor_study] [nio-8080-exec-5] c.h.i.i.StopWatchHandlerInterceptor : /admin/oldLogin consume 0 millis
2025-09-05T21:32:34.644+08:00 INFO 20230 --- [interceptor_study] [nio-8080-exec-6] c.h.i.i.StopWatchHandlerInterceptor : /admin/login consume 1 millis
登录检测
在访问某些资源时(如订单页面),需要用户登录后才能查看,因此需要进行登录检测。
流程:
1、访问需要登录的资源时,由拦截器重定向到登录页面;
2、如果访问的是登录页面,拦截器不应该拦截;
3、用户登录成功后,往 cookie/session 添加登录成功的标识(如用户编号);
4、下次请求时,拦截器通过判断 cookie/session 中是否有该标识来决定继续流程还是到登录页面;
5、在此拦截器还应该允许游客访问的资源。
拦截器代码如下所示:
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
boolean flag = true;
String ip = request.getRemoteAddr();
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
if (handler instanceof ResourceHttpRequestHandler) {
System.out.println("preHandle这是一个静态资源方法!");
} else if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
System.out.println("用户:" + ip + ",访问目标:" + method.getDeclaringClass().getName() + "." + method.getName());
}
//如果用户未登录
User user = (User) request.getSession().getAttribute("user");
if (null == user) {
//重定向到登录页面
response.sendRedirect("toLogin");
flag = false;
}
return flag;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (handler instanceof ResourceHttpRequestHandler) {
System.out.println("postHandle这是一个静态资源方法!");
} else if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
long startTime = (long) request.getAttribute("requestStartTime");
long endTime = System.currentTimeMillis();
long executeTime = endTime - startTime;
int time = 1000;
//打印方法执行时间
if (executeTime > time) {
System.out.println("[" + method.getDeclaringClass().getName() + "." + method.getName() + "] 执行耗时 : "
+ executeTime + "ms");
} else {
System.out.println("[" + method.getDeclaringClass().getSimpleName() + "." + method.getName() + "] 执行耗时 : "
+ executeTime + "ms");
}
}
}
}