网站建设是广告吗,网站开发实用技术2.8.5,怎样建设网站优化,oa官网下载总结#xff1a;Spring创建Bean循环依赖问题与Lazy注解使用详解 一前提知识储备#xff1a;1.Spring Bean生命周期机制#xff08;IOC#xff09;2.Spring依赖注入机制#xff08;DI#xff09;#xff08;1#xff09;Autowired注解标注属性set方法注入#xff08;2Spring创建Bean循环依赖问题与Lazy注解使用详解 一·前提知识储备1.Spring Bean生命周期机制IOC2.Spring依赖注入机制DI1Autowired注解标注属性set方法注入2Autowired注解标注属性注入3Autowired注解标注构造方法注入也可用于存在多个构造方法时手动指明Spring框架使用哪个构造方法创建Bean 3.了解一定的JVM类加载原理机制1第一次使用new关键字调用类构造方法创建对象时会触发类加载机制然后再执行类实例化机制2每创建一个对象就会触发类实例化机制一次注意不是类加载机制3先产生一个对象的内存地址再调用类的 clinit方法由编译器自动生成用于初始化静态变量和执行静态初始化块的代码最后才执行类的构造方法。4因此一个对象的内存地址可以在构造方法结束之前暴露出来。但Spring中所谓的提前暴露bean对象地址都是指构造方法结束之后暴露而不是构造方法结束之前暴露 4.Lazy注解使用1使用地方类、方法、构造方法、方法参数、属性字段。必须标注在IOC容器管理的bean上才会生效否则无效果2功能作用Springboot项目启动时懒加载Bean。即只会分配对象地址并暴露在IOC容器里面但不会立即执行该Bean的实例化操作构造方法3标注在类上示例延迟创建4标注在方法上示例延迟注入5标注在构造方法参数上示例延迟注入。直接标注在构造方法上面没效果加在构造方法参数上才有效果6标注在属性字段上示例 二·什么是Spring Bean循环依赖问题1.类A中存在类B属性类B中存在类C属性类C中存在类A属性等类似情况2.类A中存在类A属性等类似情况3.Spring项目启动会报异常提示 三·如何解决Spring Bean循环依赖方案一尽量避免双向依赖根本上解决设计时尽量避免双向依赖因为双向依赖很容易导致循环依赖的发生。推荐方案二使用构造函数注入 Lazy注解推荐方案三使用字段属性注入 application.yml配置方案四使用字段属性注入 Lazy注解推荐 四·Spring解决循环依赖的底层原理概述1.三级缓存容器2.为什么Bean 都已经实例化了还需要一个生产 Bean 的ObjectFactory工厂呢3.三级缓存容器工作流程示例假设现在需要实例化两个对象A、B1不存在循环依赖、或者存在循环依赖但不存在AOP操作2存在循环依赖、存在AOP操作 4.为什么要再包装一层ObjectFactory对象存入三级缓存呢说是为了解决Bean对象存在AOP代理情况那么直接生成代理对象半成品Bean放入二级缓存中这不就可以省略三级缓存了吗所以这使用三级缓存的意义在哪里5.使用构造器注入造成的循环依赖三级缓存机制无法自动解决6.为什么构造器注入配合Lazy注解就能解决循环依赖问题呢7·参考文献链接 一·前提知识储备
1.Spring Bean生命周期机制IOC 注意bean实例化就是指调用构造方法创建bean的过程
参考详情文献
https://blog.csdn.net/riemann_/article/details/118500805
2.Spring依赖注入机制DI
参考详情文献 https://juejin.cn/post/6857406008877121550#heading-17
1Autowired注解标注属性set方法注入
Component
public class Dog {// 私有成员变量private Cat cat;// 使用Autowired注解标注setter方法Autowiredpublic void setCat(Cat cat) {this.cat cat;}// 其他业务逻辑...
}2Autowired注解标注属性注入
Component
public class Dog {Autowiredprivate Cat cat;
}3Autowired注解标注构造方法注入也可用于存在多个构造方法时手动指明Spring框架使用哪个构造方法创建Bean
Component
public class Dog {private Cat cat;Autowiredpublic Dog(Cat cat) {this.cat cat;}
}3.了解一定的JVM类加载原理机制
参考详情文献 https://blog.csdn.net/weixin_48033662/article/details/135246047
1第一次使用new关键字调用类构造方法创建对象时会触发类加载机制然后再执行类实例化机制
2每创建一个对象就会触发类实例化机制一次注意不是类加载机制
3先产生一个对象的内存地址再调用类的 clinit方法由编译器自动生成用于初始化静态变量和执行静态初始化块的代码最后才执行类的构造方法。
4因此一个对象的内存地址可以在构造方法结束之前暴露出来。但Spring中所谓的提前暴露bean对象地址都是指构造方法结束之后暴露而不是构造方法结束之前暴露
示例代码如下
public class Example {private String value;private Example next;public Example(String value) {// 开启新线程并让其尝试访问this.valuenew Thread(() - {//将当前对象内存地址赋给next属性提前暴露当前对象地址System.out.println(异步线程next next);System.out.println(新线程 this.value);}).start();//将当前对象内存地址赋给next属性提前暴露当前对象地址next this;System.out.println(主线程next next);// 睡眠一段时间模拟初始化耗时操作try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}this.value value;System.out.println(构造方法内部value已初始化为 this.value);}public static void main(String[] args) {Example example new Example(Hello, World!);// 此时example引用已经可以获取但value字段还未初始化System.out.println(example example);}
}4.Lazy注解使用
1使用地方类、方法、构造方法、方法参数、属性字段。必须标注在IOC容器管理的bean上才会生效否则无效果
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;}2功能作用Springboot项目启动时懒加载Bean。即只会分配对象地址并暴露在IOC容器里面但不会立即执行该Bean的实例化操作构造方法
注意 1简述虽然会创建该类的一个对象但是不会执行该对象的构造方法 2没有标注该注解或者进行特定配置时Springboot项目启动时会默认将扫描范围内的所有Bean进行实例化操作既创建该类对象又执行该对象的构造方法
3标注在类上示例延迟创建
Lazy
Component
public class Cat {private String name;private int age;public Cat() {System.out.println(Cat无参构造方法执行);}
}4标注在方法上示例延迟注入
注意Lazy注解 Configuration注解同时标注在类上会作用所有Bean注册的类实例
Configuration
public class IocConfig {LazyBeanpublic Dog dog(){return new Dog();}
}5标注在构造方法参数上示例延迟注入。直接标注在构造方法上面没效果加在构造方法参数上才有效果
Component
public class Cat {private Dog dog;Autowiredpublic Cat(Lazy Dog dog) {this.dog dog;}
}6标注在属性字段上示例
Component
public class Cat {LazyAutowiredprivate Dog dog;
}二·什么是Spring Bean循环依赖问题
1.类A中存在类B属性类B中存在类C属性类C中存在类A属性等类似情况 示例代码 类A
Component
public class A {Autowiredprivate B b;
}类B
Component
public class B {Autowiredprivate C c;
}类C
Component
public class C {Autowiredprivate A a;
}2.类A中存在类A属性等类似情况 示例代码
Component
public class A {Autowiredprivate A a;
}3.Spring项目启动会报异常提示 三·如何解决Spring Bean循环依赖
方案一尽量避免双向依赖根本上解决设计时尽量避免双向依赖因为双向依赖很容易导致循环依赖的发生。推荐
方案二使用构造函数注入 Lazy注解推荐
1当有两个类互相依赖时只要在任意一个类的构造方法上将另一方参数标注Lazy注解即可打破循环引用当然两个类构造方法互相都加也行
2若很多个类才构成循环依赖则建议直接将每个类的构造方法上对方参数都标注Lazy注解否则你就一个个去分析循环依赖然后再单个加Lazy注解吧
注意构造函数注入是解决循环依赖问题的最佳方式之一因为它能够保证Bean在实例化时所有依赖已经注入。
示例代码
Component
public class Cat {private Dog dog;/*** 构造方法注入属性对象时被标注Lazy注解的参数对象并不会直接触发Dog类的实例化操作* 而是会先放入一个dog的代理对象在这里当后面该dog的代理对象被第一次调用时才会触发Dog的实例化操作* 但是生成的实例化对象并不会替换原本的代理对象只会将实例化对象注入代理对象里面。* 代理对象在其内部维护对实际实例的引用在后续每次调用时代理对象都会将请求转发给已经实例化的原生对象并返回原生对象的方法执行结果* param dog*/Autowiredpublic Cat(Lazy Dog dog) {//注意这里不能在构造方法里面调用dog对象一旦调用就会立即触发Dog的实例化操作this.dog dog;}
}Component
public class Dog {private Cat cat;//只要保证一方加了Lazy注解就行Autowiredpublic Dog(Lazy Cat cat) {this.cat cat;}
}方案三使用字段属性注入 application.yml配置
application.yml配置如下
#允许Spring循环引用配置默认是关闭的
spring:main:allow-circular-references: true1开启后Springboot会主动尝试利用三级缓存机制来打破循环引用。如果实在打破不了就会抛循环引用异常让用户自己解决可以配合Lazy注解解决
2一般使用Autowired注入属性的循环依赖都可以被Springboot主动打破
3Spring无法解决单纯由构造器注入导致的循环依赖因为Java语言本身的特性决定了构造器调用必须在一个实例化阶段完成无法通过延后注入的方式打破循环可以配合Lazy注解解决
4非单例bean造成的循环引用Spring也无法自动解决
代码示例
Component
public class Cat {Autowiredprivate Dog dog;//手动指明使用哪个构造方法创建beanAutowiredpublic Cat() {System.out.println(Cat无参构造方法执行);}
}Component
public class Dog {Autowiredprivate Cat cat;Autowiredpublic Dog() {System.out.println(Dog无参构造方法执行);}
}方案四使用字段属性注入 Lazy注解推荐
1当有两个类互相依赖时只要在任意一个类中另一方的属性字段上标注Lazy注解即可打破循环引用当然两个类相都加也行
2若很多个类才构成循环依赖则建议直接将每个类的另一方属性字段都标注Lazy注解否则你就一个个去分析循环依赖然后再单个加Lazy注解吧
代码示例
Component
public class Cat {LazyAutowiredprivate Dog dog;Autowiredpublic Cat() {System.out.println(Cat无参构造方法执行);}
}Component
public class Dog {LazyAutowiredprivate Cat cat;Autowiredpublic Dog() {System.out.println(Dog无参构造方法执行);}
}四·Spring解决循环依赖的底层原理概述
1.三级缓存容器
public class DefaultSingletonBeanRegistry ... {//1、最终单例Bean容器里面bean都已经完成了实例化、属性注入、初始化称之为一级缓存MapString, Object singletonObjects new ConcurrentHashMap(256);//2、早期Bean单例池缓存半成品对象完成了实例化但未完成属性注入、未执行初始化 init 方法且当前对象已经被其他对象引用了称之为二级缓存MapString, Object earlySingletonObjects new ConcurrentHashMap(16);//3、单例Bean的工厂池缓存半成品对象完成了实例化但未完成属性注入、未执行初始化 init 方法对象未被引用使用时在通过工厂创建Bean称之为三级缓存MapString, ObjectFactory? singletonFactories new HashMap(16);
}2.为什么Bean 都已经实例化了还需要一个生产 Bean 的ObjectFactory工厂呢
这跟循环依赖期间存在AOP操作有关
1若不存在循环依赖、或者存在循环依赖但没有AOP操作时ObjectFactory的getObject()方法返回的是原本bean实例对象
2若存在循环依赖且存在AOP操作时ObjectFactory的getObject()方法返回的是原本bean实例的代理对象提前创建代理对象原本Spring的设计模式是bean实例化、属性注入、初始化之后再创建代理对象的但这种特殊情况为了解决循环依赖只能提前创建。因此Spring框架在极端情况下可能出现bean后置处理器方法在属性注入、初始化方法前执行但问题也能解决
3.三级缓存容器工作流程示例假设现在需要实例化两个对象A、B
1不存在循环依赖、或者存在循环依赖但不存在AOP操作
1.先从一级、二级、三级缓存容器找对象A发现没有
2.执行A的构造方法进行实例化但未执行属性注入、初始化操作PostConstructA 只是一个半成品。
3.将早期的A暴露出去放到三级缓存容器singletonFactories中bean对象会被ObjectFactory包装起来
4.A进行属性注入发现没有依赖其他未创建的对象
5.A进行初始化操作PostConstruct完成bean创建工作
6.然后会调用addSingleton方法将自己丢到一级缓存中并将自己从二级、三级缓存中移除实际只有三级缓存容器有对象
7.再按照上述步骤创建B对象从步骤1开始
......2存在循环依赖、存在AOP操作
1.先从一级、二级、三级缓存容器找对象A发现没有
2.实例化A此时 A 还未完成属性填充和初始化方法PostConstruct的执行A 只是一个半成品。
3.提前暴露A引用为 A 创建一个 Bean 工厂并放入到 singletonFactories 中。
4.属性注入A发现 A 需要注入 B 对象但是一级、二级、三级缓存均为发现对象 B。
5.实例化B此时 B 还未完成属性填充和初始化方法PostConstruct的执行B 只是一个半成品。
6.提前暴露B引用为 B 创建一个 Bean 工厂并放入到 singletonFactories 中。
7.属性注入B发现 B 需要注入 A 对象此时在一级、二级未发现对象 A但是在三级缓存中发现了对象 A从三级缓存中得到对象 A
并将对象 A 放入二级缓存中同时删除三级缓存中的对象 A。注意此时的 A 还是一个半成品并没有完成属性填充和执行初始化方法
然后将对象 A 注入到对象 B 中对象 B 完成属性填充
8.执行B初始化方法B对象彻底创建完成。
9.将B对象放入到一级缓存中同时删除三级缓存中的对象 B。此时对象 B 已经是一个成品
10.对象 A 得到对象 B将对象 B 注入到对象 A 中对象 A 完成属性填充。对象 A 得到的是一个完整的对象 B
11.执行A初始化方法A对象彻底创建完成
12.将A对象放入到一级缓存中同时删除二级缓存中的对象 A。此时对象 A 已经是一个成品4.为什么要再包装一层ObjectFactory对象存入三级缓存呢说是为了解决Bean对象存在AOP代理情况那么直接生成代理对象半成品Bean放入二级缓存中这不就可以省略三级缓存了吗所以这使用三级缓存的意义在哪里 1正常情况下没有循环依赖 1-1Spring都是在完全创建好Bean之后才创建对应的代理对象 2为了处理循环依赖Spring有三种选择 2-1不管有没有循环依赖直接将半成品bean对象放入二级缓存用于解决循环依赖问题。会导致无法注入AOP代理对象只能是原生bean实例对象 2-2不管有没有循环依赖都提前创建好代理对象并将代理对象放入二级缓存出现循环依赖时其他对象直接就可以取到代理对象并注入。会导致无法注入原生bean实例对象只能是AOP代理对象 2-3加一个中间层只有出现循环依赖且存在AOP操作时才会提前生成代理对象其他情况下都是返回原生bean实例对象。这样就极大可能保证Spring按照原本AOP代理设计模式进行创建bean且又能解决循环依赖问题
注意虽然加一个中间层看似极大的兼顾了两种情况。但Spring框架在极端情况下还是可能出现bean后置处理器方法在属性注入、初始化方法前执行但该问题也能通过合理手段解决
5.使用构造器注入造成的循环依赖三级缓存机制无法自动解决
因为构造器循环依赖是发生在bean实例化阶段此时连bean的构造方法都没执行完早期对象都无法创建出来。因此也无法放到三级缓存。三级缓存只能是在bean实例化之后才能起到作用
6.为什么构造器注入配合Lazy注解就能解决循环依赖问题呢
1示例代码
Component
public class Cat {private Dog dog;/*** 构造方法注入属性对象时被标注Lazy注解的参数对象并不会直接触发Dog类的实例化操作* 而是会先放入一个dog的代理对象在这里当后面该dog的代理对象被第一次调用时才会触发Dog的实例化操作* 但是生成的实例化对象并不会替换原本的代理对象只会将实例化对象注入代理对象里面。* 代理对象在其内部维护对实际实例的引用在后续每次调用时代理对象都会将请求转发给已经实例化的原生对象并返回原生对象的方法执行结果* param dog*/Autowiredpublic Cat(Lazy Dog dog) {//注意这里不能在构造方法里面调用dog对象一旦调用就会立即触发Dog的实例化操作this.dog dog;}
}2使用构造器注入 Lazy注解解决循环依赖的工作流程示例一句话Lazy 注解是通过建立一个中间代理层来破解循环依赖的
1.从一、二、三级缓存总找不到cat对象开始实例化cat对象
2.由于Cat构造器参数dog上标注了Lazy注解因此Spring执行构造方法时会创建一个代理对象赋给dog属性
3.cat对象成功创建实例对象放入三级缓存里面
4.cat对象再执行属性注入、初始化操作完成最终bean创建
5.cat对象最后放入单例池里面一级缓存删除二、三级缓存里面的cat对象
6.当cat对象的dog属性被第一次调用时dog此时是代理对象会触发Dog类的实例化操作生成实例化对象dog2
7.dog2对象再执行属性注入、初始化方法完成最终bean创建放入单例池一级缓存
8.Spring最后再将dog2对象注入到cat对象的dog属性的代理对象里面
生成的实例化对象并不会替换原本的代理对象只会将实例化对象注入代理对象里面。代理对象在其内部维护对实际实例的引用
在后续每次调用时代理对象都会将请求转发给已经实例化的原生对象并返回原生对象的方法执行结果7·参考文献链接
Spring 解决循环依赖必须要三级缓存吗 Spring循环依赖解决方案 Spring使用三级缓存解决循环依赖 spring中怎么解决循环依赖的问题 Spring系列第28篇Bean循环依赖详解