【Spring源码三千问】@Lazy延迟加载与延迟注入有什么区别?

前言

@Lazy 最常用的场景是,当我们遇到循环依赖报错的时候,将报错的 bean 使用 @Lazy 进行标记。
那么,为什么加上 @Lazy 之后就不会报错了呢?

我们还可以发现,@Lazy 标记在 @Autowired/@Resource 注入的属性上,或者对应的 bean 类上,都可以解决这种循环依赖报错的问题。
那么,不同的使用方式有什么区别呢?会带来什么样不同的效果呢?

带着这些疑问,我们开始源码之旅…

版本约定

Spring 5.3.9 (通过 SpringBoot 2.5.3 间接引入的依赖)

正文

为了了解 @Lazy 的原理,我们可以先看它的源码定义,站在一个高的层次来整体的认识一下 @Lazy。

@Lazy 的定义和使用范围

@Lazy 的定义如下:

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lazy {

	/**
	 * Whether lazy initialization should occur.
	 */
	boolean value() default true;
}

可以看出,@Lazy 可以使用在类、方法、构造函数、参数、属性上

@Lazy 的作用

通过源码的注释,我们可以先得出一部分@Lazy 作用的结论。
源码的注释如下:

Indicates whether a bean is to be lazily initialized.
May be used on any class directly or indirectly annotated with @Component or on methods annotated with @Bean.
If this annotation is not present on a @Component or @Bean definition, eager initialization will occur. If present and set to true, the @Bean or @Component will not be initialized until referenced by another bean or explicitly retrieved from the enclosing BeanFactory. If present and set to false, the bean will be instantiated on startup by bean factories that perform eager initialization of singletons.
If Lazy is present on a @Configuration class, this indicates that all @Bean methods within that @Configuration should be lazily initialized. If @Lazy is present and false on a @Bean method within a @Lazy-annotated @Configuration class, this indicates overriding the 'default lazy' behavior and that the bean should be eagerly initialized.
In addition to its role for component initialization, this annotation may also be placed on injection points marked with org.springframework.beans.factory.annotation.Autowired or javax.inject.Inject: In that context, it leads to the creation of a lazy-resolution proxy for all affected dependencies, as an alternative to using org.springframework.beans.factory.ObjectFactory or javax.inject.Provider.

通过源码的注释,我们可以了解到 @Lazy 在不同的场景下提供的功能有细微的差别:

使用范围起到的作用
标记在普通 bean 类上被标记的 bean 会延迟加载
标记在 @Configuration 类上被标记的 @Configuration 类及其内部的 @Bean 都会延迟加载
标记在 @Autowired/@Resource 的属性上被标记的属性会延迟注入(注意:属性对应的 bean 并不会延迟加载)
标记在参数上被标记的参数会解析成一个延迟解析代理(lazy-resolution proxy)。
这种情况,可以使用 org.springframework.beans.factory.ObjectFactoryjavax.inject.Provider 来替代解决

从源码来分析延迟加载延迟注入 的区别:
我们主要来分析一下最常用的两种方式: 标记在属性上;标记在普通的 bean 类上。

其他的使用场景,读者感兴趣的话,可以通过作者的思路自己去探索,这样可以让自己更好的理解源码

@Lazy 主要使用在循环依赖报错的场景,所以,我们先来解释一下循环依赖的概念。

循环依赖的概念

在分析之前,我们对循环依赖的概念做一下解析。

场景(–> 表示依赖)说明
A --> B --> A称做 A 被循环依赖了。(或者:A 产生了循环依赖)
B --> A --> B称做 B 被循环依赖了。(或者:B 产生了循环依赖)

可能有些同学觉得很奇怪, A --> B --> AB --> A --> B 不是一样吗,将它还原成代码不是同一个场景吗?

还原成代码如下:

@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

A --> B --> AB --> A --> B 的确是两种不同的场景。
如果 bean A 先加载,那么产生循环依赖的顺序就是 A --> B --> A ,循环依赖的主体就是 A。
如果 bean B 先加载,那么产生循环依赖的顺序就是 B --> A --> B ,循环依赖的主体就是 B。

也就是说:A, B 加载顺序的不同,会导致产生循环依赖的主体不同。

我们分析一下,如果是 A --> B --> A 的场景,那么,A 加载时,在 populateBean() 时,会去注入 B,从而触发 B 的加载。
当 B 加载时,在 populateBean() 时,会去注入 A,此时会通过 A 对应的三级缓存获取到 bean A 的早期引用。
所以,当 A 产生循环依赖,被循环注入时,会通过 A 对应的三级缓存来获取 bean A 的早期引用。

