1. BeanDefinition阶段的分析

Spring框架中控制反转(IOC)容器的BeanDefinition阶段的具体步骤,主要涉及到Bean的定义、加载、解析,并在后面进行编程式注入和后置处理。这个阶段是Spring框架中Bean生命周期的早期阶段之一,对于理解整个Spring框架非常关键。

  • 加载配置文件、配置类

在这一步,Spring容器通过配置文件或配置类来了解需要管理哪些Bean。对于基于XML的配置,通常使用ClassPathXmlApplicationContext或者FileSystemXmlApplicationContext

  • 解析配置文件、配置类并封装为BeanDefinition

Spring框架通过使用BeanDefinitionReader实例(如XmlBeanDefinitionReader)来解析配置文件。解析后,每个Bean配置会被封装成一个BeanDefinition对象,这个对象包含了类名、作用域、生命周期回调等信息。

  • 编程式注入额外的BeanDefinition

除了配置文件定义的Bean,也可以通过编程的方式动态添加BeanDefinitionIOC容器中,这增加了灵活性。

  • BeanDefinition的后置处理

BeanDefinition的后置处理是指容器允许使用BeanDefinitionRegistryPostProcessorBeanFactoryPostProcessor来对解析后的BeanDefinition做进一步处理,例如修改Bean的属性等。

2. 加载xml配置文件

2.1 XML配置文件中加载bean的代码示例

先给出最简单的代码示例,然后逐步分析

全部代码如下:

package com.example.demo.bean;// HelloWorld.javapublicclass HelloWorld {private String message;publicvoid setMessage(String message) {this.message = message;
    }publicvoid sayHello() {
        System.out.println("Hello," + message +"!");
    }
}

主程序:

package com.example.demo;

import com.example.demo.bean.HelloWorld;
import org.springframework.context.support.ClassPathXmlApplicationContext;publicclass DemoApplication {publicstaticvoid main(String[] args) {// 创建Spring上下文(容器)
        ClassPathXmlApplicationContext context =new ClassPathXmlApplicationContext("ApplicationContext.xml");// 从容器中获取bean,假设我们有一个名为 'helloWorld' 的bean
        HelloWorld helloWorld = context.getBean("helloWorld", HelloWorld.class);// 使用bean        helloWorld.sayHello();// 关闭上下文        context.close();
    }
}

xml文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Bean定义 -->
    <bean id="helloWorld"class="com.example.demo.bean.HelloWorld">
        <!-- 设置属性 -->
        <property name="message" value="World"/>
    </bean>

</beans>

运行结果:

接着我们就从这段代码开始分析

2.2 setConfigLocations - 设置和保存配置文件路径

我们还是以Spring 5.3.7的源码为例分析

// 创建Spring上下文(容器)
        ClassPathXmlApplicationContext context =new ClassPathXmlApplicationContext("ApplicationContext.xml");

这段代码,我们利用idea点击去分析,最后在ClassPathXmlApplicationContext的重载方法里看到调用了setConfigLocations设置配置文件的路径。

接着看看setConfigLocations方法

setConfigLocations() 方法的主要作用是设定Spring 容器加载Bean 定义时所需要读取的配置文件路径。这些路径可以是类路径下的资源、文件系统中的资源或者其他任何通过URL定位的资源。该方法确保所有提供的配置路径都被保存并在稍后的容器刷新操作中使用。

源码提出来分析:

publicvoid setConfigLocations(@Nullable String... locations) {if (locations !=null) {// 使用Spring的Assert类来校验,确保传入的配置位置数组中没有null元素。
        Assert.noNullElements(locations,"Config locations must not be null");// 根据传入的配置位置数量,初始化内部存储配置位置的数组。this.configLocations =new String[locations.length];// 遍历传入的配置位置数组。for(int i =0; i < locations.length; ++i) {// 调用resolvePath方法处理每一个配置位置(可能进行必要的路径解析,如解析占位符)。// trim()用于移除字符串首尾的空格,保证保存的路径是净化的。this.configLocations[i] =this.resolvePath(locations[i]).trim();
        }
    }else {// 如果传入的配置位置是null,清除掉所有已设定的配置位置。this.configLocations =null;
    }
}

