先不去管中的循环依赖,我们先实现一个自定义注解,来模拟@的功能。
一、自定义注解模拟@
自定义Load注解,被该注解标识的字段java反射动态注入方法,将会进行自动注入
/**
* @author qcy
* @create 2021/10/02 13:31:20
*/
//只用在字段上
@Target(ElementType.FIELD)
//运行时有效,这样可以通过反射解析注解
@Retention(RetentionPolicy.RUNTIME)
public @interface Load {
新建A类与B类,其中A类中需要注入B
public class A {
@Load
private B b;
public B getB() {
return b;
}
}
public class B {
测试类
public class Main {
private static T getBean(Class clazz) throws IllegalAccessException, InstantiationException {
//[实例化][13]对象
T instance = clazz.newInstance();
//获取当前类中的所有字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//允许访问私有变量
field.setAccessible(true);
//判断字段是否被@Load注解修饰
boolean isUseLoad = field.isAnnotationPresent(Load.class);
if (!isUseLoad) {
continue;
}
//获取需要被注入的字段的class
Class fieldType = field.getType();
//递归获取字段的实例对象
Object fieldBean = getBean(fieldType);
//将实例对象注入到该字段中
field.set(instance, fieldBean);
}
return instance;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
A a = getBean(A.class);
System.out.println(a.getB().getClass());
}
最终能够打印出a对象中依赖的b的class类型
二、多例模式下的循环依赖
现在思考一个问题,如果b对象同时依赖a呢?也就是B类中需要注入A
现在直接把B类的代码改成以下的样子
public class B {
@Load
private A a;
public A getA() {
return a;
}
直接运行测试类,会发生什么呢?
出现了栈溢出!到底是哪里出问题了呢?
原来是,在实例化A后,属性注入阶段发现需要注入B的实例,于是去实例化B,B又需要依赖Ajava反射动态注入方法,因此去实例化A,一直依赖下去...
不难观察出,每调用一次,都会返回一个新的对象,也就是对应于多例模式。
多例模式中出现循环依赖,直接报出了,看来解决不了循环依赖,这也并不难理解
三、单例模式下使用缓存来解决循环依赖
如果这个时候,对于传入的同一个class,能够返回同一个实例,即单例模式,能否解决循环依赖呢?
大致的思路是,使用一个缓存map,将实例化好且属性注入完毕的对象缓存到该map中,下次直接使用即可。
可现在又遇到难题了,压根就创建不出来一个完整的A的实例对象啊,无法进行缓存。
既然无法直接将完成品放入到缓存中,那是否可以将实例对象分为两个阶段
半成品阶段
仅完成实例化,并没有完成属性注入
成品阶段
半成品完成属性注入
首先实例化A得到半成品a,接着将这个a放入到缓存中。然后实例化b时,注入缓存中半成品的a,得到成品b。最终再将成品b注入到半成品a中,此时a变为成品。
这个时候,a完成了实例化与属性注入,b也完成了实例化与属性注入,循环依赖好像就能解决了。
改下代码,直接上线!
public class Main {
//由类名可以获取到对应的实例对象
private static Map singletonObjects= new HashMap();
private static T getBean(Class clazz) throws IllegalAccessException, InstantiationException {
//先从缓存中获取
String className = clazz.getSimpleName();
if (singletonObjects.containsKey(className)) {
return (T) singletonObjects.get(className);
}
//实例化对象
T instance = clazz.newInstance();
//实例化完成后,就将这个半成品放入到缓存中
singletonObjects.put(className, instance);
//获取当前类中的所有字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//允许访问私有变量
field.setAccessible(true);
//判断字段是否被@Load注解修饰
boolean isUseLoad = field.isAnnotationPresent(Load.class);
if (!isUseLoad) {
continue;
}
//获取需要被注入的字段的class
Class fieldType = field.getType();
//递归获取字段的实例对象
Object fieldBean = getBean(fieldType);
//将实例对象注入到该字段中
field.set(instance, fieldBean);
}
return instance;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
A a1 = getBean(A.class);
A a2 = getBean(A.class);
System.out.println(a1 == a2);
System.out.println(a1.getB() == a1.getB());
B b = getBean(B.class);
System.out.println(a1.getB() == b);
}
运行后,将返回三个true,说明单例模式下的循环依赖是可以解决的。
大致的图是这样的
事情似乎到这里应该结束了,好的观众朋友们,咱们下期见。
四、多线程下隐藏的问题
以上的代码,在单线程的环境下,是没有问题的。可是放到多线程的环境中,可能就会出现空指针问题。
线程1刚把半成品a放入到缓存中,还未来得及将b注入进去。此时线程2直接在缓存中获取到了a,在尝试调用其所依赖的b的任何方法时,就会出现空指针异常。
因此上述代码,存在线程不安全的问题,怎么去解决呢?
很简单,我直接对方法加锁不就可以了吗?
对整个方法加锁确实可以解决问题,但运行性能会大打折扣。
第一次的创建与读缓存互斥,创建完成后的读与读不需要加锁。
其实问题的本质在于,缓存既存放半成品类型,又存放成品类型,导致线程根本不清楚拿到的实例的类型。
那能不能再创建一个缓存s,即早期单例对象,就存放半成品类型,先前的存放成品类型。
直接上代码
//成品缓存
private static final Map singletonObjects = new ConcurrentHashMap();
//半成品缓存
private static final Map earlySingletonObjects = new ConcurrentHashMap();
//从缓存中获取
private static Object getSingleton(String className) {
//先从成品缓存中查找
Object singletonObject = singletonObjects.get(className);
if (singletonObject == null) {
//再从半成品缓存中查找
singletonObject = earlySingletonObjects.get(className);
}
return singletonObject;
}
@SuppressWarnings("unchecked")
private static T getBean(Class clazz) throws IllegalAccessException, InstantiationException {
//先从缓存中获取
String className = clazz.getSimpleName();
Object singleton = getSingleton(className);
if (singleton != null) {
return (T) singleton;
}
synchronized (singletonObjects) {
singleton = singletonObjects.get(className);
//这里需要再进行一次检查
if (singleton != null) {
return (T) singleton;
}
//实例化对象
T instance = clazz.newInstance();
//实例化完成后,就将这个半成品放入到缓存中
earlySingletonObjects.put(className, instance);
//获取当前类中的所有字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//允许访问私有变量
field.setAccessible(true);
//判断字段是否被@Load注解修饰
boolean isUseLoad = field.isAnnotationPresent(Load.class);
if (!isUseLoad) {
continue;
}
//获取需要被注入的字段的class
Class fieldType = field.getType();
//递归获取字段的实例对象
Object fieldBean = getBean(fieldType);
//将实例对象注入到该字段中
field.set(instance, fieldBean);
}
//完成属性注入后,从半成品缓存中移除,加入到成品缓存中
earlySingletonObjects.remove(className);
singletonObjects.put(className, instance);
return instance;
}
}
public static void main(String[] args) {
new Thread(() -> {
try {
A a1 = getBean(A.class);
System.out.println("t1.a:" + a1.hashCode());
System.out.println("t1.b:" + a1.getB().hashCode());
} catch (IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
A a1 = getBean(A.class);
System.out.println("t2.a:" + a1.hashCode());
System.out.println("t2.b:" + a1.getB().hashCode());
} catch (IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
B b = getBean(B.class);
System.out.println("t3.b:" + b.hashCode());
System.out.println("t3.a:" + b.getA().hashCode());
} catch (IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}).start();
输出结果:
多次实验,从输出结果说明:在多线程的场景下,使用两级缓存能够有效避免出现空指针的问题,在一定程度上也能比整个方法加锁的效率更高。
五、什么样的循环依赖都能解决吗?
从第二节可以看出,多例模式下就不可以解决循环依赖。
我们在以上小节中写的代码,是默认全部使用反射set注入的。
而对于单例模式java反射动态注入方法-手把手教你解决循环依赖,一步一步地来窥探出三级缓存的奥秘,在经过对依赖项自然排序后,构造器注入是不可以优先于任何一个set注入的。
第一种场景:b依赖a,需要使用构造器注入a;a依赖b,需要使用set注入b
经过对Bean的自然排序后,会先去创建a,再去创建b。
这种场景,是可以解决循环依赖的。在实例化B时,已经存在半成品a。
结论:最后再使用构造器注入时,可以解决循环依赖。
第二种场景:a依赖b,需要使用构造器注入b;b依赖a,需要使用set注入a
这种场景,在实例化a时,就需要调用构造方法,因此去实例b,而b在缓存中找不到a,造成注入失败。
结论:一开始就使用构造器注入,则不能解决循环依赖。
那么都使用构造器注入时,那肯定也不能解决循环依赖的。
因此,解决循环依赖有两个小前提:
六、中解决循环依赖的原理
在中,我们使用方法从容器获取一个Bean,那么就从方法入手
类中的
public Object getBean(String name) throws BeansException {
assertBeanFactoryActive();
return getBeanFactory().getBean(name);
接着进入类中的
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
再进入到方法中,该方法比较长,截取其中比较核心的点来说
先说方法
//从缓存中获取指定的bean
进入到的方法中
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//先从一级缓存中查找
Object singletonObject = this.singletonObjects.get(beanName);
//如果一级缓存中没有,且当前bean正处于创建的过程中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
//从二级缓存中查找
singletonObject = this.earlySingletonObjects.get(beanName);
//如果二级缓存中也没有,且允许暴露早期引用时
if (singletonObject == null && allowEarlyReference) {
//从三级缓存中查找到bean的工厂
ObjectFactory singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
//调用getObject方法生成bean
singletonObject = singletonFactory.getObject();
//放入到二级缓存中
this.earlySingletonObjects.put(beanName, singletonObject);
//从三级缓存中移除
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
看到这里,似乎和之前我们写的代码很像啊。
在解决循环依赖时,其实也用到了缓存,缓存声明及定义如下:
<p><pre> private final Map singletonObjects = new ConcurrentHashMap(256);
private final Map earlySingletonObjects = new HashMap(16);
private final Map