也就是说:如果 beanX 产生了循环依赖的话,最后会使用 beanX 的三级缓存来获取 beanX 的早期引用。(这一点是很重要的,需要理解清楚)

@Lazy 标记在属性上

@Lazy 最常用的场景是当我们遇到循环依赖报错的时候,将它标记在 @Autowired/@Resource 注入的属性上。

例如:
A --> B --> A 的场景,bean A 是一个被 @Async 标记而生产的代理 bean

@Service
public class A {
    @Autowired
    @Lazy // 延迟注入 B
    private B b;

    @Async
    public void m1() {
    }
}

@Service
public class B {
    @Autowired
    private A a;
}

如果不添加 @Lazy,会得到如下的错误信息:

Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'p1Service': 
Bean with name 'p1Service' has been injected into other beans [p2Service] in its raw version as part of a circular reference, but has eventually been wrapped. 
This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.

添加上 @Lazy 之后,起到了什么作用呢?

之前讲 IoC 依赖注入时,讲到 @Autowired 注入是通过 AutowiredFieldElement 来进行处理的。
最终,它会调用 DefaultListableBeanFactory#resolveDependency() 来进行依赖的解析
resolveDependency2

可以看到,解析依赖时,就有对 @Lazy 标记的属性进行处理。具体是通过 AutowireCandidateResolver#getLazyResolutionProxyIfNecessary() 来获取到依赖属性的代理。
也就是说被 @Lazy 标记的属性,最终注入的是一个代理类,在 Spring 文档中称之为:lazy-resolution proxy(延迟解析代理)

既然注入的是一个代理,那么,也就不会去触发依赖 bean 的解析和加载了,这样,循环依赖的链条到此就终止了。也就解决了循环依赖的报错问题了。

虽然 B 被 @Lazy 标记之后会延迟注入,但是并不表示 bean B 不会加载了。
由于 B 同时是一个单例 bean,所以,在 AbstractApplicationContext#finishBeanFactoryInitialization() 时,会触发 bean B 的加载。

所以,@Lazy 标记在依赖注入的属性上,最终会注入一个 lazy-resolution proxy(延迟解析代理)。

做为一个嗅觉敏锐的技术人,从上图我们还可以看出 Spring 对 ObjectFactoryObjectProvider 类型的依赖进行了特殊处理,注入的是一个 new 出来的 DependencyObjectProvider 对象。
跟下源码,就可以发现,ObjectFactoryObjectProvider 类型的依赖在注入时,不会马上去加载真正的 bean,而是在使用 DependencyObjectProvider#getObject() 才会触发 bean 的加载。
所以,在某种程度上来说,ObjectFactoryObjectProvider 可以替代 @Lazy 的功能。

@Lazy 标记在类上

上面的例子中,也可以将 @Lazy 标记在 bean 类上,也能解决问题,如下:

@Lazy // 延迟加载 A
@Service
public class A {
    @Autowired
    private B b;

    @Async
    public void m1() {
    }
}

@Service
public class B {
    @Autowired
    private A a;
}

将 @Lazy 标记在 bean A 上之后,bean A 就会延迟加载。也就是说 B 会先于 A 进行 bean 的初始化流程。
这样的话,就变成了 B --> A --> B 的场景了,也就是 B 产生了循环依赖。

当 B 通过 @Autowired 注入 A 时,也就是 populateBean() 填充依赖时,会触发 A 的加载,走 getBean() 的流程。
当 A 通过 @Autowired 注入 B 时,可以通过三级缓存获取到 B 的早期引用。B 是普通的 bean,被循环依赖是没有问题的。

所以,@Lazy 标记在 bean A 类上,也解决了循环依赖的问题。

当 @Lazy 标记在类上之后,这个类的 lazy 延迟加载属性就为 true 了,即: BeanDefinition#isLazyInit() == true

如何做到 bean 在真正使用的时候才进行加载?

上面的例子中,@Lazy 不管是标记在依赖注入的属性上,还是标记在 bean 类上,最终 A, B 两个 bean 都在 Spring 容器启动完成之后完成了加载流程。

