Spring 实例化顺序&循环依赖问题

今天讨论两个问题、一个是 Spring 实例化所有 bean 的顺序。第二个是循环依赖问题

实例化顺序

今天发现一篇文章这么描述

微信公众号:CoderLi

我当时第一反应就肯定不太对、因为关于 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 的创建、这个不是讨论的主要目的)

微信公众号:CoderLi

以上为测试的项目、各个 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 中、让项目引入他们、又会是什么样的顺序

微信公众号:CoderLi

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 运行没有任何问题、一上到服务器就直接炸了

大致代码如下

微信公众号:CoderLi

@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;
}

微信公众号:CoderLi

那如果换过来呢?

情况二
@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 已经禁止循环依赖了、启动的时候直接报错

所以为啥是三级缓存、而不是两级缓存

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值