#yyds干货盘点#Spring源码三千问@Lazy延迟加载与延迟注入有什么区别()

一箫一剑平生意,负尽狂名十五年。这篇文章主要讲述#yyds干货盘点#Spring源码三千问@Lazy延迟加载与延迟注入有什么区别?相关的知识,希望能为你提供帮助。
@TOC
前言@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)。 < br /> 这种情况,可以使用 org.springframework.beans.factory.ObjectFactoryjavax.inject.Provider 来替代解决
从源码来分析延迟加载延迟注入 的区别:
我们主要来分析一下最常用的两种方式: 标记在属性上;标记在普通的 bean 类上。
@Lazy 主要使用在循环依赖报错的场景,所以,我们先来解释一下循环依赖的概念。
循环依赖的概念在分析之前,我们对循环依赖的概念做一下解析。
场景(--> 表示依赖) 说明
A --> B --> A 称做 A 被循环依赖了。(或者:A 产生了循环依赖)
B --> A --> B 称做 B 被循环依赖了。(或者:B 产生了循环依赖)
可能有些同学觉得很奇怪, A --& gt; B --& gt; AB --& gt; A --& gt; B 不是一样吗,将它还原成代码不是同一个场景吗?
还原成代码如下:
@Service public class A @Autowired private B b; @Service public class B @Autowired private A a;

A --& gt; B --& gt; AB --& gt; A --& gt; B 的确是两种不同的场景。
如果 bean A 先加载,那么产生循环依赖的顺序就是A --& gt; B --& gt; A ,循环依赖的主体就是 A。
如果 bean B 先加载,那么产生循环依赖的顺序就是B --& gt; A --& gt; B ,循环依赖的主体就是 B。
也就是说:A, B 加载顺序的不同,会导致产生循环依赖的主体不同。
我们分析一下,如果是A --& gt; B --& gt; A 的场景,那么,A 加载时,在 populateBean() 时,会去注入 B,从而触发 B 的加载。
当 B 加载时,在 populateBean() 时,会去注入 A,此时会通过 A 对应的三级缓存获取到 bean A 的早期引用。
所以,当 A 产生循环依赖,被循环注入时,会通过 A 对应的三级缓存来获取 bean A 的早期引用。
也就是说:如果 beanX 产生了循环依赖的话,最后会使用 beanX 的三级缓存来获取 beanX 的早期引用。(这一点是很重要的,需要理解清楚)
@Lazy 标记在属性上@Lazy 最常用的场景是当我们遇到循环依赖报错的时候,将它标记在 @Autowired/@Resource 注入的属性上。
例如:
A --& gt; B --& gt; 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() 来进行依赖的解析
#yyds干货盘点#Spring源码三千问@Lazy延迟加载与延迟注入有什么区别()

文章图片

可以看到,解析依赖时,就有对 @Lazy 标记的属性进行处理。具体是通过 AutowireCandidateResolver#getLazyResolutionProxyIfNecessary() 来获取到依赖属性的代理。
也就是说被 @Lazy 标记的属性,最终注入的是一个代理类,在 Spring 文档中称之为:lazy-resolution proxy(延迟解析代理)
既然注入的是一个代理,那么,也就不会去触发依赖 bean 的解析和加载了,这样,循环依赖的链条到此就终止了。也就解决了循环依赖的报错问题了。
所以,@Lazy 标记在依赖注入的属性上,最终会注入一个 lazy-resolution proxy(延迟解析代理)。
@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 --& gt; A --& gt; B 的场景了,也就是 B 产生了循环依赖。
当 B 通过 @Autowired 注入 A 时,也就是 populateBean() 填充依赖时,会触发 A 的加载,走 getBean() 的流程。
当 A 通过 @Autowired 注入 B 时,可以通过三级缓存获取到 B 的早期引用。B 是普通的 bean,被循环依赖是没有问题的。
所以,@Lazy 标记在 bean A 类上,也解决了循环依赖的问题。
如何做到 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 的初始化流程的。
小结本文分析了 @Lazy 延迟加载 和 延迟注入 的使用和区别:
  1. 延迟加载
    将 @Lazy 标记在 bean 类上,这样,Spring 容器就不会主动加载这个 bean。
    但当这个 bean 被依赖注入时,就会触发这个 bean 的加载。
  2. 延迟注入
    将 @Lazy 标记在依赖注入的属性上,这样,populateBean() 时就不会注入真正的依赖,而是注入一个延迟解析代理(lazy-resolution proxy),这样就不会触发这个 bean 的加载。
    当 Spring 容器进行 finishBeanFactoryInitialization() 时,就会挨个初始化所有的非懒加载 bean,此时就会触发这个依赖 bean 的加载了。
只使用 延迟加载 或者 延迟注入 的话,bean 通常都是在容器启动完成后就完成了加载过程。
如果想让一个 beanX 在真正使用的时候才进行加载的话,就需要使用 延迟加载 + 延迟注入
如果本文对你有所帮助,欢迎点赞收藏!
有关 Spring 源码方面的问题欢迎留言一起交流...
公众号后台回复:下载IoC 或者 下载AOP 可以免费下载源码测试工程…
【#yyds干货盘点#Spring源码三千问@Lazy延迟加载与延迟注入有什么区别()】阅读更多文章,请关注公众号: 老王学源码
#yyds干货盘点#Spring源码三千问@Lazy延迟加载与延迟注入有什么区别()

文章图片

博主好课推荐:
课程 地址
Dubbo源码解读——通向高手之路 https://edu.51cto.com/sd/2e565
正则表达式基础与提升 https://edu.51cto.com/sd/59587

    推荐阅读