JAVA

Spring循环依赖,一个注解搞定!

转载:Spring循环依赖,一个注解搞定!

提起循环依赖,很多人自然而然想到 Spring的三层缓存,然而,Spring产生循环依赖的场景有三种:变量注入、setter注入、构造器注入,三层缓存只能解决变量注入和 setter注入。因此,今天重点分析 Spring如何处理构造器注入产生的循环依赖?

本文大纲:

  • 什么是循环依赖?
  • 如何解决循环依赖?
  • @Lazy源码解析
  • 常见面试题

申明:本文源码基于 springboot-2.7.1、spring-5.3.24 和 JDK11,版本不同,源码可能略有差异。

一、什么是循环依赖? 


循环依赖是指:对象实例之间依赖关系构成一个闭环,通常分为:单个对象的自我依赖、两个对象的相互依赖、多个对象依赖成环。循环依赖抽象如下图:

单个对象的自我依赖

如下代码,在 UserService类中,通过构造器注入 UserService。

服务器启动报错,运行结果如下图:

出现这种类型的循环依赖,说明代码真的很low,要自我检讨,这里只是作为循环依赖的一种案例。

两个对象相互依赖

如下代码,UserService在类中通过构造器注入OrderService,OrderService在类中通过构造器注入UserService。

服务器启动报错,运行结果如下图:

这是一个经典的循环依赖问题,日常开发中比较常见。

多个对象的依赖成环

如下代码,UserService在类中通过构造器注入OrderService,OrderService在类中通过构造器注入GoodsService。GoodsService在类中通过构造器注入UserService。

服务器启动报错,运行结果如下图:

这种循环依赖比较隐蔽,多个对象依赖最终成环(这里以 3个对象依赖成环为例)。

通过上述的实例可以看出,对于构造器注入产生的循环依赖,服务器启动时都会报错,Spring无法自动处理。因此该如何处理循环依赖呢?

二、如何解决循环依赖? 


解决构造器注入产生的循环依赖,通常有3种方式:重构代码、使用变量注入或setter注入、使用 @Lazy 注解。

2.1 重构代码

既然循环依赖是代码编写带来的,最彻底的方案是重新设计结构,消除循环依赖,但是,重构代码的范围可能不可控,因此,对于测试等存在一定的回归成本,这是一种代价稍微大点的方案。

另外,代码出现循环依赖,在一定意义上(不是绝对哦)预示了 code smell:为什么会存在循环依赖?代码抽象是否合理?代码设计是否违背了 SOLID 原则?

2.2 使用变量注入或setter注入

曾经很长一段时间,变量注入和 setter注入(Spring3.x推荐的方式)是主流的编程方式, 两种方式简单,而且,Spring 可以利用三层缓存自动解决循环依赖问题。但是,从 Spring4.x开始,官方推荐构造器注入方式,而且在 github上也可以发现大部分的开源软件都转变成构造器注入。

2.3 使用 @Lazy 注解

@Lazy 是 spring 3.0 提供的一个注解,用来表示是否要延迟初始化 bean,首先看下 @Lazy 注解的源码:

从 @Lazy 注解的源码可以总结几点:

  • @Lazy 用来标识类是否需要延迟加载
  • @Lazy 可以作用在类上、方法上、构造器上、方法参数上、成员变量中
  • @Lazy 作用于类上时,通常与 @Component 及其衍生注解配合使用;
  • @Lazy 注解作用于方法上时,通常与 @Bean 注解配合使用

因此,通过 @Lazy 解决构造器循环依赖的代码改造如下:

三、@Lazy 源码解析 


本文使用 Springboot 启动,因此整体思路是:

  • Springboot 是如何启动 Spring IOC 容器?
  • 如何加载 Bean?
  • 如何处理 @Lazy 注解?

源码查看足迹可以参考下面的类:

Springboot 启动类 main() 调用 org.springframework.boot.SpringApplication#run()

org.springframework.boot.SpringApplication#refreshContext()

org.springframework.context.support.AbstractApplicationContext#refresh()

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean()org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessProperties()

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance

org.springframework.beans.factory.support.ConstructorResolver#autowireConstructor

org.springframework.beans.factory.support.ConstructorResolver#resolvePreparedArguments

org.springframework.beans.factory.support.ConstructorResolver#resolveAutowiredArgument

org.springframework.beans.factory.config.AutowireCapableBeanFactory#resolveDependency()

这里摘取了处理构造器依赖的几个核心方法来解释@Lazy 如何解决循环依赖,因为 UserService 类的构造器注入 OrderService 是强依赖关系,因此会经过 AbstractAutowireCapableBeanFactory#createBeanInstance() 中关于构造器逻辑代码:

在 autowireConstructor(beanName, mbd, ctors, args) 方法会调用 ConstructorResolver#resolvePreparedArguments(),再进入 ConstructorResolver#resolveAutowiredArgument(),再进入 DefaultListableBeanFactory#resolveDependency(),resolveDependency()方法的 getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary 逻辑就是针对 Lazy 情况进行处理:判断构造器参数是有@Lazy 注解,有则通过 buildLazyResolutionProxy 生成代理对象,无则直接返回 beanName。而在 buildLazyResolutionProxy()里会生成 一个 TargetSource 对象来和代理对象相关联。部分源码如下:

通过上面核心代码的解读,我们可以知道,构造器(参数)增加 @Lazy 注解后,Spring 不会去初始化参数对应类的实例,而是返回它的一个代理对象,解决了循环依赖问题,逻辑可以抽象为下图:

尽管循环依赖的问题解决了,但是,UserService 类依赖的只是 OrderService 的一个代理对象。因此,我们自然会好奇:当调用 orderService.getOrder()时,Spring 是如何找到 OrderService 的真实对象呢?

从上文知道,注入给 UserService 类的是一个代理,说起代理就不得不说起 Spring AOP 机制,它就是通过动态代理实现的(JDK 动态代理 和 CGLib 动态代理)。因为 OrderService 并非接口,因此不能使用 JDK 动态代理,只能通过 CGLib 进行代理,CGLib 源码如下:

这里抽取了 CGLib 动态代理核心的 3 步:

通过 CGLib 核心的 3 步可以解释,Spring 中代理类是如何与真实对象进行关联,因此,orderService 关联到真实对象可以抽象成下图:

另外,我们通过 3 张 IDEA debugger 截图来佐证上述理论:

到此,我们通过源码分析了 Spring是如何解决构造器循环依赖的。

四、常见面试题 


Spring如何解决循环依赖?

需要从变量注入,setter注入,构造器注入 三个角度去分析,切莫笼统的说成三层缓存,同时还要注意 Spring版本,因为不同的版本,处理的方式有差异。

构造器产生的循环依赖为什么Spring无法自动处理?

这个的从类加载机制来说明,变量注入,setter注入和构造器注入的差异性。

五、总结 


  • Spring 构造器注入产生的循环依赖,通常可以有 3种解决办法:重构代码、变量注入或setter注入、@Lazy 注解。推荐使用 @Lazy 注解;
  • @Lazy 注解解决思路是:初始化时注入代理对象,真实调用时通过 Spring AOP 动态代理去关联真实对象,然后再通过反射完成调用;
  • @Lazy 注解加在构造器上,作用域为构造器所有参数,加在构造器某个参数上,作用域为该参数;
  • @Lazy 注解 作用在接口上,使用 JDK 动态代理,作用在类上,使用 CGLib 动态代理;
  • 日常开发,推荐使用构造器注入方式;