从拦截器错误到 Spring MVC 深入理解:一次项目调试的心路历程
前段时间在做项目时遇到一个 bug,最终定位到了拦截器这一步。具体是我定义了一个拦截器,用于在处理请求之前执行一些操作。然而,在拦截器的 preHandle 方法中,尽管 handler 对象确实是 HandlerMethod 的实例,但 handler instanceof HandlerMethod
的检查始终返回 false ,这令我费解。
下面是拦截器中的相关代码段:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("The specific class name of handler: {}", handler.getClass().getName());
log.info("Is handler a HandlerMethod class?: {}", (handler instanceof HandlerMethod));
log.info("The class loader of handler: {}", handler.getClass().getClassLoader());
log.info("The class loader of HandlerMethod: {}", HandlerMethod.class.getClassLoader());
if (!(handler instanceof HandlerMethod)) {
log.info("Request intercepted, request URI: {}", request.getRequestURI());
return true;
}
// Other logic...
return true;
}
在日志中,我可以清楚地看到 handler 对象确实是 HandlerMethod 类的实例,并且 handler 和 HandlerMethod 使用的是相同的类加载器:
The specific class name of handler: org.springframework.web.method.HandlerMethod
Is handler a HandlerMethod class?: false
The class loader of handler: jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7
The class loader of HandlerMethod: jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7
紧接着我排查了:
- 我使用的是 Spring Boot 2.7.3 版本,并且已经确认:项目中没有包含多个版本的 Spring 框架。
- 项目中没有与类加载器相关的特殊配置。
顺便插一嘴,之所以在日志中也要检查 HanlderMethod 和 handle 的类加载器,是因为不同的类加载器可以加载同一个类的不同实例。即使两个类名和包名相同,如果它们是由不同的类加载器加载的,Java会将它们视为不同的类。在这一块我还耽误了许多时间,最终排查出没有问题。
当时做到这我已经有点崩溃了,但实在找不到问题所在,于是无奈在 stackoverflow 平台发帖求助:
instanceof HandlerMethod' Always Returns false in a Spring Interceptor, Despite Same Class Loader
发完之后已经筋疲力竭,就只想着快点把这些事情结束,于是换了种写法,规避了这个问题:
if (handler.getClass().getName().equals(HandlerMethod.class.getName())) {
// 检查handler的类型并输出类加载器信息
log.info("handler对象的具体类名:{}", handler.getClass().getName());
log.info("handler是HandlerMethod类吗:{}", (handler instanceof HandlerMethod));
log.info("handler的类加载器:{}", handler.getClass().getClassLoader());
log.info("HandlerMethod的类加载器:{}",HandlerMethod.class.getClassLoader());
return true;
}
过了两个月,这几天我在梳理我做过的这个项目,突然想到 stackoverflow 上的这篇问答,于是点开看,发现收到一个评论是:
Maybe you imported the wrong package. The correct one is org.springframework.web.method.HandlerMethod instead of org.springframework.messaging.handler.HandlerMethod
然后我看了看我导入的包,果然是......,就此破案,这一切的罪魁祸首,居然是因为我导入了错误的包。
现在冷静下来,距离也远了许多,能以一种更宏观的角度去审视这个错误,归根结底还是因为我对 Spring 框架理解的不足,这是我的第一个学习项目,当时做的方式更接近于模仿,对一些内在的原因并没有一些深层次的理解,比如 preHandler 作为拦截器,在执行流中应当位于 Controller 层的前面,为什么这个时候就能拿到 handler,并判断 handler 的类型。于是我开始进一步的梳理,基于我现在的理解水平,画了一个 Spring MVC 请求处理流程图:
如图所示,
- 请求到达 Spring Boot 嵌入式容器(tomcat),将请求转换成 HttpServletRequest 类型的请求对象 request,并创建 HttpServletResponse 类型的响应对象 response,都传给 DispatchServlet
- DispatcherServlet 会通过 HandlerMapping 将请求映射成具体的处理器 handler
- 调用已注册的拦截器,request, response, handler 作为参数被传递给拦截器
- 通过的 hanlder 会匹配到对应的 HandlerAdapter
- 之后就可以通过 HandlerAdapter 调用具体的 Controller 层方法
- Controller层 方法继续调用 Service 层、Repository 层方法
- 返回一个 ResponseEntity 或者一个数据对象给 DispatcherServlet
- DispatcherServlet 使用消息转换器将对象序列化为 JSON 格式的响应体
- DispatcherServlet 将转换后的 JSON 数据封装为 HTTP 响应,并通过 HttpServletResponse 对象返回给前端
补充:Spring MVC 中的拦截器有三个生命周期,分别是请求执行前,执行后,还有编译后。对于前后端分离架构来说,执行前拦截器是最常用的:preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
现在我之前的疑惑 —— 为什么 preHandler 能拿到 handler,这是因为早在请求到达 DispatcherServlet 的时候,就已经通过 HandlerMapping 映射成了具体的 handler,因此在拦截器中就可以通过判断 handler 的类型来判断
之前只知道:
Dispatcher Servlet 是 Spring MVC 框架中的核心组件,负责将请求分发到相应的处理器(Controller)进行处理。它是一个前端控制器(Front Controller),统一处理所有进入的 HTTP 请求,并将响应返回给客户端。
现在在解决这个 bug 的过程中引发的一系列思考,让我对 Spring MVC 与 DispatcherServlet 的理解 更加深刻。
QA:hanlder 是什么?除了 HandlerMethod 还有哪些类型?
在 Spring MVC 中,handler 是一个能够处理 HTTP 请求的组件,可以是控制器方法、实现特定接口的类等。 有多种类型的 handler,根据具体的实现方式和用途有所不同。以下是一些常见的 handler类型:
-
HandlerMethod
- 基于注解的控制器方法。
- 包含了控制器实例、方法、方法参数等信息。
- 例如,使用 @RequestMapping 注解的方法就是 HandlerMethod。
-
HttpRequestHandler
- 更底层的处理器接口,直接处理 HttpServletRequest 和 HttpServletResponse 对象。
- 适用于需要直接处理请求和响应的场景。
public class MyHttpRequestHandler implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("Hello from HttpRequestHandler");
}
}
-
Controller
- 传统的处理器接口,实现该接口的类会被用作请求处理器。
- 在现代 Spring MVC 应用中较少使用,更多的是使用注解方式。
public class MyController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mav = new ModelAndView("exampleView");
mav.addObject("message", "Hello from Controller");
return mav;
}
}
QA:org.springframework.messaging.handler.HandlerMethod 与 org.springframework.web.method.HandlerMethod 的区别在哪?
虽然两个类的名称相同,但它们在不同的上下文中实现了不同的功能。
-
org.springframework.messaging.handler.HandlerMethod
- 类层次:位于 Spring Messaging 模块中,处理消息的传递和调用。
- 功能实现:封装消息处理方法,并提供调用这些方法的功能,支持消息参数的解析。
-
org.springframework.web.method.HandlerMethod
- 类层次:位于 Spring MVC 模块中,处理 HTTP 请求的传递和调用。
- 功能实现:封装控制器方法,并提供调用这些方法的功能,支持 HTTP 请求参数的解析。
QA:HandlerAdapter 的意义是什么?
HandlerAdapter 是一个适配器接口,用于将不同类型的处理器(如 HandlerMethod、HttpRequestHandler 等)适配为统一的调用方式。HandlerAdapter 的主要职责是调用实际的处理器方法。
转载自:https://juejin.cn/post/7381662346273898533