Spring源码阅读Spring容器启动原理(上)-配置资源加载和注册

Posted Javachichi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring源码阅读Spring容器启动原理(上)-配置资源加载和注册相关的知识,希望对你有一定的参考价值。

Spring是日常工作中最常用的框架,而IoC是其最核心的思想之一。IoC(Inversion of Control)的意思是控制反转,就是把原先我们代码里面需要实现的对象创建、依赖的代码,反转给容器来帮忙实现。整个容器的启动过程主要包含两大部分,第一部分是配置资源加载和注册,第二部分是Bean实例的创建和依赖注入(DI)。本文会根据源码详细介绍配置资源加载和注册的实现原理。

Spring核心容器类

为了方便后续介绍,我们先得了解两个Spring核心容器类,分别是BeanFactoryBeanDefinition

BeanFactory

BeanFactory意思是管理Bean的工厂,它定义了getBean()containsBean()等管理Bean的通用方法,我们熟悉的ApplicationContext 接口就继承了BeanFactory接口。 可以简单认为,BeanFactory实例就是Spring底层容器,它是容器中所有Bean的管理者

public interface BeanFactory {

	//对FactoryBean的转义定义,因为如果使用bean的名字检索FactoryBean得到的对象是工厂生成的对象,
	//如果需要得到工厂本身,需要转义
	String FACTORY_BEAN_PREFIX = "&";

	
	//根据bean的名字,获取在IOC容器中得到bean实例
	Object getBean(String name) throws BeansException;

	//根据bean的名字和Class类型来得到bean实例,增加了类型安全验证机制。
	<T> T getBean(String name, @Nullable Class<T> requiredType) throws BeansException;

	Object getBean(String name, Object... args) throws BeansException;

	<T> T getBean(Class<T> requiredType) throws BeansException;

	<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;


	//提供对bean的检索,看看是否在IOC容器有这个名字的bean
	boolean containsBean(String name);

	// 是否是单例bean
	boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

	// 是否是原型Bean
	boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
	
	boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
	
	boolean isTypeMatch(String name, @Nullable Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
	
	//得到bean实例的Class类型
	@Nullable
	Class<?> getType(String name) throws NoSuchBeanDefinitionException;
	
	//得到bean的别名,如果根据别名检索,那么其原名也会被检索出来
	String[] getAliases(String name);

} 

BeanDefinition

BeanDefinition意思是Bean定义,它描述了Bean的各种基础属性信息,比如beanClassNamesingletonlazyInitscope等等。可以认为,BeanDefinition是完整创建Bean实例的基础,而本文真正介绍的就是BeanDefinition的加载和注册原理

BeanDefinition-method

基于Xml的配置资源加载和注册

配置资源加载和注册具体来说就是BeanDefinition的定位、加载和注册三部分。在Spring中,容器最后是以ApplicationContext实例存在的。根据具体资源的不同,分为各类ApplicationContext,比如ClasspathXmlApplicationContextAnnotationConfigApplicationContext等等。类图如下: ApplicationContext

ApplicationContext允许上下文嵌套,对于一个Bean的查找,优先从当前上下文查找,如果没找到,则从父上下文查找,逐级向上。

寻找入口

对于xml配置的Spring应用,在main()方法中实例化ClasspathXmlApplicationContext即可创建一个IoC容器。我们可以从这个构造方法开始,探究一下IoC容器的初始化过程。

ApplicationContext app = new ClassPathXmlApplicationContext("application.xml"); 

实际调用的构造方法如下: ClassPathXmlApplicationContext-constructor

设置资源解析器和配置路径

在创建ClassPathXmlApplicationContext容器时,构造方法做以下两项重要工作:

