bean定义加入ioc容器的源码分析
errol发表于2025-08-27 | 分类为 编程 | 标签为spring源码java

本文是spring框架源码学习系列的第一篇。

当然,也并非那种吃透代码、扣细节程度的分析,只是单纯从一个开发者的角度,简单研究一些常用的组件的创建,添加和运作方式。

从整体上认识框架,了解其工作原理,对排查和解决问题都很有帮助,提升框架使用熟练度的同时,也可以在一定程度上定制和拓展框架,所以个人认为,这是有必要的一环。

文章要分析的是比较接近框架“底层”的源码。一般来说,从java类转为spring bean要经历一个过程:

java类 -> 字节码 -> class -> bean定义 -> ioc -> bean -> ioc

也就是说,在spring中,是先有bean定义,才有bean对象,意识到这一点很重要。

本文研究的只是其中的一部分,从class到ioc,即: 字节码 -> class -> bean定义,主要是关于spring如何创建bean定义,以及如何加入ioc容器的问题,因此相对来说没那么复杂。

以下将尽可能排除其他干扰因素,用一个最简单的spring应用为案例,来分析这个转换的过程。

bean定义,即BeanDefinition,是spring框架中的内部数据结构,它是对一个bean的元数据描述(Metadata),如类信息、作用域、构造参数、依赖、属性、初始化方法等等,spring会根据bean定义来实例化bean

一、项目结构

.
|___debug-5.iml
|___pom.xml
|___src
| |___test
| | |___java
| |___main
| | |___resources
| | | |___application.properties
| | |___java
| | | |___com
| | | | |___err0l
| | | | | |___spring
| | | | | | |___debug5
| | | | | | | |___HelloWorld.java
| | | | | | | |___Debug5Application.java
| | | | | | | |___TestController.java
| | | | | | | |___AppConfig.java

其中,AppConfig为主配置类,Debug5Application为启动类,HelloWorld和TestController是没有任何业务逻辑的普通类。

// 配置类; 启用组件扫描功能;
@Configuration
@ComponentScan
public class AppConfig {
}

// 启动类
public class Debug5Application {
    public static void main(String[] args) {
        // 使用基于注解的ApplicationContext
        final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        final String[] definitionNames = context.getBeanDefinitionNames();
        for (String name : definitionNames) {
            System.out.println(name);
        }
    }
}

@Component
public class HelloWorld {
    public String key = "hello, ";
    public String value = "world";
}

@Controller
public class TestController {
}

pom.xml里也只有spring的核心依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.2.8</version>
    </dependency>
</dependencies>

二、注册ConfigurationClassPostProcessor

ConfigurationClassPostProcessor是一个BeanFactoryPostProcessor接口的实现类,是spring的核心组件之一,用于处理使用@Configuration注解标注的配置类,如AppConfig。

因此,要分析bean定义的注册过程,需重点关注该类。

同时,它也是一个spring bean,或者说,任何受spring管理的对象都称为bean,所以也需要注册进ioc,其调用栈为:

- new AnnotationConfigApplicationContext(AppConfig.class);
-- this();
--- new AnnotatedBeanDefinitionReader(this);
---- AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);

image

图2-1 给postProcessBeanDefinitionRegistry()设置断点

image

图2-2 注册ConfigurationClassPostProcessor的bean定义信息

安装好依赖,并在ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry()方法里打上断点后,并启动应用。

BeanFactoryPostProcessor是spring框架中的一个扩展点,主要作用是在ioc容器实例化任何bean之前,读取并修改容器中已注册的bean定义的元数据信息(包括注册新的定义)

三、调用BeanFactoryPostProcessor

BeanFactoryPostProcessor的调用先于任何bean相关的调用逻辑之前。

- new AnnotationConfigApplicationContext(AppConfig.class);
-- refresh();
--- invokeBeanFactoryPostProcessors(beanFactory);
---- PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
----- invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
------ postProcessor.postProcessBeanDefinitionRegistry(registry);

image

图3-1 执行到postProcessBeanDefinitionRegistry()方法中断点处

四、调用ConfigurationClassPostProcessor

1、筛选配置类

spring会先将ioc容器中的所有bean定义都当做是配置类候选,但经筛选后,只剩AppConfig。

image

图4-1 筛选候选

顺便一提,AppConfig的bean定义是在实例化AnnotationConfigApplicationContext时加入ioc容器的

2、解析配置类

接着解析配置类候选。

image

图4-2 解析候选

将配置类bean定义包装为ConfigurationClass后再处理。

- ConfigurationClass configClass = new ConfigurationClass(beanDef.getMetadata(), beanName, (beanDef instanceof ScannedGenericBeanDefinition));
- processConfigurationClass(configClass, DEFAULT_EXCLUSION_FILTER);
-- doProcessConfigurationClass(configClass, sourceClass, filter);
3、扫描组件

查找当前bean定义中是否使用了@ComponentScan注解,说白了就是检查是否启用组件扫描功能。

image

图4-3 查找@ComponentScan

