java面试题

面试题

  • 1.Spring、SpringMVC、SpringBoot
    • [1.1 Spring如何解决循环依赖, 二级缓存能不能解决循环依赖, 为什么要用三级缓存](#1.1 Spring如何解决循环依赖, 二级缓存能不能解决循环依赖, 为什么要用三级缓存)
      • [1.1.1 什么是循环依赖](#1.1.1 什么是循环依赖)
      • [1.1.2 Spring解决循环依赖的思想](#1.1.2 Spring解决循环依赖的思想)
      • [1.1.3 三级缓存](#1.1.3 三级缓存)
      • [1.1.4 通过代码了解bean的创建过程](#1.1.4 通过代码了解bean的创建过程)
      • [1.1.5 Spring初始化bean的过程如下](#1.1.5 Spring初始化bean的过程如下)
      • [1.1.6 循环依赖的N种场景](#1.1.6 循环依赖的N种场景)
      • [1.1.7 总结](#1.1.7 总结)

1.Spring、SpringMVC、SpringBoot

1.1 Spring如何解决循环依赖, 二级缓存能不能解决循环依赖, 为什么要用三级缓存

1.1.1 什么是循环依赖

循环依赖其实就是循环引用, 简单来说, 就是两个或两个以上的bean互相持有对象, 形成闭环. 例如A依赖于B, B依赖于C, 而C又依赖于A

1.1.2 Spring解决循环依赖的思想

Spring单例对象的初始化主要分为三个步骤:

  1. 实例化: 其实就是调用对象的构造方法实例化对象
  2. 属性注入: 对bean的依赖属性进行填充
  3. 初始化: 属性注入成功后, 执行自定义初始化

循环依赖主要发生在第一、第二步骤, 也就是构造器循环依赖和属性循环依赖

Spring解决循环依赖也是从bean的初始化过程着手的, 对于单例来说, 在Spring容器整个生命周期内, 有且只有一个对象, 所以很容易想到这个对象应该存在Cache中, Spring为了解决单例的循环依赖, 使用了三级缓存

1.1.3 三级缓存

所谓三级缓存, 其实就是存放不同状态下的bean

  • singletonObjects 一级缓存
    用于保存实例化、注入、初始化完成的bean实例
  • earlySingletonObjects 二级缓存
    用于保存实例化完成的bean实例(这个bean中的属性还没被赋值)
  • singletonFactories 三级缓存
    用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

下面的代码就是我们获取bean的逻辑

java 复制代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
	Object singletonObject = this.singletonObjects.get(beanName);
	if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
		synchronized (this.singletonObjects) {
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
				ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
				if (singletonFactory != null) {
					singletonObject = singletonFactory.getObject();
					this.earlySingletonObjects.put(beanName, singletonObject);
					this.singletonFactories.remove(beanName);
				}
			}
		}
	}
	return singletonObject;
}

上面的代码需要解释两个参数:

  • allowEarlyReference: 是否允许从singletonFactories中通过getObject拿到对象
  • isSingletonCurrentlyInCreation:
    判断当前单例bean是否正在创建中,也就是没有初始化完成(比如A的构造器依赖了B对象所以得先去创建B对象,
    或则在A的populateBean过程中依赖了B对象,得先去创建B对象,这时的A就是处于创建中的状态。)

我们梳理下整个过程:

  1. Spring先从一级缓存singletonObjects中获取
  2. 如果获取不到, 并且对象正在创建中(这个判断很关键, 说明了这个对象是被管理, 但是还未创建完成), 接着从二级缓存earlySingletonObjects中获
  3. 如果还是获取不到, 并且允许singletonFactories通过getObject获取,就从三级缓存singletonFactory.getObject获取
  4. 如果获取到了, 则从singletonFactories中移除, 将对象存入earlySingletonObjects, 其实也就是从三级缓存移动到了二级缓存

Spring解决循环依赖的诀窍是依赖于singletonFactories这个三级缓存

三级缓存的类型是ObjectFactory, 它是一个泛型接口, 只有一个方法

1.1.4 通过代码了解bean的创建过程

AbstractAutowireCapableBeanFactory#doCreateBean 我们一起来看下源码

如果bean是单例, 同时允许从singletonFactories获取bean,并且当前bean正在创建中, ,那么就把beanName放入三级缓存(singletonFactories)中:

这段代码发生在createBeanInstance之后,也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用。

1.1.5 Spring初始化bean的过程如下

  1. 首先尝试从一级缓存中获取serviceA实例,发现不存在并且serviceA不在创建过程中;
  2. serviceA完成了初始化的第一步(实例化:调用createBeanInstance方法,即调用默认构造方法实例化);
  3. 将自己(serviceA)提前曝光到singletonFactories中;
  4. 此时进行初始化的第二步(注入属性serviceB),发现自己依赖对象serviceB,此时就尝试去get(B),发现B还没有被实create,所以走create流程;
  5. serviceB完成了初始化的第一步(实例化:调用createBeanInstance方法,即调用默认构造方法实例化);
  6. 将自己(serviceB)提前曝光到singletonFactories中;
  7. 此时进行初始化的第二步(注入属性serviceA)
  8. 于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀);
  9. B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中;
  10. 此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中;

知道了这个原理时候,肯定就知道为啥Spring不能解决"A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象"这类问题了!因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。

1.1.6 循环依赖的N种场景

1.1.7 总结

Spring通过三级缓存来解决循环依赖, 其中一级缓存为单例池(singletonObjects), 二级缓存为早期曝光对象(earlySingletonObjects), 三级缓存为早期曝光工厂(singletonFactories)

当A、B两个类发生循环依赖时, 在A完成实例化之后, 就使用实例化后的对象去创建一个对象工厂, 并添加到三级缓存中, 如果A被AOP代理, 那么通过这个工厂获取到的就是A代理后的对象, 反之, 则为A实例化对象

当A进行属性注入时, 就会创建B, 此时B又依赖于A, 所以创建B的同时又会调用getBean(A)来获取A的依赖, 会依次从一级、二级、三级缓存中去寻找A, 最终在三级缓存中找到了A的工厂, 并生成对应的对象, 将其注入到B中, 紧接着B会走完它的生命流程, 包括初始化、后置处理等等, 当B创建完成后, B的对象就会存入一级缓存, 此时就会回到一开始的地方, 将B再注入到A中, 此后A再走完它的生命周期

为什么不能只使用二级缓存?

如果只使用二级缓存, 那么二级缓存是在A实例化后就缓存好了, 这个时候A在设置属性的过程中去获取B(这个时候A还没有被aop的后置处理器增强),创建B的过程中,B依赖A,b去缓存中拿A拿到的是没有经过AOP代理的A。就有问题。

简单来说: 如果只使用二级缓存无法解决AOP代理问题, 因为二级缓存的执行时机是实例化之后, 属性注入之前, 而AOP是在后置处理器阶段执行(在初始化前后), 此时二级缓存无法确定需要缓存的是实例对象还是代理对象

二级缓存的作用是什么?

如果出现循环依赖+aop时,多个地方注入这个动态代理对象需要保证都是同一个对象,而三级缓存中的取出来的动态代理对象每次都是新对象,地址值不一样。

参考内容:
spring如何解决循环依赖
spring为什么要使用三级缓存解决循环依赖
spring三级缓存之为大多数合理而设计

相关推荐
Code成立2 分钟前
《Java核心技术 卷I》用户图形界面鼠标事件
java·开发语言·计算机外设
Xiao Fei Xiangζั͡ޓއއ22 分钟前
一觉睡醒,全世界计算机水平下降100倍,而我却精通C语言——scanf函数
c语言·开发语言·笔记·程序人生·面试·蓝桥杯·学习方法
记录无知岁月24 分钟前
【MATLAB】目标检测初探
开发语言·yolo·目标检测·matlab·yolov3·yolov2
鸽鸽程序猿27 分钟前
【算法】【优选算法】二分查找算法(下)
java·算法·二分查找算法
远望清一色38 分钟前
基于MATLAB身份证号码识别
开发语言·图像处理·算法·matlab
遇见你真好。42 分钟前
自定义注解进行数据脱敏
java·springboot
NMBG221 小时前
[JAVAEE] 面试题(四) - 多线程下使用ArrayList涉及到的线程安全问题及解决
java·开发语言·面试·java-ee·intellij-idea
Py小趴1 小时前
Python自学之Colormaps指南
开发语言·python·数据可视化
晒足以百八十1 小时前
基于Python 和 pyecharts 制作招聘数据可视化分析大屏
开发语言·python·信息可视化
像污秽一样1 小时前
Spring MVC初探
java·spring·mvc