0
点赞
收藏
分享

微信扫一扫

《Spring》第十七篇 循环依赖 (二)

IT影子 2022-03-11 阅读 69

目录

1. 原型Bean情况下的循环依赖解析

首先Spring在扫描阶段是不会扫描原型bean的, 这些原型bean只有在调用的时候才会去创建,针对原型Bean列出几种场景来简单描述下:
(1) 假设有两个对象,A类与B类, 并且都被定义为原型

@Component
@Scope("prototype")
public class A {

    @Autowired
    private B b;
}

@Component
@Scope("prototype")
public class B {

    @Autowired
    private A a;
}

当使用getBean从容器中获取A类Bean时, 程序会直接报错。
这时因为容器中不会存在A类对应得Bean, 然后就需要去创建A类, 并且当实例化得到一个A原始对象时, 由于是原型的, 所以不会向三级缓存singletonFactories中去暴露一个lambda表达式的。然后继续对b属性进行属性注入, 而B类在容器中也不会存在,所以也是去创建, 并且实例化生成的B原始对象也不会放入三级缓存中, 这样不管是A类还是B类在属性注入的时候,都无法在三级缓存中拿到所需的对象来完成属性注入,所以势必连Bean的生命周期都走不完, 陷入死循环了
这种情况Spring是无法解决的
在这里插入图片描述
虽然Spring无法自动去解决这个循环依赖的问题,但是开发者可以手动去解决,只需要添加一个注解@Lazy就可以了

@Component
@Scope("prototype")
public class A {

    @Autowired
    @Lazy
    private B b;
}

@Component
@Scope("prototype")
public class B {

    @Autowired
    private A a;
}

@Lazy注解不管是加载A类中b属性上,还是B类中的a属性上,亦或者二者都加,都能解决两者都是原型的循环问题, 因为在A类实例化生成A原始对象后, 在给b属性进行属性注入时,解析到b属性被@Lazy注解定义了, 会生成一个代理B类的临时代理对象来完成对b属性的注入,然后A类可以继续去完成Bean的整个生命周期。同样创建B类时, 给a属性赋值时,重新进入A类的生命周期,又会新建一个A类对象完成a属性的赋值,这样B类也能完成整个Bean生命周期了。
@Lazy的使用,可以打破A类对B类的依赖
在这里插入图片描述
(2) 假设有两个对象,A类与B类, 其中有一个类是单例的,另一个是原型的

@Component
public class A {

    @Autowired
    private B b;
}

@Component
@Scope("prototype")
public class B {

    @Autowired
    private A a;
}

A类和B类只要有一个是单例的,不管另一个是单例还是原型,都会正常创建Bean
如上例子: 如果现在A类是单例的,B类是原型的, 那么A类在实例化生成A原始对象后,会放入到三级缓存singletonFactories中,并暴露一个lambda表达式在三级缓存中, 然后在给b属性赋值时, 由于B类是原型的,只能去新建, 当B类实例化生成B原始对象后,不会放入三级缓存中,在给a属性赋值时,能从三级缓存中拿到A类暴露的lambda表达式去生成一个对象(可能时原始对象,也有可能是一个代理对象), 那么B类就能完成属性注入,继而完成整个Bean生命周期并创建一个B类对应的Bean对象,这样A类就能拿到B类对应的Bean, 完成属性注入,继而完成整个Bean生命周期并创建一个A类对应的Bean对象,并放入缓存中
在这里插入图片描述

2. 构造方法导致的循环依赖

构造方法是用在Bean生命周期实例化阶段,会经历推断构造方法的环节来确定使用哪个构造函数来实例化一个原始对象。也会出现循环依赖, 接下来用例子来简单描述下:

@Component
public class A {

    public A(B b) {
        System.out.println(b);
    }
}

@Component
public class B {

    @Autowired
    private A a;
}

A类和B类都是单例的, 并且A类中的构造函数,需要B类的对象做为参数才能完成构建, 当A类在实例化阶段时, 经过推断构造方法之后,使用例子中的构造函数去实例化对象时, 先拿B类对象的类型去BeanFactory中寻找, 这里假设就是找不到的(这个找的逻辑有点复杂不在这里叙述了,可以去看另一篇文章《Spring》第十二篇 推断构造方法机制), 然后就去创建B类, B类在实例化之后,去给a属性赋值, 但是此时从一级缓存,二级缓存,三级缓存都找不到A类的对象,无法完成属性注入,也就无法完成Bean的生命周期了。这是因为A类还处在实例化阶段, 还未来得及向三级缓存中暴露lambda表达式, 所以A类的构造参数也获取不到B类对应的Bean,陷入循环依赖中了
在这里插入图片描述
同理这种循环依赖, Spring是无法自动解决的。那就需要靠开发者手动的在B类中的a属性上添加一个@Lazy注解,就解决了。因为添加@Lazy注解后,可以生成一个临时代理对象帮助B类完成对a属性的赋值,继而完成B类对应的Bean对象的创建。B类对应的Bean创建成功,那么A类的构造函数就能得到B类型的参数,完成实例化, 继而完成A类对应Bean对象的创建。

