本文已收录在公众号:https://mp.weixin.qq.com/s/FIm84EGVV21phajCaLjgaA算法
你可能会有以下问题:spring
一、想看Spring源码,可是不知道应当如何入手去看,对整个Bean的流程没有概念,碰到相关问题也没有头绪如何下手数组
二、看过几遍源码,没办法完全理解,没什么感受,没过一阵子又忘了缓存
本文将结合实际问题,由问题引出源码,并在解释时会尽可能以图表的形式让你一步一步完全理解Spring Bean的IOC、DI、生命周期、做用域等。bash
循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终造成闭环。好比A依赖于B,B依赖于C,C又依赖于A。以下图:数据结构
如何理解“依赖”呢,在Spring中有:多线程
直接上代码:并发
@Service
public class A {
public A(B b) { }
}
复制代码
@Service
public class B {
public B(C c) {
}
}
复制代码
@Service
public class C {
public C(A a) { }
}复制代码
结果:项目启动失败,发现了一个cycle。app
@Service
public class A1 {
@Autowired
private B1 b1;
}复制代码
@Service
public class B1 {
@Autowired
public C1 c1;
}复制代码
@Service
public class C1 {
@Autowired public A1 a1;
}复制代码
结果:项目启动成功ide
@Service
@Scope("prototype")
public class A1 {
@Autowired
private B1 b1;
}复制代码
@Service
@Scope("prototype")
public class B1 {
@Autowired
public C1 c1;
}复制代码
@Service
@Scope("prototype")
public class C1 {
@Autowired public A1 a1;
}复制代码
结果:项目启动失败,发现了一个cycle。
现象总结:一样对于循环依赖的场景,构造器注入和prototype类型的属性注入都会初始化Bean失败。由于@Service默认是单例的,因此单例的属性注入是能够成功的。
分析缘由也就是在发现SpringIOC的过程,若是对源码不感兴趣能够关注每段源码分析以后的总结和循环依赖问题的分析便可。
简单一段代码做为入口
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
ac.getBean(XXX.class);复制代码
ClassPathXmlApplicationContext是一个加载XML配置文件的类,与之相对的还有AnnotationConfigWebApplicationContext,这两个类大差不差的,只是ClassPathXmlApplicationContext的Resource是XML文件而AnnotationConfigWebApplicationContext是Scan注解得到的。
看到第二行就已经能够直接获取bean的实例了,因此第一行构造方法时,就已经完成了对全部bean的加载。
ClassPathXmlApplicationContext举例,他里面储存的东西以下:
对象名 | 类 型 | 做 用 | 归属类 |
configResources | Resource[] | 配置文件资源对象数组 | ClassPathXmlApplicationContext |
configLocations | String[] | 配置文件字符串数组,存储配置文件路径 | AbstractRefreshableConfigApplicationContext |
beanFactory | DefaultListableBeanFactory | 上下文使用的Bean工厂 | AbstractRefreshableApplicationContext |
beanFactoryMonitor | Object | Bean工厂使用的同步监视器 | AbstractRefreshableApplicationContext |
id | String | 上下文使用的惟一Id,标识此ApplicationContext | AbstractApplicationContext |
parent | ApplicationContext | 父级ApplicationContext | AbstractApplicationContext |
beanFactoryPostProcessors | List<BeanFactoryPostProcessor> | 存储BeanFactoryPostProcessor接口,Spring提供的一个扩展点 | AbstractApplicationContext |
startupShutdownMonitor | Object | refresh方法和destory方法公用的一个监视器,避免两个方法同时执行 | AbstractApplicationContext |
shutdownHook | Thread | Spring提供的一个钩子,JVM中止执行时会运行Thread里面的方法 | AbstractApplicationContext |
resourcePatternResolver | ResourcePatternResolver | 上下文使用的资源格式解析器 | AbstractApplicationContext |
lifecycleProcessor | LifecycleProcessor | 用于管理Bean生命周期的生命周期处理器接口 | AbstractApplicationContext |
messageSource | MessageSource | 用于实现国际化的一个接口 | AbstractApplicationContext |
applicationEventMulticaster | ApplicationEventMulticaster | Spring提供的事件管理机制中的事件多播器接口 | AbstractApplicationContext |
applicationListeners | Set<ApplicationListener> | Spring提供的事件管理机制中的应用监听器 | AbstractApplicationContext |
构造方法以下:
接下来大概看看refresh方法:
子方法先不看,先看看refresh方法的结构,其实就有几点值得学习:
一、方法为何加锁? 是为了不多线程的场景下同时刷新Spring上下文
二、虽然整个方法是加锁的,可是却用了Synchronized关键字的对象锁startUpShutdownMonitor,这样作有两个好处:
(1)关闭资源的时候会调用close()方法,close()方法也使用了一样的对象锁,而关闭资源的close和refresh的两个冲突的方法,这样能够避免冲突
(2)此处对象锁相对于整个方法加锁的话,同步的范围更小了,锁的粒度更小,效率更高
三、这个方法refresh定义了整个Spring IOC的流程,每个方法名字都清晰易懂,可维护性、可读性很强
总结:看源码须要找准入口,看的时候多思考,学习Spring的巧妙的设计。ApplicationContext的构造方法中最关键是方法是refresh,其中有一些比价好的设计。
这个方法做用是获取刷新Spring上下文的Bean工厂:
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
this.refreshBeanFactory();
return this.getBeanFactory();
}复制代码
protected final void refreshBeanFactory() throws BeansException {
if (this.hasBeanFactory()) {
this.destroyBeans();
this.closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = this.createBeanFactory();
beanFactory.setSerializationId(this.getId());
this.customizeBeanFactory(beanFactory);
this.loadBeanDefinitions(beanFactory);
synchronized(this.beanFactoryMonitor) {
this.beanFactory = beanFactory; }
} catch (IOException var5) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + this.getDisplayName(), var5);
}
}复制代码
这断代码的核心是DefaultListableBeanFactory,核心类咱们再整理一下,以图表格式:
下面有三个加粗的Map,这些个Map是解决问题的关键。。。咱们以后详细分析
对象名 | 类 型 | 做 用 | 归属类 |
aliasMap | Map<String, String> | 存储Bean名称->Bean别名映射关系 | SimpleAliasRegistry |
singletonObjects | Map<String, Object> | 存储单例Bean名称->单例Bean实现映射关系 | DefaultSingletonBeanRegistry |
singletonFactories | Map<String, ObjectFactory> | 存储Bean名称->ObjectFactory实现映射关系 | DefaultSingletonBeanRegistry |
earlySingletonObjects | Map<String, Object> | 存储Bean名称->预加载Bean实现映射关系 | DefaultSingletonBeanRegistry |
registeredSingletons | Set<String> | 存储注册过的Bean名 | DefaultSingletonBeanRegistry |
singletonsCurrentlyInCreation | Set<String> | 存储当前正在建立的Bean名 | DefaultSingletonBeanRegistry |
disposableBeans | Map<String, Object> | 存储Bean名称->Disposable接口实现Bean实现映射关系 |
DefaultSingletonBeanRegistry |
factoryBeanObjectCache | Map<String, Object> | 存储Bean名称->FactoryBean接口Bean实现映射关系 | FactoryBeanRegistrySupport |
propertyEditorRegistrars | Set<PropertyEditorRegistrar> | 存储PropertyEditorRegistrar接口实现集合 | AbstractBeanFactory |
embeddedValueResolvers | List<StringValueResolver> | 存储StringValueResolver(字符串解析器)接口实现列表 | AbstractBeanFactory |
beanPostProcessors | List<BeanPostProcessor> | 存储 BeanPostProcessor接口实现列表 | AbstractBeanFactory |
mergedBeanDefinitions | Map<String, RootBeanDefinition> | 存储Bean名称->合并过的根Bean定义映射关系 | AbstractBeanFactory |
alreadyCreated | Set<String> | 存储至少被建立过一次的Bean名集合 | AbstractBeanFactory |
ignoredDependencyInterfaces | Set<Class> | 存储不自动装配的接口Class对象集合 | AbstractAutowireCapableBeanFactory |
resolvableDependencies | Map<Class, Object> | 存储修正过的依赖映射关系 | DefaultListableBeanFactory |
beanDefinitionMap | Map<String, BeanDefinition> | 存储Bean名称-->Bean定义映射关系 | DefaultListableBeanFactory |
beanDefinitionNames | List<String> | 存储Bean定义名称列表 | DefaultListableBeanFactory |
接下来简要分析一下loadBeanDefinitions。
对于这个BeanDefinition,我是这么理解的: 它是SpringIOC过程当中间的一个产物,能够当作是对Bean定义的抽象,里面封装的数据都是与Bean定义相关的,封装了一些基本的bean的Property、initi-method、destroy-method等。
这里的主要方法是loadBeanDefinitions,这里不详细展开说,它主要作了几件事:
一、初始化了BeanDefinitionReader
二、经过BeanDefinitionReader获取Resource,也就是xml配置文件的位置,而且把文件转换成一个叫Document的对象
三、接下来须要将Document对象转化成容器内部的数据结构(也就是BeanDefinition),也便是将Bean定义的List、Map、Set等各类元素进行解析,转换成Managed类(Spring对BeanDefinition数据的封装)放在BeanDefinition中;这个方法是RegisterBeanDefinition(),也就是解析的过程。
四、解析完成后,会把解析的结果放到BeanDefinition对象中并设置到一个Map中
以上这个过程就是BeanDefinition在IOC容器中的注册。
再回到Refresh方法,总结每一步以下图:
总结:这一部分步骤主要是Spring如何加载Xml文件或者注解,并把它解析成BeanDefinition。
先回到以前的refresh方法(也就是在构造ApplicationContext时的方法),咱们跳过不重要的部分:
咱们直接看finishBeanFactoryInitialization里面的preInstantiateSingletons方法,顾名思义初始化全部的单例bean,截取部分以下:
如今来看核心的getBean方法,对于全部获取Bean对象是实例,都是用这个getBean方法,这个方法最终调用的是doGetBean方法,这个方法就是所谓的DI(依赖注入)发生的地方。
程序=数据+算法,以前的BeanDefinition就是“数据”,依赖注入也就是在BeanDefinition准备好状况下进行进行的,这个过程不简单,由于Spring提供了不少参数配置,每个参数都表明了IOC容器的特性,这些特性的实现须要在Bean的生命周期中完成。
代码比较多,就不贴了,你们能够自行查看AbstractBeanFactory里面的doGetBean方法,这里直接上图,这个图就是依赖注入的整个过程:
总结:Spring建立好了BeanDefinition以后呢,会开始实例化Bean,而且对Bean的依赖属性进行填充。实例化时底层使用了CGLIB或Java反射技术。上图中instantiateBean核PupulateBean方法很重要!
咱们先总结一下以前的结论:
一、构造器注入和prototype类型的field注入发生循环依赖时都没法初始化
二、field注入单例的bean时,尽管有循环依赖,但bean仍然能够被成功初始化
针对这几个结论,提出问题
以前在DefaultListableBeanFactory类中,列出了一个表格;如今我把关键的精华属性列出来:
一级缓存:
/** 保存全部的singletonBean的实例 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);
二级缓存:
/** 保存全部早期建立的Bean对象,这个Bean尚未完成依赖注入 */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
三级缓存:
/** singletonBean的生产工厂*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
/** 保存全部已经完成初始化的Bean的名字(name) */
private final Set<String> registeredSingletons = new LinkedHashSet<String>(64);
/** 标识指定name的Bean对象是否处于建立状态 这个状态很是重要 */
private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(16));
复制代码
前面三个Map,咱们称为单例初始化的三级缓存,理解这个问题,咱们目前只需关注“三级”,也就是singletonFactories
分析:
对于问题1,单例的设值注入,若是A中注入了B,B应该是A中的一个属性,那么猜测应该是A已经被instantiate(实例化)以后,在populateBean(填充A中的属性)时,对B进行初始化。
对于问题2,instantiate(实例化)其实就是理解成new一个对象的过程,而new的时候确定要执行构造方法,因此猜测对于应该是A在instantiate(实例化)时,进行B的初始化。
有了分析和猜测以后呢,围绕关键的属性,根据从上图的doGetBean方法开始到populateBean全部的代码,我整理了以下图:
上图是整个过程当中关键的代码路径,感兴趣的能够本身debug几次,最关键的解决循环依赖的是如上的两个标红的方法,第一个方法getSingleton会从singletonFactories里面拿Singleton,而addSingletonFactory会把Singleton放入singletonFactories。
对于问题1:单例的设值注入bean是如何解决循环依赖问题呢?若是A中注入了B,那么他们初始化的顺序是什么样子的?
假设循环注入是A-B-A:A依赖B(A中autowire了B),B又依赖A(B中又autowire了A):
本质就是三级缓存发挥做用,解决了循环。
对于当时问题2,instantiate(实例化)其实就是理解成new一个对象的过程,而new的时候确定要执行构造方法,因此猜测对于应该是A在instantiate(实例化)时,进行B的初始化。
答案也很简单,由于A中构造器注入了B,那么A在关键的方法addSingletonFactory()以前就去初始化了B,致使三级缓存中根本没有A,因此会发生死循环,Spring发现以后就抛出异常了。至于Spring是如何发现异常的呢,本质上是根据Bean的状态给Bean进行mark,若是递归调用时发现bean当时正在建立中,那么久抛出循环依赖的异常便可。
那么prototype的Bean是如何初始化的呢?
prototypeBean有一个关键的属性:
/** Names of beans that are currently in creation */
private final ThreadLocal<Object> prototypesCurrentlyInCreation =
new NamedThreadLocal<Object>("Prototype beans currently in creation");
复制代码
保存着正在建立的prototype的beanName,在流程上并无暴露任何factory之类的缓存。而且在beforePrototypeCreation(String beanName)
方法时,把每一个正在建立的prototype的BeanName放入一个set中:
protected void beforePrototypeCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
if (curVal == null) {
this.prototypesCurrentlyInCreation.set(beanName);
}
else if (curVal instanceof String) {
Set<String> beanNameSet = new HashSet<String>(2);
beanNameSet.add((String) curVal);
beanNameSet.add(beanName);
this.prototypesCurrentlyInCreation.set(beanNameSet);
}
else {
Set<String> beanNameSet = (Set<String>) curVal;
beanNameSet.add(beanName);
}
}复制代码
而且会循环依赖时检查beanName是否处于建立状态,若是是就抛出异常:
protected boolean isPrototypeCurrentlyInCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
return (curVal != null &&
(curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
}复制代码
从流程上就能够查看,不管是构造注入仍是设值注入,第二次进入同一个Bean的getBean方法是,必定会在校验部分抛出异常,所以不能完成注入,也就不能实现循环引用。
总结:Spring在InstantiateBean时执行构造器方法,构造出实例,若是是单例的话,会将它放入一个singletonBeanFactory的缓存中,再进行populateBean方法,设置属性。经过一个singletonBeanFactory的缓存解决了循环依赖的问题。
如今你们已经对Spring整个流程有点感受了,咱们再来解决一个简单的常见的问题:
考虑一下以下的singleton代码:
@Service
public class SingletonBean{
@Autowired
private PrototypeBean prototypeBean;
public void doSomething(){
System.out.println(prototypeBean.toString()); }
}复制代码
@Component
@Scope(value="prototype")
public class PrototypeBean{
}复制代码
一个Singleton的Bean中Autowired了一个prototype的Bean,那么问题来了,每次调用SingletonBean.doSomething()时打印的对象是否是同一个呢?
有了以前的知识储备,咱们简单分析一下:由于Singleton是单例的,因此在项目启动时就会初始化,prototypeBean本质上只是它的一个Property,那么ApplicationContex中只存在一个SingletonBean和一个初始化SingletonBean时建立的一个prototype类型的PrototypeBean。
那么每次调用SingletonBean.doSomething()时,Spring会从ApplicationContex中获取SingletonBean,每次获取的SingletonBean是同一个,因此即使PrototypeBean是prototype的,但PrototypeBean仍然是同一个。每次打印出来的内存地址确定是同一个。
解决办法也很简单,这种状况咱们不能经过注入的方式注入一个prototypeBean,只能在程序运行时手动调用getBean("prototypeBean")方法,我写了一个简单的工具类:
@Service
public class SpringBeanUtils implements ApplicationContextAware {
private static ApplicationContext appContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringBeanUtils.appContext=applicationContext;
}
public static ApplicationContext getAppContext() {
return appContext;
}
public static Object getBean(String beanName) {
checkApplicationContext();
return appContext.getBean(beanName);
}
private static void checkApplicationContext() {
if (null == appContext) {
throw new IllegalStateException("applicaitonContext未注入");
}
}
@SuppressWarnings("unchecked")
public static <T> T getBean(Class<T> clazz) {
checkApplicationContext();
Map<?, ?> map = appContext.getBeansOfType(clazz);
return map.isEmpty() ? null : (T) map.values().iterator().next();
}
}复制代码
对于这个ApplicationContextAware接口:
在某些特殊的状况下,Bean须要实现某个功能,但该功能必须借助于Spring容器才能实现,此时就必须让该Bean先获取Spring容器,而后借助于Spring容器实现该功能。为了让Bean获取它所在的Spring容器,可让该Bean实现ApplicationContextAware接口。
感兴趣的读者本身能够试试。
总结:
回到循环依赖的问题,有的人可能会问singletonBeanFactory只是一个三级缓存,那么一级缓存和二级缓存有什么用呢?
其实你们只要理解整个流程就能够切入了,Spring在初始化Singleton的时候大体能够分几步,初始化——设值——销毁,循环依赖的场景下只有A——B——A这样的顺序,但在并发的场景下,每一步在执行时,都有可能调用getBean方法,而单例的Bean须要保证只有一个instance,那么Spring就是经过这些个缓存外加对象锁去解决这类问题,同时也能够省去没必要要的重复操做。Spring的锁的粒度选取也是很吊的,这里暂时不深刻研究了。
解决此类问题的关键是要对SpringIOC和DI的整个流程作到心中有数,看源码通常状况下不要求每一行代码都了解透彻,可是对于整个的流程和每一个流程中在作什么事须要了然,这样实际遇到问题时才能够很快的切入进行分析解决。
但愿这篇文章能够帮助你对Spring的IOC和DI的流程有一个更深入的认识!