在上下文被刷新的时候,这些配置文件位置会被读取,并且Spring容器将解析其中定义的beans并将它们注册到容器中。setConfigLocations() 方法只是设置了这些位置,而实际的加载和注册过程是在上下文刷新时完成的。

这个setConfigLocations方法通常不是由用户直接调用的,而是在ApplicationContext初始化的过程中被框架调用,例如在基于XML的配置中,我们会在初始化ClassPathXmlApplicationContextFileSystemXmlApplicationContext时提供配置文件的路径。

debug的时候,可以看到把测试代码中设置的xml 配置文件的路径保存了。

2.3 refresh - 触发容器刷新,配置文件的加载与解析

我们上面看到ClassPathXmlApplicationContext方法里面,执行完setConfigLocations后,紧接着有个refresh方法,我们来看看。

Spring框架中,refresh()方法是非常关键的,它是ApplicationContext接口的一部分。这个方法的主要功能是刷新应用上下文,加载或者重新加载配置文件中定义的Bean,初始化所有的单例,配置消息资源,事件发布器等。

代码提出来分析:

publicvoid refresh() throws BeansException, IllegalStateException {// 同步块,确保容器刷新过程的线程安全
    synchronized(this.startupShutdownMonitor) {// 开始上下文刷新的步骤记录,用于监控和诊断
        StartupStep contextRefresh =this.applicationStartup.start("spring.context.refresh");// 准备刷新过程,设置开始时间,状态标志等this.prepareRefresh();// 获取新的BeanFactory,如果是第一次刷新则创建一个BeanFactory
        ConfigurableListableBeanFactory beanFactory =this.obtainFreshBeanFactory();// 配置BeanFactory,注册忽略的依赖接口等this.prepareBeanFactory(beanFactory);try {// 允许BeanFactory的后置处理器对其进行修改this.postProcessBeanFactory(beanFactory);// 开始Bean工厂的后置处理步骤的监控
            StartupStep beanPostProcess =this.applicationStartup.start("spring.context.beans.post-process");// 调用BeanFactoryPostProcessorsthis.invokeBeanFactoryPostProcessors(beanFactory);// 注册BeanPostProcessors到BeanFactorythis.registerBeanPostProcessors(beanFactory);// Bean后置处理步骤结束            beanPostProcess.end();// 初始化MessageSource组件,用于国际化等功能this.initMessageSource();// 初始化事件广播器this.initApplicationEventMulticaster();// 留给子类覆盖的定制方法this.onRefresh();// 注册监听器this.registerListeners();// 初始化剩余的单例Beanthis.finishBeanFactoryInitialization(beanFactory);// 完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,发布ContextRefreshedEvent事件this.finishRefresh();
        }catch (BeansException var10) {// 捕获BeansException,记录警告信息,销毁已创建的Beanif (this.logger.isWarnEnabled()) {this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt:" + var10);
            }// 销毁已经初始化的单例Beanthis.destroyBeans();// 取消刷新,重置同步监视器上的标志位this.cancelRefresh(var10);// 抛出异常,结束刷新过程throw var10;
        }finally {// 在刷新的最后,重置Spring内核中的共享缓存this.resetCommonCaches();// 结束上下文刷新步骤的记录            contextRefresh.end();
        }
    }
}

这个方法精确执行一系列步骤来配置ApplicationContext,包括Bean的加载、注册和初始化。刷新过程包括了Bean定义的载入、注册以及Bean的初始化等一系列复杂的步骤。

在现代Spring框架中,ApplicationContext一般在容器启动时刷新一次。一旦容器启动并且上下文被刷新,所有的Bean就被加载并且创建了。尽管技术上可能存在调用refresh()方法多次的可能性,但这在实际中并不常见,因为这意味着重置应用上下文的状态并重新开始。这样做将销毁所有的单例Bean,并重新初始化它们,这在大多数应用中是不可取的,不仅代价昂贵而且可能导致状态丢失、数据不一致等问题。

