嘿,大家好!我是小米,一个充满活力、喜欢分享技术的29岁开发者。今天的文章,我们要来聊一聊一个发生在我们开发环境的惊险故事。这个问题折腾了我们整个团队好一阵子,最终我们发现元凶竟然是一个看似无害的 @Async 注解。废话不多说,直接开讲!
故事的开始:微服务无法启动
就在昨天,我们的开发环境微服务无法启动,连续尝试了好几次,每次启动的时候日志都报错:
读完这个错误日志,我们开发团队的小伙伴们都挠了挠头,心想这到底是个啥?特别是我们刚刚合并的代码中也没有涉及到什么大改动,为什么会突然冒出这么个问题?
错误日志解析
首先,让我们来分析一下这个错误日志究竟在说什么。
- Bean 被注入到其他 Beans 中:错误提示说,tradeService 这个 Bean 被注入到了其他两个 Bean(returnOrderService 和 refundOrderService)中,这本身其实是没什么问题的。在 Spring 的依赖注入机制下,Bean 被注入到其他 Bean 中是一件再普通不过的事。
- Raw version 被注入:问题的关键是这里提到的 “raw version”(原始版本)。在 Spring 中,Bean 经过创建和初始化后,可能还会被代理包装。比如,AOP 或者 @Async 会通过动态代理对 Bean 进行增强。那么这里的意思是,tradeService 被注入时,其他服务接收到的不是它的最终版本,而是未经过增强的原始版本。
- Circular reference(循环引用)问题:这个提示还提到了可能存在的循环引用问题。Spring 为了避免 Bean 的循环依赖,采用了三级缓存机制。在 Bean 创建过程中,如果有 Bean 依赖另一个还未完全初始化完成的 Bean,Spring 会暂时将原始 Bean 注入,这样可以打破循环引用的死锁。
- “over-eager type matching”:这个错误的最后部分提到了可能的原因:过度积极的类型匹配。在某些情况下,Spring 容器会过早地初始化某些 Bean,这可能会导致一些尚未完全准备好的 Bean 被注入。
至此,我们已经大致明白问题的轮廓:tradeService 这个 Bean 因为某种原因,被注入了它的原始版本,而不是最终被代理增强后的版本。
团队排查:@Async 的可疑之处
我们开始逐步排查代码变更,最后注意到一个开发同事在 tradeService 的某个方法上添加了 @Async 注解。大家知道,@Async 是 Spring 提供的一个非常方便的异步执行注解。通过在方法上加上这个注解,Spring 会把这个方法的执行交给一个线程池,避免占用主线程资源。
虽然 @Async 本身非常好用,但它的实现依赖于 AOP 动态代理机制。当 Spring 看到一个方法被 @Async 注解修饰时,它会创建一个代理对象,代理对象接管方法的调用逻辑。在这种情况下,如果你直接在其他地方引用了这个 Bean 的原始版本,而不是代理后的版本,就会导致预期外的行为——比如像我们遇到的错误。
正是这个 @Async 导致了 Bean 没有被完整初始化,进而引发了循环依赖和代理注入的问题。
深入解析:Spring 的代理机制与循环依赖
为了更好地理解这个问题,我们需要进一步探讨 Spring 是如何处理 Bean 的依赖注入和代理机制。
1. Bean 的创建过程
在 Spring 中,Bean 的创建分为以下几个步骤:
- 实例化:首先,Spring 会通过构造函数或者工厂方法实例化 Bean。
- 依赖注入:接下来,Spring 会为这个 Bean 注入所需的依赖,这时候如果某个 Bean 依赖还没有准备好,Spring 就会遇到循环依赖问题。
- 初始化:在依赖注入完成后,Spring 会执行 Bean 的初始化方法,比如 @PostConstruct 标注的方法,或者执行一些自定义的初始化逻辑。
- 代理包装:在初始化完成后,如果这个 Bean 需要增强(如 AOP 或者 @Async),Spring 会为这个 Bean 创建一个代理对象。
2. 循环依赖
如果两个 Bean 相互依赖,比如 A 依赖 B,而 B 又依赖 A,那么 Spring 就会遇到循环依赖问题。为了打破这种循环,Spring 会在依赖注入时注入原始版本的 Bean,而不是最终被代理包装后的版本。
在我们的案例中,tradeService 被 returnOrderService 和 refundOrderService 引用,而它自身因为使用了 @Async 注解,导致它需要被代理。如果 Spring 在创建代理之前就将它注入到了其他服务中,就会引发前面提到的那个错误。
找到了问题的根源,我们就可以着手解决它了。
方法一:调整依赖结构
最直接的解决方案就是重新审视我们的依赖结构。我们可以考虑是否能打破循环引用,避免 tradeService 被过早地注入到其他 Bean 中。
在我们的案例中,我们尝试通过调整依赖关系,将 tradeService 的一些依赖注入拆分到其他组件中,避免直接注入原始 Bean。
方法二:使用 @Lazy 延迟加载
如果调整依赖结构有些困难,另一种方法是使用 @Lazy 注解。通过将 Bean 设置为懒加载,Spring 会推迟它的实例化,直到它真正被使用时才会创建。这可以有效避免 Bean 在初始化过程中被过早地注入。
方法三:禁用 @Async 的代理
在某些场景下,如果 @Async 只是在某些方法上使用,而不是全局依赖,我们可以考虑不使用代理。在 Spring 中,可以通过配置文件或者编程的方式禁用某些 Bean 的代理行为。
但这种方式比较少见,通常我们会选择保留 @Async 代理,毕竟它为我们提供了异步执行的便利。
END
这次开发环境中的微服务启动问题,最终是由 @Async 引起的代理问题导致的。通过深入理解 Spring 的 Bean 代理机制和循环依赖的处理方法,我们成功找到了问题的根源,并采取了有效的解决方案。这个案例告诉我们,在使用诸如 @Async 这样的注解时,一定要小心其背后的代理机制,尤其是在存在复杂依赖关系的场景下。
如果你也遇到类似的问题,记得从错误日志中寻找线索,逐步排查问题的根源。希望这篇文章对大家有所帮助!
我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!