1. 引言

我们最近研究了 Lightrun —— 一个开发者可观测性平台 —— 探索它能如何帮助我们更好地观察和理解应用程序。

Spring 大量使用注解来控制各种功能,这些注解可以通过多种方式工作。这使得编写应用变得极其高效 —— 我们只需添加适当的注解即可启用功能。然而,当注解失效时,诊断过程会令人沮丧,因为没有直接的方法调用可供追踪。

本文将探讨如何使用 Lightrun 诊断 Spring 注解在应用程序中的工作机制。

2. 调试事务边界

Spring 使用 @Transactional 注解标记需要在事务中执行的方法。其原理是 Spring 在构建时检测该注解,并构造一个 JDK 代理来包装我们的类实例。该代理负责处理所有事务边界细节,确保事务在方法执行前启动,并在方法完成后正确清理。

因此,调试事务意味着我们必须首先确定需要关注的切入点。 最简单的方法是在事务方法中添加 快照 并执行它,从而捕获堆栈跟踪:

Frames

这里可以看到从控制器(deleteTask:74, TasksController)到事务服务(deleteTaskById:40, TasksService)的完整堆栈跟踪。由于我们的代码是直接方法调用,Spring 在两者之间插入了所有这些逻辑。

现在需要判断哪些堆栈帧是关键。许多条目聚焦于代理和反射调用,但中间有三个明显与事务相关:

  • invoke:119, TransactionInterceptor
  • invokeWithinTransaction:388, TransactionAspectSupport
  • proceedWithInvocation:123, TransactionInterceptor$1

从方法名可以推断大致流程:**invokeWithinTransaction 极可能是 Spring 管理事务边界的核心位置。** 这正是我们需要重点调试的地方。

在 IDE 中打开这段代码,可以看到具体实现:

Invoking within a transaction

为了理解其对代码的影响,我们可以用 Lightrun 在运行时向关键行添加日志

  • 382 行后添加日志:显示新启动的事务
  • 392 行添加日志:显示异常是否中止了事务
  • 408 行后添加日志:显示事务结束时的结果

同时在控制器和服务中添加日志(观察事务内外行为),就能清晰看到完整流程:

Logs Console

这里启动了三个 Spring 事务:一个在服务调用外,两个在服务内。服务内的两个事务对应两个仓库调用,但由于都使用 PROPAGATION_REQUIRED,它们实际参与了同一个数据库事务。

关键收获:无需中断运行中的应用,我们就获得了事务启动/结束时机、回滚状态和输出结果的精确信息。

3. 调试缓存边界

Spring 支持通过注解缓存方法结果。添加注解后,Spring 会自动在方法调用周围插入代码:缓存结果,并在适当时返回缓存值而非实际调用方法。

缓存调试极其棘手:缓存命中时底层代码不会执行,导致其中任何日志(包括我们手动添加或 Lightrun 注入的)都不会触发。但 Lightrun 允许我们在代码和缓存逻辑中同时添加日志。

同样先通过快照查看 Spring 注入的调用链:

Snapshot Snapshot frame

这里看到控制器、服务以及 Spring 插入的调用。关键类是 CacheInterceptorCacheAspectSupport(实际后者是前者的父类)。深入代码发现核心逻辑在 CacheAspectSupport.execute()

Inspect code

中间部分检查缓存命中/未命中并执行相应逻辑。因此我们可以在这些位置添加日志,无论命中与否都能观察行为:

  • 414 行日志:显示缓存命中/未命中状态
  • 421 行日志:标记即将调用底层方法
  • 423 行后日志:显示返回值(无论来自缓存还是实际调用)

现在可以清晰看到缓存行为:

Logs

两次获取相同资源的调用:第一次缓存未命中,调用实际服务;第二次缓存命中,跳过服务调用。

没有 Lightrun 时:只能看到控制器两次被调用,服务仅调用一次,但无法知晓原因。

4. 调试请求映射

Spring WebMVC 是框架核心部分,负责处理 HTTP 请求并转发到正确控制器。但当请求映射出错时,定位问题会非常痛苦。

Lightrun 提供了相同模式的调试工具,但 Spring 内部在此领域更复杂,需要更多探索。

首先在任意控制器添加快照并触发,获取调用堆栈:

Lightrun snapshots

探索后发现 DispatcherServlet.doDispatch() 很关键,它调用了 getHandler() —— 这应该是确定请求处理器的位置。堆栈显示了通向控制器的多个 Spring 调用。

查看内部实现

Get the handler

它遍历 HandlerMapping 实例集合,依次询问是否能处理请求。在 1261 行添加日志观察实际行为:

Logs

日志立即显示涉及的处理器映射,且最后一个返回了结果。接下来查看 RequestMappingHandlerMappinggetHandler() 实现(实际在父类 AbstractHandlerMethodMappinglookupHandlerMethod() 中)。

关键部分是 addMatchingMappings(),添加日志观察匹配过程:

Logs

大部分映射未匹配,但有一个匹配了 "GET /{id}" —— 正是我们预期的结果。如果全无匹配,问题就出在请求映射上。例如使用不支持的 HTTP 方法或错误 URI 路径时,这里会显示全 null 匹配,直接暴露问题根源:

Logs

⚠️ 调试技巧:如果问题出在处理器方法调用,可继续深入诊断;如果问题在于方法未被调用,此时已能定位原因。

5. 总结

本文通过几个 Spring 注解示例,展示了如何使用 Lightrun 诊断其工作机制。 我们学习了如何利用这些工具理解功能生效和失效的场景。

这些技术同样适用于其他库和框架。下次需要诊断第三方库时,不妨试试这些方法?

想了解更多 Lightrun 内容,可访问其博客


原始标题:Debugging Spring Method Annotations Using Lightrun | Baeldung