对于基于xmlApplicationContext(如ClassPathXmlApplicationContext),在调用refresh()方法时会重新读取和解析配置文件,然后重新创建BeanFactoryBean的定义。如果容器已经被刷新过,则需要先销毁所有的单例Bean,关闭BeanFactory,然后重新创建。通常,这个功能用于开发过程中或者测试中,不推荐在生产环境使用,因为它的开销和风险都很大。

我们来看一下重点,加载配置文件的操作在哪里?这里图上我标注出来了,obtainFreshBeanFactory方法里面有个refreshBeanFactory方法。

refreshBeanFactory方法是个抽象方法,我们来看看实现类是怎么实现的,根据继承关系找到实现类的refreshBeanFactory方法。

refreshBeanFactory()方法通常在refresh()方法中被调用。这个方法确保当前ApplicationContext含有一个清洁状态的BeanFactory

代码提出来分析:

protected finalvoid refreshBeanFactory() throws BeansException {// 检查当前应用上下文是否已经包含了一个BeanFactoryif (this.hasBeanFactory()) {// 如果已经存在BeanFactory,销毁它管理的所有beanthis.destroyBeans();// 关闭现有的BeanFactory,释放其可能持有的任何资源this.closeBeanFactory();
    }try {// 创建一个DefaultListableBeanFactory的新实例,这是Spring中ConfigurableListableBeanFactory接口的默认实现
        DefaultListableBeanFactory beanFactory =this.createBeanFactory();// 为beanFactory设置一个序列化ID,这个ID后面可以用于反序列化
        beanFactory.setSerializationId(this.getId());// 允许子类定制新创建的beanFactorythis.customizeBeanFactory(beanFactory);// 从底层资源(例如XML文件)中加载bean定义到beanFactorythis.loadBeanDefinitions(beanFactory);// 将新的beanFactory赋值给这个上下文的beanFactory属性this.beanFactory = beanFactory;
    }catch (IOException var2) {// 如果在解析bean定义资源过程中发生I/O异常,将其包装并重新抛出为ApplicationContextExceptionthrownew ApplicationContextException("I/O错误解析用于" +this.getDisplayName() +"的bean定义源", var2);
    }
}

这个方法在AbstractApplicationContext的具体实现中被重写。它提供了刷新bean工厂的模板——如果已经存在一个,则将其销毁并关闭;然后创建一个新的bean工厂,进行定制,并填充bean定义。在加载bean定义(例如,从XML文件读取)时,如果遇到I/O异常,会抛出一个ApplicationContextException,提供有关错误性质的更多上下文信息。

这段代码我们可以看到有loadBeanDefinitions方法,是从底层资源(例如XML文件)中加载bean定义到beanFactory,逻辑很复杂,我们下面来进行单独分析。

2.4 loadBeanDefinitions - 具体的BeanDefinition加载逻辑

this.loadBeanDefinitions 方法是在AbstractApplicationContext 的子类中实现的,这种模式是一个典型的模板方法设计模式的例子。在模板方法设计模式中,一个算法的框架(即一系列的步骤)被定义在父类的方法中,但是一些步骤的具体实现会延迟到子类中完成。

AbstractApplicationContext 提供了refreshBeanFactory 方法的框架,这个方法定义了刷新BeanFactory 的步骤,但是它将loadBeanDefinitions 的具体实现留给了子类。子类需要根据具体的存储资源类型(比如XML 文件、Java 注解、Groovy 脚本等)来实现这个方法。

子类AbstractXmlApplicationContext实现的loadBeanDefinitions 方法如下:

loadBeanDefinitions()方法是Spring框架中用于加载、解析并注册Bean定义的核心方法。其基本职责是从一个或多个源读取配置信息,然后将这些信息转换成Spring容器可以管理的Bean定义。这个方法通常在Spring上下文初始化过程中被调用,是Spring容器装载Bean定义的关键步骤。

代码提出来分析:

