今天讨论两个问题、一个是 Spring 实例化所有 bean 的顺序。第二个是循环依赖问题
实例化顺序
今天发现一篇文章这么描述
我当时第一反应就肯定不太对、因为关于 Spring 实例化顺序这个、吃过大亏
AbstractApplicationContext#finishBeanFactoryInitialization
中
// Instantiate all remaining (non-lazy-init) singletons.
beanFactory.preInstantiateSingletons();
这个方法就是去实例化已经注册的非延迟实例化 bean
/** List of bean definition names, in registration order. */
private volatile List<String> beanDefinitionNames = new ArrayList<>(256);
public void preInstantiateSingletons() throws BeansException {
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
........
getBean(beanName);
}
}
}
我们看到 beanDefinitionNames 保存的就是 beanName、它的顺序就是我们注册 BeanDefinition 的顺序、换言之、这个就是我们实例化 bean 的顺序(当然这个过程可能穿插依赖 bean 的创建、这个不是讨论的主要目的)
以上为测试的项目、各个 bean 之间不存在依赖关系、只是单纯在构造函数中打印日志、
那这个实例化顺序真的是按 beanName 的字母顺序吗
日志打印创建的顺序如下
new aop
new RootA
new service
new C
new D
new B
new e
new A,beanName is z
new f
在 project 模块中、同一层级是按类的名称进行排序注册的、跟 beanName 没啥关系。这个注册的顺序主要跟扫描 bean 有关系、默认都是层级扫描的
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
for (File content : listDirectory(dir)) {
String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
if (!content.canRead()) {
}
else {
doRetrieveMatchingFiles(fullPattern, content, result);
}
}
if (getPathMatcher().match(fullPattern, currPath)) {
result.add(content);
}
}
}
protected File[] listDirectory(File dir) {
File[] files = dir.listFiles();
if (files == null) {
if (logger.isInfoEnabled()) {
logger.info("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
}
return new File[0];
}
Arrays.sort(files, Comparator.comparing(File::getName));
return files;
}
同一层级、不论文件夹还是文件、按升序获取、如果是文件则递归。因为 Java 的规范类名都是大写、包名都是小写、所以肯定是先是外层然后再到内层的类的
那么如果我们将上面的 ABCDEF 打包在一个 jar 中、让项目引入他们、又会是什么样的顺序
new aop
new RootA
new service
new B
new e
new D
new C
new f
new A
是不是毫无头绪、这个就是压缩包读取出来的顺序。相关代码在 PathMatchingResourcePatternResolver#doFindPathMatchingJarResources
public ZipEntry next() {
synchronized (ZipFile.this) {
ensureOpen();
if (i >= total) {
throw new NoSuchElementException();
}
long jzentry = getNextEntry(jzfile, i++);
if (jzentry == 0) {
String message;
if (closeRequested) {
message = "ZipFile concurrently closed";
} else {
message = getZipMessage(ZipFile.this.jzfile);
}
throw new ZipError("jzentry == 0" +
",\n jzfile = " + ZipFile.this.jzfile +
",\n total = " + ZipFile.this.total +
",\n name = " + ZipFile.this.name +
",\n i = " + i +
",\n message = " + message
);
}
ZipEntry ze = getZipEntry(null, jzentry);
freeEntry(jzfile, jzentry);
return ze;
}
}
这个是 main 方法自己读区的顺序、
public static void main(String[] args) throws IOException {
JarFile jarFile = new JarFile(new File("/Users/xxx/IdeaProjects/spring-order/target/spring-order-1.0-SNAPSHOT.jar"));
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
System.out.println(jarEntry.getName());
}
}
再说说实际工作遇到的关于顺序的这个问题、这个问题一个是上面说的注册的顺序、一个是 ConditionalOnBean
注解判断的时机
项目是多模块、在本地 idea 运行没有任何问题、一上到服务器就直接炸了
大致代码如下
@Component
@ConditionalOnBean(CacheSwitch.class)
public class CaffeineCacheManager {
public CaffeineCacheManager() {
System.out.println("new caffeine");
}
}
@Configuration
public class CacheConfig {
@Bean
public CacheSwitch cacheSwitch(){
return new CacheSwitch();
}
}
关键点在于 ConditionalOnBean
依赖的 Bean 是通过 @Bean 注解给到 Spring 的、当时 Redis 还没接入、默认 getCache 返回 null、而本地缓存 Caffeine 是接入了的。
本地运行的时候、缓存一致时使用 Caffeine 的、一上到服务器、使用的是 Redis、直接一堆空指针异常、而 Caffine 的 Manager 根本没有实例化。
因为在本地运行的时候解释 CacheConfig 这个配置类先于 CaffineCacheManager、而在服务器则在其后面、导致 Conditional 条件不符合、直接从 BeanDefinitionRegistry 中移除掉。
循环依赖问题
- 构造器循环依赖
- setter 循环依赖
我们知道对于构造器循环依赖、Spring 是没有办法解决的、但是对于 setter 循环依赖是可以通过三级缓存解决的。
那么如果有一个循环依赖、A 在构造器依赖 B、而 B 在通过setter 的方法注入A、那么这个 Spring 可以解决吗
情况一
@Component
public class A {
public A(B b) {
}
}
@Component
public class B {
@Autowired
private A a;
}
那如果换过来呢?
情况二
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
public B(A a) {
}
}
这样子就可以了、其实结合上面实例化顺序、就不太难理解、为啥可以
情况一
- 欲创建实例化 A、发现需要实例B
- 创建实例B、填充属性的时候发现需要实例A、
- 欲创建实例A、但是此时A已经在 Spring 的正在创建集合中、抛出异常
情况二
- 创建实例A、将A提前暴露出去、放到三级缓存中、填充属性B
- 欲创建B、发现需要实例A、从三级缓存中获取、创建成功、返回B实例给A填充
在讨论一个问题就是为啥需要三级缓存?两级行不行?这个在以前的文章讨论过
我们首先来看看第三级缓存做了什么
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
先思考一个问题?为啥要包装一层 ObjectFactory ?
我们都知道 Spring 创建动态代理是在 Bean 初始化完成、在 BeanPostProcessor的 after 中创建的
AbstractAutoProxyCreator
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
如果像上面的情况二、B 依赖A、A 假如是一个代理对象(AOP拦截A的方法也好、事务或者缓存也好)、那么 B 在获取 A 的时候、就应该是拿到 A 的代理对象、而不是 A 真实对象。但是动态代理却是在 Bean 初始化完成之后创建的、这可怎么办
那就提供一个接口呗、当存在这种循环依赖的时候、我就创建动态代理、后续在 BeanPostProcessor 的时候我不再创建不就得了呗
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
看看 AbstractAutoProxyCreator 的逻辑
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}
正如我们所说的那样子
再回到我们的问题、没有第三级缓存可以吗?
首先二级缓存就已经可以解决 setter 循环依赖的问题了的、但是对于这种动态代理的依赖怎么办?
比如说我们在 bean 实例化之后马上创建动态代理、不管它有没有循环依赖、我们都提前创建好它、并且将它放到二级缓存中。
这种做法可以是可以、并且也不会有什么太大的问题、但是这不符合 Spring Bean 的生命周期、Spring 的动态代理按照其生命周期来说、是在 Bean 完全初始化、一切都准备妥当之后再创建的。
所以循环依赖其实是破坏了 Spring Bean 的生命周期的、在没有完成初始化的时候便创建了动态代理暴露出去了。
Spring Boot 2.6.0 已经禁止循环依赖了、启动的时候直接报错
所以为啥是三级缓存、而不是两级缓存