@Lazy原理分析——它为什么可以解决特殊的循环依赖问题?
@Lazy原理分析——它为什么可以解决特殊的循环依赖问题?
在Spring框架中,循环依赖是一个常见的问题,尤其是在使用构造函数注入时。虽然Spring默认解决了一些常规场景下的循环依赖问题,但某些特殊场景(如prototype类型的循环依赖、构造函数注入的循环依赖等)仍然需要额外的处理。本文将深入分析@Lazy注解的原理,解释它如何解决这些特殊的循环依赖问题。
版本约定
Spring 5.3.9 (通过 SpringBoot 2.5.3 间接引入的依赖)
@Lazy注解的工作原理
在分析@Resource与@Autowired的区别时,我们发现@Resource注入时会通过ResourceElement.java
处理@Lazy的逻辑。具体代码如下:
// CommonAnnotationBeanPostProcessor.ResourceElement#getResourceToInject()
protected Object getResourceToInject(Object target, String requestingBeanName) {
return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) :
getResource(this, requestingBeanName));
}
protected Object buildLazyResourceProxy(final LookupElement element, final @Nullable String requestingBeanName) {
TargetSource ts = new TargetSource() {
@Override
public Class<?> getTargetClass() {
return element.lookupType;
}
@Override
public boolean isStatic() {
return false;
}
@Override
public Object getTarget() {
return getResource(element, requestingBeanName);
}
@Override
public void releaseTarget(Object target) {
}
};
ProxyFactory pf = new ProxyFactory();
pf.setTargetSource(ts);
if (element.lookupType.isInterface()) {
pf.addInterface(element.lookupType);
}
ClassLoader classLoader = (this.beanFactory instanceof ConfigurableBeanFactory ?
((ConfigurableBeanFactory) this.beanFactory).getBeanClassLoader() : null);
return pf.getProxy(classLoader);
}
可以看出,被@Lazy标记的属性,在populateBean
注入依赖时,会直接注入一个proxy对象。并且,不会触发注入对象的加载。这样的话,就不会产生bean的循环加载问题了。
@Autowired对@Lazy的处理也是类似的,可以查看AutowiredFieldElement.java
中对@Lazy的处理。大同小异的。具体代码是在:ContextAnnotationAutowireCandidateResolver#getLazyResolutionProxyIfNecessary()
中
示例说明
假设存在以下循环依赖场景:
@Service
public class A {
private B b;
public A(@Lazy B b) {
this.b = b;
}
}
@Service
public class B {
private A a;
public B(@Lazy A a) {
this.a = a;
}
}
在这种场景下,Spring会报错:Requested bean is currently in creation: Is there an unresolvable circular reference?
但是,我们加上@Lazy之后,就能正常启动了!原因在于:
- 假设A先加载,在创建A的实例时,会触发依赖属性B的加载,在加载B时发现它是一个被@Lazy标记过的属性。
- 那么,就不会去直接加载B,而是产生了一个代理对象注入到了A中,这样A就能正常的初始化完成放入一级缓存了。
- 自然,B加载时,再去注入A就能直接从一级缓存中获取到A,这样B也能正常初始化完成了。
- 所以,循环依赖的问题就解决了!
@Lazy的两种使用方式
延迟加载
@Component
public class DemoLazy2 {
@Autowired
DemoLazy1 demoLazy1;
}
@Component
@Lazy
public class DemoLazy1 {
@Autowired
DemoLazy2 demoLazy2;
}
如图,无代理对象产生。原理在调整了bean的实例化顺序,DemoLazy1是先实例化的,但加了@Lazy注解,实例化被跳过,DemoLazy2实例化时触发了DemoLazy1的实例化,源码AbstractApplicationContext#finishBeanFactoryInitialization>
DefaultListableBeanFactory#preInstantiateSingletons(这里是轮询beanDefinitionNames实例化单例非懒加载bean,注入属性时,不会走这段逻辑,也就是说@Lazy加类上实例化时在这里被跳过,加属性上不走这个逻辑会生成代理对象)
延迟注入
@Component
public class DemoLazy2 {
@Autowired
@Lazy
DemoLazy1 demoLazy1;
}
@Component
public class DemoLazy1 {
@Autowired
DemoLazy2 demoLazy2;
}
如图,延迟依赖的bean是代理bean了。再看看上文说的,实际注入的对象与 Spring 容器中存放的对象不是同一个对象!!!,如图。
无论延迟加载还是延迟依赖都会实例化循环依赖的两个bean,只有两者一起使用,才是真正的懒加载,即使用到的时候再实例化。
延迟加载+延迟依赖
@Component
public class DemoLazy2 {
@Autowired
@Lazy
DemoLazy1 demoLazy1;
}
@Component
@Lazy
public class DemoLazy1 {
@Autowired
DemoLazy2 demoLazy2;
}
如图,单例池并没有DemoLazy1,正常情况它和DemoLazy2几乎是挨在一起的。
注意:被@Lazy标记的依赖属性比较特殊,实际注入的对象与Spring容器中存放的对象不是同一个对象!!!