// 使用DefaultListableBeanFactory作为Bean定义注册的目标工厂,加载Bean定义protectedvoid loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {// 创建一个读取XML Bean定义的读取器,并将工厂传入用于注册定义
    XmlBeanDefinitionReader beanDefinitionReader =new XmlBeanDefinitionReader(beanFactory);// 设置环境对象,可能包含属性解析相关的环境配置
    beanDefinitionReader.setEnvironment(this.getEnvironment());// 设置资源加载器,允许读取器加载XML资源
    beanDefinitionReader.setResourceLoader(this);// 设置实体解析器,用于解析XML中的实体如DTD
    beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));// 初始化Bean定义读取器,可能设置一些参数,如是否验证XMLthis.initBeanDefinitionReader(beanDefinitionReader);// 调用重载的loadBeanDefinitions,根据配置的资源和位置加载Bean定义this.loadBeanDefinitions(beanDefinitionReader);
}// 初始化Bean定义读取器,主要设置是否进行XML验证protectedvoid initBeanDefinitionReader(XmlBeanDefinitionReader reader) {// 设置XML验证模式,通常取决于应用上下文的配置
    reader.setValidating(this.validating);
}// 通过XmlBeanDefinitionReader加载Bean定义protectedvoid loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {// 获取所有配置资源的数组(如XML配置文件)
    Resource[] configResources =this.getConfigResources();// 如果配置资源非空,则加载这些资源if (configResources !=null) {
        reader.loadBeanDefinitions(configResources);
    }// 获取所有配置文件位置的数组
    String[] configLocations =this.getConfigLocations();// 如果配置文件位置非空,则加载这些位置指定的配置文件if (configLocations !=null) {
        reader.loadBeanDefinitions(configLocations);
    }
}

loadBeanDefinitions(DefaultListableBeanFactory beanFactory)方法中,首先创建了一个XmlBeanDefinitionReader实例,这个读取器是专门用来解析XML配置文件并把Bean定义加载到DefaultListableBeanFactory中。beanDefinitionReader的相关属性被设置了,包括环境变量、资源加载器和实体解析器。这些设置确保了beanDefinitionReader能正确地解析XML文件并能解析文件中的占位符和外部资源。

接着,通过调用initBeanDefinitionReader方法,可以对XmlBeanDefinitionReader实例进行一些额外的配置,例如设置XML验证。最后,调用loadBeanDefinitions(XmlBeanDefinitionReader reader)方法实际进行加载操作。这个方法会调用读取器来实际地读取和解析XML文件,把Bean定义加载到Spring容器中。

loadBeanDefinitions(XmlBeanDefinitionReader reader)方法中,首先尝试从getConfigResources方法获取XML配置文件资源,如果存在这样的资源,则通过reader加载这些定义。其次,尝试获取配置文件位置信息,如果存在,则通过reader加载这些位置指定的配置文件。这种设计允许从不同的来源加载配置,如直接从资源文件或者从指定的文件路径。

debug可以看到reader和configLocations的详细状态

cke_135.png

这里看到还有一个reader.loadBeanDefinitions(configLocations);这是在做什么呢?下面接着来看!

2.5 loadBeanDefinitions - 由XmlBeanDefinitionReader实现

debug的时候可以看到这里的reader是XmlBeanDefinitionReader,点击跟踪reader.loadBeanDefinitions(configLocations);方法,调用的方法在AbstractBeanDefinitionReader,而XmlBeanDefinitionReader 继承自 AbstractBeanDefinitionReader。

cke_136.png

这里配置文件循环加载,有一个count += this.loadBeanDefinitions(location); 继续跟踪!

cke_137.png

这段代码的逻辑动作大致为:

  1. 根据传入的资源位置字符串,通过资源加载器(ResourceLoader)获取对应的资源。
  2. 如果资源加载器是资源模式解析器(ResourcePatternResolver),它会处理路径中的模式(比如通配符),加载所有匹配的资源。
  3. 读取资源,解析并注册其中定义的所有bean定义。
  4. 如果提供了一个实际资源的集合(actualResources),解析出来的资源将被添加到这个集合中。
  5. 返回加载并注册的bean定义的数量。

我们还是看重点,继续跟踪里面的loadBeanDefinitions

cke_138.png

代码提出来分析:

