Spring是日常工作中最常用的框架,而IoC
是其最核心的思想之一。IoC(Inversion of Control)的意思是控制反转,就是把原先我们代码里面需要实现的对象创建、依赖的代码,反转给容器来帮忙实现。整个容器的启动过程主要包含两大部分,第一部分是配置资源加载和注册,第二部分是Bean
实例的创建和依赖注入(DI)。本文会根据源码详细介绍配置资源加载和注册的实现原理。
Spring核心容器类
为了方便后续介绍,我们先得了解两个Spring核心容器类,分别是BeanFactory
、BeanDefinition
。
BeanFactory
BeanFactory
意思是管理Bean
的工厂,它定义了getBean()
、containsBean()
等管理Bean的通用方法,我们熟悉的ApplicationContext
接口就继承了BeanFactory
接口。 可以简单认为,BeanFactory
实例就是Spring底层容器,它是容器中所有Bean
的管理者。
1 | public interface BeanFactory { |
BeanDefinition
BeanDefinition
意思是Bean
定义,它描述了Bean
的各种基础属性信息,比如beanClassName
、singleton
、lazyInit
、scope
等等。可以认为,BeanDefinition
是完整创建Bean
实例的基础,而本文真正介绍的就是BeanDefinition
的加载和注册原理。
基于Xml的配置资源加载和注册
配置资源加载和注册具体来说就是BeanDefinition
的定位、加载和注册三部分。在Spring中,容器最后是以ApplicationContext
实例存在的。根据具体资源的不同,分为各类ApplicationContext
,比如ClasspathXmlApplicationContext
,AnnotationConfigApplicationContext
等等。类图如下:
ApplicationContext
允许上下文嵌套,对于一个Bean
的查找,优先从当前上下文查找,如果没找到,则从父上下文查找,逐级向上。
寻找入口
对于xml配置的Spring应用,在main()
方法中实例化ClasspathXmlApplicationContext
即可创建一个IoC容器。我们可以从这个构造方法开始,探究一下IoC容器的初始化过程。
1 | ApplicationContext app = new ClassPathXmlApplicationContext("application.xml"); |
实际调用的构造方法如下:
设置资源解析器和配置路径
在创建ClassPathXmlApplicationContext
容器时,构造方法做以下两项重要工作:
- 调用父类容器的构造方法
super(parent)
方法为容器设置好Bean
资源加载器。 - 调用父类
AbstractRefreshableConfigApplicationContext
的setConfigLocations(configLocations)
方法设置Bean
配置路径。
通过追踪ClassPathXmlApplicationContext
的继承体系,发现其父类的父类AbstractApplicationContext
中初始化IoC容器所做的主要操作如下:
从上面的代码可以看出,默认将会使用路径匹配资源解析器PathMatchingResourcePatternResolver
进行配置资源解析。继续跟踪构造方法源码:
在设置完容器的资源解析器之后,接下来ClassPathXmlApplicationContext
调用其父类 AbstractRefreshableConfigApplicationContext
的setConfigLocations()
方法设置配置定位路径,该方法的源码如下:
至此,Spring IoC容器已经设置到配置资源解析器,并加载好了配置定位路径,接下来就要正式启动容器了。
开始启动
Spring IoC 容器对Bean
配置资源的载入是从refresh()
方法开始的,refresh()
是一个模板方法,规定了IoC 容器的启动流程,具体逻辑要交给其子类去实现。refresh()
方法源码如下:
refresh()
方法主要为 IoC 容器Bean
的生命周期管理提供条件,载入配置信息是在第二步ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
进行的。
创建容器
obtainFreshBeanFactory()
方法调用子类容器的refreshBeanFactory()
方法,启动容器载入Bean
配置信息,代码如下:
容器真正调用的是其子类AbstractRefreshableApplicationContext
实现的refreshBeanFactory()
方法,方法的源码如下:
该方法中最重要的是调用了loadBeanDefinitions(beanFactory)
方法,从方法名就能推断出,该方法完成了BeanDefinition
的加载和注册!
载入配置路径
loadBeanDefinitions(beanFactory)
真正调用的是其子类AbstractXmlApplicationContext
的实现,源码如下:
由于我们使用ClassPathXmlApplicationContext
作为例子分析,因此getConfigResources
的返回值为null
,因此程序执行reader.loadBeanDefinitions(configLocations)
。
在XmlBeanDefinitionReader
的抽象父类AbstractBeanDefinitionReader
中定义了载入过程。源码如下:
AbstractRefreshableConfigApplicationContext
的loadBeanDefinitions(String... locations)
方法实际上是调用 AbstractBeanDefinitionReader
的loadBeanDefinitions()
方法。
从对AbstractBeanDefinitionReader
的loadBeanDefinitions()
方法源码分析可以看出该方法就做了两件事:
- 调用资源加载器的获取资源方法
resourceLoader.getResource(location)
,获取到要加载的资源。 - 真正执行加载功能是其子类
XmlBeanDefinitionReader
的loadBeanDefinitions()
方法。
获取配置资源
resourceLoader.getResource(location)
真正调用的是DefaultResourceLoader
实现的方法,源码如下:
getResource()
支持了多种途径加载资源,在本例中,最终执行的是从容器自身类路径中资源中获取。即执行getResourceByPath(location)
方法,最终返回的是ClassPathContextResource
实例。至此,完成了配置资源的获取。
加载配置
在获取配置资源之后,接下来执行AbstractBeanDefinitionReader
的loadBeanDefinitions(resource)
方法。源码如下:
loadBeanDefinitions(resource)
真正调用的是子类XmlBeanDefinitionReader
的方法。
在上面的实现中,先将xml配置资源解析成了一个Document
对象,然后根据该对象执行注册BeanDefinition
。注册BeanDefinition
源码如下:
这里进一步调用了DefaultBeanDefinitionDocumentReader
的registerBeanDefinitions()
方法。
从源码可以看出,加载xml配置时,如果碰到<import>
,会先导入该标签引用的资源;如果碰到<alias>
,会将定义的别名注册到容器中;如果是<Bean>
,则会解析并注册BeanDefinition
,这也是我们关注的重点。
在delegate.parseBeanDefinitionElement(ele)
方法中完成了BeanDefinition
的解析处理,具体解析逻辑不是本文的重点,这里只需要知道,此时BeanDefinitionHolder
中包含了xml配置文件中的各种信息。解析完成之后,通过BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())
进行注册。
向容器注册
BeanDefinitionReaderUtils
的registerBeanDefinition()
方法源码如下:
实际的注册逻辑调用了BeanDefinitionRegistry
类的registerBeanDefinition()
方法。而BeanDefinitionRegistry
的实现类为DefaultListableBeanFactory
,继续跟踪源码:
从这里可以发现,BeanDefinition
信息实际注册到了DefaultListableBeanFactory
的beanDefinitionMap
属性中。这里请注意,DefaultListableBeanFactory
是一个BeanFactory
。通过源码分析发现,不管是ClassPathXmlApplicationContext
还是AnnotationConfigApplicationContext
都持有DefaultListableBeanFactory
实例。也就是说,BeanDefinition
已经注册到容器中了,通过容器,可以获取所有的BeanDefinition
注册信息,为后续的Bean
的创建和依赖注入提供了基础!
基于Annotation的配置资源加载和注册
目前更常见的的是通过注解来配置Bean
,比如在类上加@Controller
、@Service
、@Component
等。下面我们就来分析基于Annotation的配置资源加载和注册过程。
寻找入口
基于Annotation的Spring IoC容器实例是AnnotationConfigApplicationContext
,我们可以通过它的构造方法进行分析。最关键的两个构造方法如下:
1 | // 创建一个新的 AnnotationConfigApplicationContext,从给定的带注解的加载bean定义并自动刷新上下文。 |
注解驱动的Spring容器有两种创建方式:
- 直接传入带有相关注解的类。
- 指定要扫描的基础包,将包下面所有带相关注解的Bean全部加载进去。
接下来,分别介绍这两种容器创建方式。
直接传入带有相关注解的类
显然,BeanDefinition
注册的关键代码在register(annotatedClasses)
中,我们直接看相关源码:
这里调用了AnnotatedBeanDefinitionReader
的register()
方法,继续跟进去:
从上面的源码我们可以看出,注册注解Bean
定义类的基本步骤如下:
- 使用注解元数据解析器解析注解
Bean
中关于作用域的配置。 - 使用
AnnotationConfigUtils
的processCommonDefinitionAnnotations()
方法处理注解Bean
定义类中通用的注解。 - 使用
AnnotationConfigUtils
的applyScopedProxyMode()
方法创建对于作用域的代理对象。 - 通过
BeanDefinitionReaderUtils
向容器注册Beandefinition
。
下面,我们详细分析这4个步骤:
使用注解元数据解析器解析注解Bean
中关于作用域的配置
通过调用AnnotationScopeMetadataResolver
类的resolveScopeMetadata()
方法解析注解Bean
定义类的作用域元信息。
使用AnnotationConfigUtils
的processCommonDefinitionAnnotations()
方法处理注解Bean
定义类中通用的注解
显然,这里在解析@Lazy
、@Primary
、@DependsOn
等注解值,然后给BeanDefinition
对应的字段赋值。
使用AnnotationConfigUtils
的applyScopedProxyMode()
方法创建对于作用域的代理对象
该方法根据作用域@Scope
注解的值,为Bean
定义应用相应的代理模式,主要是在Spring面向切面编程(AOP)中使用。
通过BeanDefinitionReaderUtils
向容器注册Beandefinition
该方法就是将Beandefinition
注册到容器中,前面详细介绍过,不再赘述。
指定要扫描的基础包
当创建注解处理容器时,如果传入的初始参数是注解Bean
定义类所在的包时,注解容器将扫描给定的包及其子包,将扫描到的注解Bean
定义载入并注册。
ClassPathBeanDefinitionScanner 扫描给定的包及其子包
AnnotationConfigApplicationContext
的scan()
方法实际调用了ClassPathBeanDefinitionScanner
的scan()
方法,源码如下:
从源码可以看到,在doScan()
方法里面,扫描了基础包下所有包含Bean
定义注解的类,并且给BeanDefinition
设置了对应的属性值。
向容器注册
在doScan()
方法最后,调用了registerBeanDefinition(definitionHolder, this.registry)
向容器注册。这个在前面介绍过,不再赘述。
小结
不管是基于xml
还是注解方式,都是将配置信息解析到BeanDefinition
中,然后再注册到容器中。至此,在容器中就能获取所有Bean
定义信息,接下来就是Bean
实例的创建和依赖注入(DI),下一章节再见!
原创不易,觉得文章写得不错的小伙伴,点个赞👍 鼓励一下吧~
欢迎关注我的开源项目:一款适用于SpringBoot的轻量级HTTP调用框架