3. @Async情况下的循环依赖解析

在介绍@Async用法之前, 先介绍下源码AbstractAutowireCapableBeanFactory.doCreateBean() 中★标记的位置,
为什么会有一个if (exposedObject == bean) 的校验?

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
		.........
		
		// 1.实例化bean
		// 2.实例化后
		Object exposedObject = bean;
		try {
			//3.属性注入
			populateBean(beanName, mbd, instanceWrapper);
			//4.初始化
			exposedObject = initializeBean(beanName, exposedObject, mbd);
		} catch (Throwable ex) {
			if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
				throw (BeanCreationException) ex;
			} else {
				throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
			}
		}

		// 5.循环依赖的兜底判断逻辑
		// 如果是循环依赖, 那么earlySingletonExposure就是true
		if (earlySingletonExposure) {
			// 标记不允许循环依赖, 避免又去三级缓存中找
			Object earlySingletonReference = getSingleton(beanName, false);
			if (earlySingletonReference != null) {
			     // 一般情况下,这个if判断是为true// ★ 为啥需要这么一层的校验? 
				if (exposedObject == bean) {
					exposedObject = earlySingletonReference;
				} else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
				    // 如果是异步的情况可能会走到此else逻辑中
					String[] dependentBeans = getDependentBeans(beanName);
					Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
					for (String dependentBean : dependentBeans) {
						if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
							actualDependentBeans.add(dependentBean);
						}
					}
					if (!actualDependentBeans.isEmpty()) {
						throw new BeanCurrentlyInCreationException(beanName,
								"Bean with name '" + beanName + "' has been injected into other beans [" +
										StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
										"] 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.");
					}
				}
			}
		}

		// 6.Bean销毁的逻辑(判断是否有特殊的销毁逻辑,并放入缓存中)
		.........
		
		return exposedObject;
	}

通过使用@Async注解验证上述源码中的抛出的异常信息,并解释为什么需要这个if (exposedObject == bean)判断

@Component
public class A {

    @Autowired
    private B b;

    @Async
    public void a() {
        System.out.println(b);
    }
}

@Component
public class B {

    @Autowired
    private A a;

    public void b() {
        System.out.println(a);
    }
}

@Configuration
@ComponentScan("com.dependence")
@EnableAsync  // 开启异步调用
public class SpringDependenceTest {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringDependenceTest.class);
        A a = context.getBean("a", A.class);
        System.out.println(a);

        B b = context.getBean("b", B.class);
        System.out.println(b);
    }
}

在A类中的方法上添加@Async,运行时,直接就报上面源码中抛出的异常信息。
注意一定要配合@EnableAsync注解一起使用,这个注解是开启异步, 否则单独使用@Async注解会不生效的。
那么为什么会出现这种异常?
因为使用@Async注解,异步的线程会使用AsyncAnnotationBeanPostProcessor去产生一个A类的代理对象, 那么和原线程产生的A类对象就可能不是同一个对象了,可能就会产生同一个类有两种的形态了,如果此时有另一个类也依赖了该类,那么注入的时候就不知道选择哪种形态的对象来赋值了。
在这里插入图片描述
这种情况可以解决吗? 可以的, 下面列出了两种解决办法

## 1.A类中的b属性上加上@Lazy注解
@Component
public class A {

    @Autowired
    @Lazy
    private B b;

    @Async
    public void a() {
        System.out.println(b);
    }
}

因为A类在实例化后, 在对b属性进行赋值时,由于b属性被@Lazy注解定义,那么就直接生成一个临时的代理对象完成b属性的赋值,那么A类在整个生命周期的过程中就断开与B类的依赖,所以此时一级缓存二级缓存都不存在A类的对象,所以在源码中的getSingleton(beanName, false)方法执行的结果是就是空, 所以不会走到下面的比对判断逻辑中,即使@Async注解会生成一个代理对象, 也不影响的。
所以总结一句话: b属性上加了@Lazy注解,会跳过比对判断逻辑

## 2. 将@Async注解移到B类中的方法上
@Component
public class B {

    @Autowired
    private A a;

    @Async
    public void b() {
        System.out.println(a);
    }
}

这就是一个顺序的问题了, 当A类实例化后,对b属性进行依赖注入, 并率先完成A类对应Bean的创建,并且此时二级缓存中只有一个A类的对象,不存在B类的对象,B类的对象此时只存在于三级缓存中, 所以此时即使B类中的方法上定义了@Async注解,会产生一个代理对象,但是由于此时一级缓存,二级缓存中都不存在B类的对象, 所以在源码中的getSingleton(beanName, false)方法执行的结果是就是空, 所以跳过了下面的比对判断逻辑中

举报

相关推荐

0 条评论