publicint loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {// 将Resource包装为EncodedResource,允许指定编码,然后继续加载Bean定义returnthis.loadBeanDefinitions(new EncodedResource(resource));

}publicint loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {// 断言传入的EncodedResource不为空
Assert.notNull(encodedResource,"EncodedResource must not be null");// 如果日志级别为trace,则输出跟踪日志if (this.logger.isTraceEnabled()) {this.logger.trace("Loading XML bean definitions from" + encodedResource);

}// 获取当前线程正在加载的资源集合
Set<EncodedResource> currentResources = (Set)this.resourcesCurrentlyBeingLoaded.get();// 检查资源是否已经在加载中,如果是,则抛出BeanDefinitionStoreException异常,避免循环加载if (!currentResources.add(encodedResource)) {thrownew BeanDefinitionStoreException("Detected cyclic loading of" + encodedResource +" - check your import definitions!");

}else {int var6;// 这将用来存储加载的Bean定义数量try {// 打开资源的InputStream进行读取
InputStream inputStream= encodedResource.getResource().getInputStream();

Throwable var4=null;try {// 将InputStream封装为InputSource,XML解析器可以接受这个类型
InputSource inputSource=new InputSource(inputStream);// 如果资源编码不为空,设置资源的编码if (encodedResource.getEncoding() !=null) {

inputSource.setEncoding(encodedResource.getEncoding());

}// 实际加载Bean定义的方法,返回加载的Bean定义数量
var6=this.doLoadBeanDefinitions(inputSource, encodedResource.getResource());

}catch (Throwable var24) {// 捕获Throwable以便在finally块中处理资源释放
var4= var24;throw var24;

}finally {// 关闭InputStream资源if (inputStream !=null) {if (var4 !=null) {try {

inputStream.close();

}catch (Throwable var23) {// 添加被抑制的异常
var4.addSuppressed(var23);

}

}else {

inputStream.close();

}

}

}

}catch (IOException var26) {// 抛出IOException异常,如果解析XML文档失败thrownew BeanDefinitionStoreException("IOException parsing XML document from" + encodedResource.getResource(), var26);

}finally {// 从当前加载的资源集合中移除该资源
currentResources.remove(encodedResource);// 如果当前加载的资源集合为空,则从ThreadLocal中移除if (currentResources.isEmpty()) {this.resourcesCurrentlyBeingLoaded.remove();

}

}// 返回加载的Bean定义数量return var6;

}

}
在这段代码中,loadBeanDefinitions 首先将Resource转换为EncodedResource,这允许它保留关于资源编码的信息。然后,它尝试将资源加载为InputStream并将其转换为InputSource,这是XML解析所需要的。接着它调用doLoadBeanDefinitions方法,实际上负责解析XML并注册Bean定义。

在这个过程中,代码确保了不会循环加载相同的资源,并且在加载资源时,如果发生异常,会适当地清理资源并报告错误。加载的Bean定义数量在完成后被返回。

我们来重点看下这段代码的重点步骤:doLoadBeanDefinitions方法!

2.6 doLoadBeanDefinitions - 读取并解析XML配置文件内容

cke_139.png

doLoadBeanDefinitions方法做了什么?

具体步骤如下:

  1. 使用doLoadDocument方法将给定的InputSource解析为 DOM Document对象。这个Document对象代表了 XML 文件的结构。
  2. 通过调用registerBeanDefinitions方法,将解析得到的Document中的 Bean 定义注册到 Spring 的 Bean 工厂中。这个方法返回注册的 Bean 定义的数量。
  3. 如果日志级别设置为 DEBUG,则会记录加载的 Bean 定义数量。

这里重点是registerBeanDefinitions方法,继续跟踪代码

cke_140.png

继续看重点,最终追到doRegisterBeanDefinitions方法

cke_141.png

doRegisterBeanDefinitions(Element root) 方法是 Spring 框架中用于解析 XML 配置文件中的 Bean 定义并注册它们到 Spring 容器的方法。这个方法通常在 XML 文件读取并转换成 DOM(Document Object Model)树之后调用,此时 XML 文件的根元素通过参数 root 传递给这个方法。

代码提出来分析:

