Spring 特性个人理解
前言
作为一个 Java 程序员,平时打交道最多的自然是 Spring,正逢最近有空,打算借助源码和官方文档仔细温习一下 Spring 相关的特性,或许会持续更新。
本文中贴出的 Spring 源码均是基于 Spring 6.2.12
IoC - 控制反转
依赖注入
这个环节 Spring 官方主要介绍了两种 DI 的方式,构造器注入和 Setter 注入,Spring 官方也对如如何使用两种注入方式做了指引:Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies.
不过根据我目前为止接触到的项目来看,大多数项目其实没有进行这一层面的区分,对于依赖都是采用 @Autowired 注解,这个注解的依赖注入方式其实跟 Setter 注入差距不大,只不过 @Autowired 是通过反射强制在属性上直接注入,而 Setter 注入是通过反射通过 Setter 方法注入,这些依赖绝大多数都是不可或缺的,按照官方指引实际上应该采用构造器注入。
官方实际上也推荐主要使用构造器注入依赖:The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null.
循环依赖
循环依赖算是八股文常问的了,Spring 官方虽然单独提了这点,但是却没有做详细的技术解释,只留下了一句简单的:If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario. 这句话其实挺暧昧的,只说了“有可能产生一个无法解决的循环依赖”,这就延申出了两个问题:
- Spring 是如何解决循环依赖的?
- 哪些循环依赖是可以解决的?哪些循环依赖是不可以解决的?
不过其实只要弄懂第一点,第二点自然就能想通了。
Spring 如何解决循环依赖的?
以下代码均来自 org.springframework.beans.factory.support.AbstractBeanFactory
1 | protected <T> T doGetBean(String name, Class<T> requiredType, |
getSingleton 这个方法就没什么好解释的了,三个 Map 一级一级往下找,这也就是八股文中广为人知的 三级缓存解决循环依赖。
1 | protected Object getSingleton(String beanName, boolean allowEarlyReference) { |
从源码里面可以注意到关键词 singleton ,稍微对 Bean 的配置有点了解就知道这应该代指的是作用域,初步的结论就是只有作用域是 singleton 的 Bean 之间产生循环依赖才能被解决。
从 org.springframework.beans.factory.config.ConfigurableBeanFactory 可以看出 Spring 对 Bean 的作用域提供了两种配置
1 | public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry { |
这里涉及到一点设计模式的概念:单例模式和原型模式,为了接下来的内容不产生歧义,请先弄明白这两个设计模式的概念。
解决循环依赖的核心手法:暴露实例的早期引用,基于这个前提就能想明白为什么只有作用域是 singleton 的 Bean 之间产生循环依赖才能被解决。因为 singleton 每次返回的是同一个 Bean,只要初始化成功了,它的引用就不会发生变化,所以造点暴露出去被引用也无伤大雅。
能够解决哪些循环依赖?
两个发生循环引用的 Bean 之间就可以分为三种情况:singleton 和 singleton 、prototype 和 singleton 、prototype 和 prototype
Singleton 和 Singleton
这种情况不用过多分析,通过源码可以得知就是支持 singleton 的,所以这种情况下的循环依赖是可以被解决的。
Prototype 和 Singleton
这种情况就比较特殊了,需要分情况讨论:
prototypeBean 先实例化:singletonBean 实例化时会想依赖注入prototypeBean,而prototype作用域是不支持暴露早期引用的,自然会依赖注入失败。singletonBean 先实例化:prototypeBean 实例化时会想依赖注入singletonBean,而singleton作用域是支持暴露早期引用的,自然会依赖注入成功。
值得一提的是,从 org.springframework.beans.factory.support.DefaultListableBeanFactory 中可以看出, Spring 启动的时候只会初始化作用域是 singleton 的 Bean。
1 | public void preInstantiateSingletons() throws BeansException { |
基于这些 Bean 再来填充他们的属性进行依赖注入,所以保证了 singleton Bean 先实例化。
Prototype 和 Prototype
这种情况稍微有点奇葩,虽然本质上是不允许的,但可能会启动成功,也可能会启动失败。
- 启动成功:从上一个情况中已经清楚 Spring 在启动阶段只会实例化
singletonBean,如何两个prototypeBean 产生了循环依赖,但没有被任何singletonBean 要求依赖注入,那么他们会逃过一劫从而启动成功。 - 启动失败:自然就是上面的反例了,被
singletonBean 要求依赖注入,Spring 尝试实例化 Bean 时就会发现这个问题。
这种情况下启动成功还是失败不能作为依据,即使启动成功了,也有可能用着用着发现用不了,不过这种情况还是挺罕见的,毕竟绝大多数 Bean 是 singleton,而 prototype 的 Bean 基本上不可避免被 singleton Bean 要求依赖注入。
对循环依赖的一些思考
虽然 Spring 官方一定程度上解决了循环依赖的问题,但实际上 Spring 官方并不认同循环依赖的存在,为什么这么说呢?我觉得主要是以下两点:
- 官方推荐使用构造器注入:假设是一个全参构造函数,那么即使是单例 Bean 也无法解决循环依赖,因为全参缺一不可,本身都实例化不出来就更别提解决循环依赖了。
- 官方默认是不允许循环依赖的:
spring.main.allow-circular-references这个配置项默认是false
所以一旦出现了循环依赖,更应该做的是审视自己的模块设计是否合理,而不是依赖于 Spring 解决。
异步实例化
相信在 org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons() 中注意到了 CompletableFuture 。是的, Spring 在 6.2.0 版本之后推出了异步实例化功能。
1 | private CompletableFuture<?> preInstantiateSingleton(String beanName, RootBeanDefinition mbd) { |
从源码可以看出,Spring 会优先初始化同步的 Bean,之后再去初始化异步的 Bean,不过根据 Bean 的实例化步骤来看,异步的 Bean 要是被同步的 Bean 要求依赖注入了,实际上“异步”就会失效了。如果不考虑同步和异步的先后顺序,那么整个 Bean 的实例化会变得十分复杂且不可控,要是考虑了先后顺序吧,能不能提速也是个问题。这估计也是为什么 Spring 官方一直到 6.2.0 才稍微妥协了一下的原因。
这里推荐一下 why 哥的文章,来龙去脉写地挺详细:13年过去了,Spring官方竟然真的支持Bean的异步初始化了!


