1. 引言
我们最近研究了 Lightrun —— 一个开发者可观测性平台 —— 探索它能如何帮助我们更好地观察和理解应用程序。
Spring 大量使用注解来控制各种功能,这些注解可以通过多种方式工作。这使得编写应用变得极其高效 —— 我们只需添加适当的注解即可启用功能。然而,当注解失效时,诊断过程会令人沮丧,因为没有直接的方法调用可供追踪。
本文将探讨如何使用 Lightrun 诊断 Spring 注解在应用程序中的工作机制。
2. 调试事务边界
Spring 使用 @Transactional
注解标记需要在事务中执行的方法。其原理是 Spring 在构建时检测该注解,并构造一个 JDK 代理来包装我们的类实例。该代理负责处理所有事务边界细节,确保事务在方法执行前启动,并在方法完成后正确清理。
因此,调试事务意味着我们必须首先确定需要关注的切入点。 最简单的方法是在事务方法中添加 快照 并执行它,从而捕获堆栈跟踪:
这里可以看到从控制器(deleteTask:74, TasksController
)到事务服务(deleteTaskById:40, TasksService
)的完整堆栈跟踪。由于我们的代码是直接方法调用,Spring 在两者之间插入了所有这些逻辑。
现在需要判断哪些堆栈帧是关键。许多条目聚焦于代理和反射调用,但中间有三个明显与事务相关:
invoke:119, TransactionInterceptor
invokeWithinTransaction:388, TransactionAspectSupport
proceedWithInvocation:123, TransactionInterceptor$1
从方法名可以推断大致流程:**invokeWithinTransaction
极可能是 Spring 管理事务边界的核心位置。** 这正是我们需要重点调试的地方。
在 IDE 中打开这段代码,可以看到具体实现:
为了理解其对代码的影响,我们可以用 Lightrun 在运行时向关键行添加日志:
- 382 行后添加日志:显示新启动的事务
- 392 行添加日志:显示异常是否中止了事务
- 408 行后添加日志:显示事务结束时的结果
同时在控制器和服务中添加日志(观察事务内外行为),就能清晰看到完整流程:
这里启动了三个 Spring 事务:一个在服务调用外,两个在服务内。服务内的两个事务对应两个仓库调用,但由于都使用 PROPAGATION_REQUIRED
,它们实际参与了同一个数据库事务。
✅ 关键收获:无需中断运行中的应用,我们就获得了事务启动/结束时机、回滚状态和输出结果的精确信息。
3. 调试缓存边界
Spring 支持通过注解缓存方法结果。添加注解后,Spring 会自动在方法调用周围插入代码:缓存结果,并在适当时返回缓存值而非实际调用方法。
缓存调试极其棘手:缓存命中时底层代码不会执行,导致其中任何日志(包括我们手动添加或 Lightrun 注入的)都不会触发。但 Lightrun 允许我们在代码和缓存逻辑中同时添加日志。
同样先通过快照查看 Spring 注入的调用链:
这里看到控制器、服务以及 Spring 插入的调用。关键类是 CacheInterceptor
和 CacheAspectSupport
(实际后者是前者的父类)。深入代码发现核心逻辑在 CacheAspectSupport.execute()
:
中间部分检查缓存命中/未命中并执行相应逻辑。因此我们可以在这些位置添加日志,无论命中与否都能观察行为:
- 414 行日志:显示缓存命中/未命中状态
- 421 行日志:标记即将调用底层方法
- 423 行后日志:显示返回值(无论来自缓存还是实际调用)
现在可以清晰看到缓存行为:
两次获取相同资源的调用:第一次缓存未命中,调用实际服务;第二次缓存命中,跳过服务调用。
❌ 没有 Lightrun 时:只能看到控制器两次被调用,服务仅调用一次,但无法知晓原因。
4. 调试请求映射
Spring WebMVC 是框架核心部分,负责处理 HTTP 请求并转发到正确控制器。但当请求映射出错时,定位问题会非常痛苦。
Lightrun 提供了相同模式的调试工具,但 Spring 内部在此领域更复杂,需要更多探索。
首先在任意控制器添加快照并触发,获取调用堆栈:
探索后发现 DispatcherServlet.doDispatch()
很关键,它调用了 getHandler()
—— 这应该是确定请求处理器的位置。堆栈显示了通向控制器的多个 Spring 调用。
查看内部实现:
它遍历 HandlerMapping
实例集合,依次询问是否能处理请求。在 1261 行添加日志观察实际行为:
日志立即显示涉及的处理器映射,且最后一个返回了结果。接下来查看 RequestMappingHandlerMapping
的 getHandler()
实现(实际在父类 AbstractHandlerMethodMapping
的 lookupHandlerMethod()
中)。
关键部分是 addMatchingMappings()
,添加日志观察匹配过程:
大部分映射未匹配,但有一个匹配了 "GET /{id}" —— 正是我们预期的结果。如果全无匹配,问题就出在请求映射上。例如使用不支持的 HTTP 方法或错误 URI 路径时,这里会显示全 null
匹配,直接暴露问题根源:
⚠️ 调试技巧:如果问题出在处理器方法调用,可继续深入诊断;如果问题在于方法未被调用,此时已能定位原因。
5. 总结
本文通过几个 Spring 注解示例,展示了如何使用 Lightrun 诊断其工作机制。 我们学习了如何利用这些工具理解功能生效和失效的场景。
这些技术同样适用于其他库和框架。下次需要诊断第三方库时,不妨试试这些方法?
想了解更多 Lightrun 内容,可访问其博客。