  1. 调用父类容器的构造方法super(parent)方法为容器设置好Bean资源加载器。
  2. 调用父类AbstractRefreshableConfigApplicationContextsetConfigLocations(configLocations)方法设置Bean配置路径。

通过追踪ClassPathXmlApplicationContext的继承体系,发现其父类的父类AbstractApplicationContext中初始化IoC容器所做的主要操作如下: AbstractApplicationContext-constructor

从上面的代码可以看出,默认将会使用路径匹配资源解析器PathMatchingResourcePatternResolver进行配置资源解析。继续跟踪构造方法源码: PathMatchingResourcePatternResolver-constructor

在设置完容器的资源解析器之后,接下来ClassPathXmlApplicationContext调用其父类 AbstractRefreshableConfigApplicationContextsetConfigLocations()方法设置配置定位路径,该方法的源码如下:

AbstractRefreshableConfigApplicationContext-setConfigLocations

至此,Spring IoC容器已经设置到配置资源解析器,并加载好了配置定位路径,接下来就要正式启动容器了

开始启动

Spring IoC 容器对Bean配置资源的载入是从refresh()方法开始的,refresh()是一个模板方法,规定了IoC 容器的启动流程,具体逻辑要交给其子类去实现。refresh()方法源码如下: AbstractApplicationContext-refresh

refresh()方法主要为 IoC 容器Bean的生命周期管理提供条件,载入配置信息是在第二步ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();进行的。

如果你觉得自己学习效率低,缺乏正确的指导,可以加入资源丰富,学习氛围浓厚的技术圈一起学习交流吧!
[Java架构群]
群内有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的JAVA交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。

创建容器

obtainFreshBeanFactory()方法调用子类容器的refreshBeanFactory()方法,启动容器载入Bean配置信息,代码如下: AbstractApplicationContext-obtainFreshBeanFactory

容器真正调用的是其子类AbstractRefreshableApplicationContext实现的refreshBeanFactory()方法,方法的源码如下: AbstractRefreshableApplicationContext-refreshBeanFactory

该方法中最重要的是调用了loadBeanDefinitions(beanFactory)方法,从方法名就能推断出,该方法完成了BeanDefinition的加载和注册!

载入配置路径

loadBeanDefinitions(beanFactory)真正调用的是其子类AbstractXmlApplicationContext的实现,源码如下: AbstractXmlApplicationContext-loadBeanDefinitions1

AbstractXmlApplicationContext-loadBeanDefinitions2

由于我们使用ClassPathXmlApplicationContext作为例子分析,因此getConfigResources的返回值为null,因此程序执行reader.loadBeanDefinitions(configLocations)

XmlBeanDefinitionReader的抽象父类AbstractBeanDefinitionReader中定义了载入过程。源码如下: AbstractXmlApplicationContext-loadBeanDefinitions

AbstractRefreshableConfigApplicationContextloadBeanDefinitions(String... locations)方法实际上是调用 AbstractBeanDefinitionReaderloadBeanDefinitions()方法。

从对AbstractBeanDefinitionReaderloadBeanDefinitions()方法源码分析可以看出该方法就做了两件事:

  1. 调用资源加载器的获取资源方法resourceLoader.getResource(location),获取到要加载的资源。
  2. 真正执行加载功能是其子类XmlBeanDefinitionReaderloadBeanDefinitions()方法。

获取配置资源

resourceLoader.getResource(location)真正调用的是DefaultResourceLoader实现的方法,源码如下: DefaultResourceLoader-getResource

getResource()支持了多种途径加载资源,在本例中,最终执行的是从容器自身类路径中资源中获取。即执行getResourceByPath(location)方法,最终返回的是ClassPathContextResource实例。至此,完成了配置资源的获取。

加载配置

在获取配置资源之后,接下来执行AbstractBeanDefinitionReaderloadBeanDefinitions(resource)方法。源码如下: AbstractXmlApplicationContext-loadBeanDefinitions2

loadBeanDefinitions(resource)真正调用的是子类XmlBeanDefinitionReader的方法。 XmlBeanDefinitionReader-loadBeanDefinitions

在上面的实现中,先将xml配置资源解析成了一个Document对象,然后根据该对象执行注册BeanDefinition。注册BeanDefinition源码如下: XmlBeanDefinitionReader-registerBeanDefinitions

这里进一步调用了DefaultBeanDefinitionDocumentReaderregisterBeanDefinitions()方法。 DefaultBeanDefinitionDocumentReader-registerBeanDefinitions

DefaultBeanDefinitionDocumentReader-parseBeanDefinitions

从源码可以看出,加载xml配置时,如果碰到<import>,会先导入该标签引用的资源;如果碰到<alias>,会将定义的别名注册到容器中;如果是<Bean>,则会解析并注册BeanDefinition,这也是我们关注的重点。 DefaultBeanDefinitionDocumentReader-processBeanDefinition

delegate.parseBeanDefinitionElement(ele)方法中完成了BeanDefinition的解析处理,具体解析逻辑不是本文的重点,这里只需要知道,此时BeanDefinitionHolder中包含了xml配置文件中的各种信息。解析完成之后,通过BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())进行注册。

向容器注册

BeanDefinitionReaderUtilsregisterBeanDefinition()方法源码如下: BeanDefinitionReaderUtils-registerBeanDefinition

实际的注册逻辑调用了BeanDefinitionRegistry类的registerBeanDefinition()方法。而BeanDefinitionRegistry的实现类为DefaultListableBeanFactory,继续跟踪源码: DefaultListableBeanFactory-registerBeanDefinition

从这里可以发现,BeanDefinition信息实际注册到了DefaultListableBeanFactorybeanDefinitionMap属性中。这里请注意,DefaultListableBeanFactory是一个BeanFactory。通过源码分析发现,不管是ClassPathXmlApplicationContext还是AnnotationConfigApplicationContext都持有DefaultListableBeanFactory实例。也就是说,BeanDefinition已经注册到容器中了,通过容器,可以获取所有的BeanDefinition注册信息,为后续的Bean的创建和依赖注入提供了基础!

基于Annotation的配置资源加载和注册

目前更常见的的是通过注解来配置Bean,比如在类上加@Controller@Service@Component等。下面我们就来分析基于Annotation的配置资源加载和注册过程。