如果我们想让 bean A 在真正使用的时候才进行加载,该怎么去配置呢?
答:需要两个步骤:

  1. 将 @Lazy 标记在 bean A 类上,使得 bean A 不会被 Spring 容器主动加载
  2. 将依赖注入 bean A 的地方使用 @Lazy 将属性进行标记

沿用上面的例子,代码如下:

@Lazy // 延迟加载 A
@Service
public class A {
    @Autowired
    private B b;

    @Async
    public void m1() {
    }
}

@Service
public class B {
    @Autowired
    @Lazy // 延迟注入 A
    private A a;
}

也就是说,如果我们想让 bean A 在真正使用的时候才通过 Spring 容器进行加载的话,就需要使用 @Lazy 将 A 标记成 延迟加载 和 延迟注入。
这样的话,在 Spring 容器启动时,所有的地方都不会主动去加载 bean A 了。

如何验证 bean 是在使用的时候才进行加载的?

如何能快速的验证 bean A 是在使用的时候才进行加载的呢?
我们可以为 bean A 增加一个默认的构造函数:

@Lazy // 延迟加载 A
@Service
public class A {
    @Autowired
    private B b;

   public P1Service() {
      log.info("A创建实例...");
   }

    @Async
    public void m1() {
       log.info("异步执行--A中打印B:" + b + "--" + b.getClass());
    }
}

启动容器后,会发现构造函数中的日志并不会打印,而是在调用 A.m1() 方法时才会打印。
这样也就说明了 A 是在真正使用的时候才去创建的实例,完成 bean 的初始化流程的。

也可以通过源码调试的方式,可以更加直观的观察 A 的加载流程是什么时候进行的。
只需要将断点打在 bean 的创建流程中即可,即: AbstractAutowireCapableBeanFactory#createBeanInstance()
了解 bean 的创建流程请戳:bean 的创建过程

小结

本文分析了 @Lazy 延迟加载 和 延迟注入 的使用和区别:

  1. 延迟加载
    将 @Lazy 标记在 bean 类上,这样,Spring 容器就不会主动加载这个 bean。
    但当这个 bean 被依赖注入时,就会触发这个 bean 的加载。

  2. 延迟注入
    将 @Lazy 标记在依赖注入的属性上,这样,populateBean() 时就不会注入真正的依赖,而是注入一个延迟解析代理(lazy-resolution proxy),这样就不会触发这个 bean 的加载。
    当 Spring 容器进行 finishBeanFactoryInitialization() 时,就会挨个初始化所有的非懒加载 bean,此时就会触发这个依赖 bean 的加载了。

只使用 延迟加载 或者 延迟注入 的话,bean 通常都是在容器启动完成后就完成了加载过程。
如果想让一个 beanX 在真正使用的时候才进行加载的话,就需要使用 延迟加载 + 延迟注入

使用 ObjectFactoryObjectProvider 同样也可以达到类似延迟注入的效果。


如果本文对你有所帮助,欢迎点赞收藏!

源码测试工程下载:
老王读Spring IoC源码分析&测试代码下载
老王读Spring AOP源码分析&测试代码下载

公众号后台回复:下载IoC 或者 下载AOP 可以免费下载源码测试工程…

阅读更多文章,请关注公众号: 老王学源码
gzh


系列博文:
【老王读Spring AOP-0】SpringAop引入&&AOP概念、术语介绍
【老王读Spring AOP-1】Pointcut如何匹配到 join point
【老王读Spring AOP-2】如何为 Pointcut 匹配的类生成动态代理类
【老王读Spring AOP-3】Spring AOP 执行 Pointcut 对应的 Advice 的过程
【老王读Spring AOP-4】Spring AOP 与Spring IoC 结合的过程 && ProxyFactory 解析
【老王读Spring AOP-5】@Transactional产生AOP代理的原理
【老王读Spring AOP-6】@Async产生AOP代理的原理
【Spring 源码阅读】Spring IoC、AOP 原理小总结

相关阅读:
【Spring源码三千问】Spring动态代理:什么时候使用的 cglib,什么时候使用的是 jdk proxy?
【Spring源码三千问】Advice、Advisor、Advised都是什么接口?
【Spring源码三千问】没有AspectJ,Spring中如何使用SpringAOP、@Transactional?
【Spring源码三千问】Spring AOP 中 AbstractAdvisorAutoProxyCreator、AbstractAdvisingBeanPostProcessor的区别
【Spring 源码三千问】同样是AOP代理bean,为什么@Async标记的bean循环依赖时会报错?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老王学源码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值