结合前文可知,AppConfig类确实带有@ComponentScan注解,因此,componentScans不会为空,进而开始以当前SourceClass为基础,扫描其他组件。

image

图4-4 扫描组件

如果未设置@ComponentScan的basePackages属性的话,则取当前配置类的所在包为基础路径。

image

图4-5 获得包名

image

图4-6 根据包名查找

其规则为,类路径、包名、资源名拼接在一起的表达式。

4、加载字节码文件

classpath*:com/err0l/spring/debug5/**/*.class

image

图4-7 获得资源文件

5、创建bean定义

进一步判断,如果符合条件的话,如@Component、@Configuration、@Service、@Controller等,就将其包装为bean定义,并加入candidates待返回上层

image

图4-8 判断是否为组件(候选)

如,本文代码中的HelloWorld、TestController。

image

图4-9 将字节码对应的class对象包装为bean定义信息

6、将bean定义加入ioc

findCandidateComponents()方法返回调用后,进一步处理候选对象,如判断ioc容器中是否已经存在了同名的bean定义等。

image

图4-10 判断候选是否符合加入ioc的条件

如果checkCandidate(beanName, candidate)为true,则最终将bean定义信息加入ioc容器。

if (checkCandidate(beanName, candidate)) {
    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
    definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
    beanDefinitions.add(definitionHolder);
    // 将definitionHolder加入registry(ioc)
    registerBeanDefinition(definitionHolder, this.registry);
}
7、进一步处理bean定义

类似的,在this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName())返回后,对已加入ioc容器的bean定义(scannedBeanDefinitions)进行处理。

for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
    // ...
    if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
        parse(bdCand.getBeanClassName(), holder.getBeanName());v
    }
}

因为加入ioc容器的bean定义本身,也可能是配置类,所以需要进一步处理。

image

图4-11 判断已解析的bean定义是否为配置类-1

如果是配置类的话,就以解析配置类的方式,对其进行解析:

- ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)
- parse(bdCand.getBeanClassName(), holder.getBeanName());
-- ConfigurationClass configClass = new ConfigurationClass(reader, beanName);
-- processConfigurationClass(configClass, DEFAULT_EXCLUSION_FILTER);

image

图4-12 判断已解析的bean定义是否为配置类-2

至于判断条件,首先看bean定义是否使用了@Configuration注解,其次看是否使用了@ImportResource、@Import、@Component(包括其他派生注解)、@ComponentScan,最后看里是否含有@Bean标注的方法。

也就是说,使用了@Component或其派生注解的类,都会被spring看做是一个配置类,更准确地说,是一个轻量级的配置类

// CONFIGURATION_CLASS_ATTRIBUTE: configurationClass
// CONFIGURATION_CLASS_LITE: lite
if (config != null || Boolean.TRUE.equals(beanDef.getAttribute(CANDIDATE_ATTRIBUTE)) || isConfigurationCandidate(metadata)) {
    beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
}
8、@Import相关

@ComponentScan注解相关的逻辑执行完后,开始处理@Import注解。

// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

@Import是spring提供的另一种导入组件的方式,主要是为了导入第三方组件而设置,使用时需要实现ImportSelector或ImportBeanDefinitionRegistrar接口,像springboot就是用这种方式来整合第三方依赖。

由于本文并没有用到,所以... 略。

基本的原理都是一样的,筛选 -> 解析 -> 加入ioc

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
    Class<?>[] value();
}
9、@ImportResource相关

@ImportResource也一样,略。

AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
// ...
10、@Bean相关
1) 包装@Bean方法为BeanMethod

接着处理@Bean注解相关部分,如果存在,则包装为BeanMethod类型,并加入当前配置类中(但未调用)。

image

图4-13 获取@Bean标注的方法

在以上步骤都运行后,调用当前节点的最终处理方法(不过,因为本案例比较简单,这个方法里什么都没做)。

image

图4-14 调用deferredImportSelectorHandler.process()方法

2) 将BeanMethod包装为bean定义,并加入ioc

主配置类解析完毕,最后的最后,加载配置类中附加的bean定义,如@Bean标注的方法等。

image

图4-15 调用this.reader.loadBeanDefinitions(configClasses)

如果存在BeanMethod的话,就将其包装为bean定义,并加入ioc容器。

for (BeanMethod beanMethod : configClass.getBeanMethods()) {
    loadBeanDefinitionsForBeanMethod(beanMethod);
}

至此,关于spring装载bean定义的源码分析到尾声了。

四、结语

本文粗浅地分析了从创建bean定义到加入ioc容器的过程。

其中的关键在于ConfigurationClassPostProcessor类,它是spring解析组件的入口,以及@ComponentScan注解,只有启用了该注解,spring才会扫描除主配置类以外的组件。

有多种定义(标记)组件的方式,如@Component、@Configuration、@Bean等,使用这些注解标注的类编译为字节码后,在运行时通过类加载器获取得,并且会被包装为bean定义加入ioc容器。

同时存在先后顺序:使用@Component及其派生注解的对象是首选,其次才是@Bean。

后续会通过这些bean定义信息来实例化组件(本文未涉及的部分)。

以上。

返回