寻找入口

基于Annotation的Spring IoC容器实例是AnnotationConfigApplicationContext,我们可以通过它的构造方法进行分析。最关键的两个构造方法如下:

// 创建一个新的 AnnotationConfigApplicationContext,从给定的带注解的加载bean定义并自动刷新上下文。
public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
    this();
    register(annotatedClasses);
    refresh();
}

// 创建一个新的 AnnotationConfigApplicationContext,扫描给定包中的 bean 定义并自动刷新上下文。
public AnnotationConfigApplicationContext(String... basePackages) {
        this();
        scan(basePackages);
        refresh();
        } 

注解驱动的Spring容器有两种创建方式:

  1. 直接传入带有相关注解的类。
  2. 指定要扫描的基础包,将包下面所有带相关注解的Bean全部加载进去。

接下来,分别介绍这两种容器创建方式。

直接传入带有相关注解的类

显然,BeanDefinition注册的关键代码在register(annotatedClasses)中,我们直接看相关源码: AnnotationConfigApplicationContext-register

这里调用了AnnotatedBeanDefinitionReaderregister()方法,继续跟进去: AnnotatedBeanDefinitionReader-register

AnnotatedBeanDefinitionReader-doRegisterBean

从上面的源码我们可以看出,注册注解Bean定义类的基本步骤如下:

  1. 使用注解元数据解析器解析注解Bean中关于作用域的配置。
  2. 使用AnnotationConfigUtilsprocessCommonDefinitionAnnotations()方法处理注解Bean定义类中通用的注解。
  3. 使用AnnotationConfigUtilsapplyScopedProxyMode()方法创建对于作用域的代理对象。
  4. 通过BeanDefinitionReaderUtils向容器注册Beandefinition

下面,我们详细分析这4个步骤:

使用注解元数据解析器解析注解Bean中关于作用域的配置

通过调用AnnotationScopeMetadataResolver类的resolveScopeMetadata()方法解析注解Bean定义类的作用域元信息。 AnnotationScopeMetadataResolver-resolveScopeMetadata

使用AnnotationConfigUtilsprocessCommonDefinitionAnnotations()方法处理注解Bean定义类中通用的注解

AnnotationConfigUtils-processCommonDefinitionAnnotations

显然,这里在解析@Lazy@Primary@DependsOn等注解值,然后给BeanDefinition对应的字段赋值。

使用AnnotationConfigUtilsapplyScopedProxyMode()方法创建对于作用域的代理对象

AnnotationConfigUtils-applyScopedProxyMode

该方法根据作用域@Scope注解的值,为Bean定义应用相应的代理模式,主要是在Spring面向切面编程(AOP)中使用。

通过BeanDefinitionReaderUtils向容器注册Beandefinition

该方法就是将Beandefinition注册到容器中,前面详细介绍过,不再赘述。

指定要扫描的基础包

当创建注解处理容器时,如果传入的初始参数是注解Bean定义类所在的包时,注解容器将扫描给定的包及其子包,将扫描到的注解Bean定义载入并注册。

ClassPathBeanDefinitionScanner 扫描给定的包及其子包

AnnotationConfigApplicationContextscan()方法实际调用了ClassPathBeanDefinitionScannerscan()方法,源码如下:

ClassPathBeanDefinitionScanner-scan

ClassPathBeanDefinitionScanner-doScan

从源码可以看到,在doScan()方法里面,扫描了基础包下所有包含Bean定义注解的类,并且给BeanDefinition设置了对应的属性值。

向容器注册

doScan()方法最后,调用了registerBeanDefinition(definitionHolder, this.registry)向容器注册。这个在前面介绍过,不再赘述。

小结

不管是基于xml还是注解方式,都是将配置信息解析到BeanDefinition中,然后再注册到容器中。至此,在容器中就能获取所有Bean定义信息,接下来就是Bean实例的创建和依赖注入(DI),下一章节再见!

最后

秃头哥给大家分享一篇一线开发大牛整理的java高并发核心编程神仙文档,里面主要包含的知识点有:多线程、线程池、内置锁、JMM、CAS、JUC、高并发设计模式、Java异步回调、CompletableFuture类等。

文档地址:一篇神文就把java多线程,锁,JMM,JUC和高并发设计模式讲明白了

码字不易,如果觉得本篇文章对你有用的话,请给我一键三连!关注作者,后续会有更多的干货分享,请持续关注!

以上是关于Spring源码阅读Spring容器启动原理(上)-配置资源加载和注册的主要内容,如果未能解决你的问题,请参考以下文章

Spring源码分析专题——目录

spring源码阅读-- 容器启动之创建bean

spring源码阅读-- 容器启动之加载BeanDefinition

spring源码阅读-- 容器启动之BeanFactoryPostProcessor

#yyds干货盘点#Spring 源码阅读Spring IoCAOP 原理小总结

深入浅出Spring原理及实战「原理分析专题」不看源码就带你剖析IOC容器核心流程以及运作原理