protectedvoid doRegisterBeanDefinitions(Element root) {// 保存旧的解析代理(delegate),以便之后可以恢复
BeanDefinitionParserDelegate parent=this.delegate;// 创建新的解析代理(delegate),用于处理当前XML根节点的解析this.delegate =this.createDelegate(this.getReaderContext(), root, parent);// 如果当前节点使用的是Spring默认的XML命名空间if (this.delegate.isDefaultNamespace(root)) {// 获取根节点的"profile"属性
String profileSpec= root.getAttribute("profile");// 检查"profile"属性是否有文本内容if (StringUtils.hasText(profileSpec)) {// 按逗号、分号和空格分隔"profile"属性值,得到指定的profiles数组
String[] specifiedProfiles= StringUtils.tokenizeToStringArray(profileSpec,",;");// 如果当前环境不接受任何指定的profiles,则不加载该Bean定义文件if (!this.getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {// 如果日志级别是DEBUG,则记录跳过文件的信息if (this.logger.isDebugEnabled()) {this.logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +"] not matching:" +this.getReaderContext().getResource());

}// 退出方法,不进行后续处理return;

}

}

}// 在解析XML前进行预处理,可被重写的方法this.preProcessXml(root);// 解析XML根节点下的Bean定义this.parseBeanDefinitions(root,this.delegate);// 在解析XML后进行后处理,可被重写的方法this.postProcessXml(root);// 恢复旧的解析代理(delegate)this.delegate = parent;

}

上述代码片段是Spring框架用于注册Bean定义的内部方法。该方法在解析XML配置文件并注册Bean定义到Spring容器时被调用。它包含处理profile属性以根据运行时环境决定是否加载特定Bean定义的逻辑,以及前后处理钩子,允许在解析前后进行自定义操作。最后,它确保解析代理(delegate)被重置为之前的状态,以维护正确的状态。

接着,我们要看看是如何解析xml的,重点关注下parseBeanDefinitions方法

2.7 parseBeanDefinitions - 解析XML中的BeanDefinition元素

cke_142.png

parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) 方法的主要目的是遍历 XML 配置文件的根节点,解析并注册其中定义的所有 Bean。该方法负责区分不同类型的元素,即默认命名空间下的标准元素和自定义命名空间下的自定义元素,并对它们进行相应的处理。

代码提出来分析:

protectedvoid parseBeanDefinitions(Element root, BeanDefinitionParserDelegatedelegate) {// 判断根节点是否使用的是Spring的默认命名空间if (delegate.isDefaultNamespace(root)) {// 获取所有子节点
NodeList nl= root.getChildNodes();// 遍历所有子节点for (int i =0; i < nl.getLength(); ++i) {

Node node= nl.item(i);// 只处理Element类型的节点(过滤掉文本节点等其他类型)if (node instanceof Element) {

Element ele= (Element)node;// 如果子元素节点也是默认命名空间,则调用parseDefaultElement方法解析if (delegate.isDefaultNamespace(ele)) {this.parseDefaultElement(ele,delegate);

}else {// 如果子元素节点不是默认命名空间,则调用parseCustomElement方法解析// 这通常表示节点定义了自定义的行为,可能是用户自定义的标签或者是Spring扩展的标签delegate.parseCustomElement(ele);

}

}

}

}else {// 如果根节点不是默认命名空间,那么它可能是一个自定义标签的顶级元素// 在这种情况下,直接调用parseCustomElement进行解析delegate.parseCustomElement(root);

}

}

这段代码的作用是解析XML文件中定义的bean。它检查每个XML元素(包括根元素和子元素),并根据这些元素是否属于Spring的默认命名空间(通常是""),调用不同的处理方法。如果元素属于默认命名空间,那么它将调用parseDefaultElement来解析标准的Spring配置元素,例如<bean>。如果元素不属于默认命名空间,那么将认为它是一个自定义元素,并调用parseCustomElement来解析。自定义元素通常是由开发人员定义或Spring扩展提供的,以增加框架的功能。

这里可以看到是一个循环处理Element节点,解析的动作主要是parseDefaultElement方法,继续来看看。

cke_143.png

parse