DEV Community

Liu yu
Liu yu

Posted on • Edited on

一文学懂Spring的核心

GPT4o给我提了一个问题:ApplicationContext是如何创建Bean的?

Bean 是 Spring 中的 Object,也可以理解为 Object + Spring 的管理机制。

我写了一个用于查看 Spring 源码流程的测试代码,下面是完整的代码:

package com.origin;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;

@Component
class SimpleBean {

    public SimpleBean() {
        System.out.println("1. SimpleBean 构造函数被调用");
    }

    @PostConstruct
    public void init() {
        System.out.println("2. SimpleBean 初始化方法被调用");
    }

    @PreDestroy
    public void destroy() {
        System.out.println("3. SimpleBean 销毁方法被调用");
    }
}

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = 
            new AnnotationConfigApplicationContext("com.origin");

        // 反射构造
        SimpleBean bean = context.getBean(SimpleBean.class);

        context.close();
    }
}
Enter fullscreen mode Exit fullscreen mode

这里的注解 @Component 是告诉 Spring:“Spring 快看过来,我这个类 SimpleBean 是一个 Bean 哦!”

@Component
class SimpleBean {}
Enter fullscreen mode Exit fullscreen mode

我们在这个 Bean 里面实现了构造函数、初始化方法 init()、销毁方法 destroy(),来观察 Spring 在创建和销毁 Bean 时的顺序。


看一下 main 函数的三句话:

public class Main {
    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = 
            new AnnotationConfigApplicationContext("com.origin");

        // 反射构造
        SimpleBean bean = context.getBean(SimpleBean.class);

        context.close();
    }
}
Enter fullscreen mode Exit fullscreen mode

这三句话的意思分别是:

  1. 创建一个容器,名为 context(Spring 用来管理 Bean 的“工厂”叫做容器);
  2. 使用反射构造一个 SimpleBean 的对象(反射是 Java 里一种很抽象但有用的机制,简单来说就是“不走寻常路”来创建对象);
  3. 关闭刚才创建的容器。

运行结果:

1. SimpleBean 构造函数被调用  
2. SimpleBean 初始化方法被调用  
3. SimpleBean 销毁方法被调用
Enter fullscreen mode Exit fullscreen mode

ApplicationContext 创建 Bean 的过程(源码调试)

我通过设置断点,调试了一下午终于搞懂了这三句话的底层逻辑。下面详细解读每一步:

1. 初始化容器

AnnotationConfigApplicationContext context = 
    new AnnotationConfigApplicationContext("com.origin");
Enter fullscreen mode Exit fullscreen mode

这句话是初始化容器。Spring 容器通过扫描指定的包 com.origin(也就是我们用 @Component 标注的类所在包)来找出有哪些类应该注册成 Bean。

当然,也可以通过配置文件来搞,比如 XML 配置:

<bean id="SimpleBean" class="com.origin.SimpleBean"/>
Enter fullscreen mode Exit fullscreen mode

这和 @Component 的作用类似,都是声明 Bean,只不过形式不同。这个过程叫做 注册 Bean


2. 创建 Bean 实例

关键在这句话:

SimpleBean bean = context.getBean(SimpleBean.class);
Enter fullscreen mode Exit fullscreen mode

调用链如下:

AbstractApplicationContext.getBean()
├── getBeanFactory().getBean()
│   ├── doGetBean()
│   │   ├── 检查单例缓存
│   │   ├── 创建 Bean(如果不存在)
│   │   │   ├── createBean()
│   │   │   │   ├── 解析 Bean 类
│   │   │   │   ├── 前置处理
│   │   │   │   ├── doCreateBean()
│   │   │   │   │   ├── createBeanInstance() // 实例化
│   │   │   │   │   │   └── 使用反射调用构造函数
│   │   │   │   │   ├── populateBean()      // 属性填充
│   │   │   │   │   └── initializeBean()    // 初始化
│   │   │   │   │       ├── invokeAwareMethods()
│   │   │   │   │       ├── applyBeanPostProcessorsBeforeInitialization()
│   │   │   │   │       │   └── @PostConstruct
│   │   │   │   │       └── applyBeanPostProcessorsAfterInitialization()
│   │   │   │   └── 后置处理
│   │   │   └── 加入单例缓存
│   │   └── 返回 Bean 实例
│   └── 类型转换(如果需要)
└── 返回 Bean
Enter fullscreen mode Exit fullscreen mode

这个图其实可以不用死记硬背,简单来说就是:

  • getBean() 先检查缓存有没有这个 Bean;
  • 没有就调用 doGetBean() 创建 Bean;
  • 然后往下一直调用,最终使用 Java 反射来实例化这个类。

Java 反射创建实例的关键方法:

public T newInstance(Object... initargs) {
    Class<?> caller = override ? null : Reflection.getCallerClass();
    return newInstanceWithCaller(initargs, !override, caller);
}
Enter fullscreen mode Exit fullscreen mode

这里:

  • initargs:就是构造函数的参数,Spring 会根据需要填充;
  • Reflection.getCallerClass():拿到调用者信息,主要用于安全校验;
  • 最终调用 constructorToUse.newInstance(args) 来创建实例。

2.1 Spring 是如何选择构造器的?

Spring 使用 InstantiationStrategy(默认是 SimpleInstantiationStrategy)来选择构造器:

constructorToUse.setAccessible(true);
return (T) constructorToUse.newInstance(args);
Enter fullscreen mode Exit fullscreen mode

如果使用的是无参构造器:

constructorToUse.newInstance(); // 等价于 newInstance(new Object[0])
Enter fullscreen mode Exit fullscreen mode

如果有参数,那 Spring 会自动解析依赖,准备参数:

constructorToUse.newInstance(args);
Enter fullscreen mode Exit fullscreen mode

依赖注入

Spring 会解析 @Autowired 或构造函数的参数,决定是否注入其他依赖:

class A {
    @Autowired
    private B b;
}
Enter fullscreen mode Exit fullscreen mode

实例化 A 时,Spring 会先创建并注入 B。


调用 init() 方法

这个一般是做一些初始化或准备工作,Spring 会自动调用。


放入容器缓存

创建完后,Spring 会将其加入单例池中,便于后续复用。


Bean 的依赖注入顺序:两个例子

第一个例子:

测试代码如下:

@Component
class SimpleBean {
    @Autowired
    private DependentBean dependentBean;
    ...
}

@Component
class DependentBean {
    ...
}
Enter fullscreen mode Exit fullscreen mode

输出:

1. DependentBean 构造函数被调用  
2. DependentBean 初始化方法被调用  
1. SimpleBean 构造函数被调用  
2. SimpleBean 初始化方法被调用  
3. SimpleBean 销毁方法被调用  
3. DependentBean 销毁方法被调用
Enter fullscreen mode Exit fullscreen mode

结论:

Spring 会先创建依赖 Bean(DependentBean),再创建主 Bean(SimpleBean)。销毁顺序则相反。


第二个例子:

仅仅把类名替换成 A 和 B,结构不变:

@Component
class A {
    @Autowired
    private B b;
    ...
}

@Component
class B {
    ...
}
Enter fullscreen mode Exit fullscreen mode

输出变为:

1. A 构造函数被调用  
1. B 构造函数被调用  
2. B 初始化方法被调用  
2. A 初始化方法被调用  
3. A 销毁方法被调用  
3. B 销毁方法被调用
Enter fullscreen mode Exit fullscreen mode

这是为什么?!


真相:Bean 的注册顺序决定了谁先触发构造!

调用:

new AnnotationConfigApplicationContext("com.origin");
Enter fullscreen mode Exit fullscreen mode

Spring 会扫描 com.origin 包,并按 类名字典序 注册 Bean。

举例:

  • 如果注册顺序是 [A, B],那 A 会先被创建;
  • A 被创建时发现依赖 B,就顺手创建 B。

这就导致了输出顺序变了。


如何验证这个说法?

加入一个自定义的 BeanFactoryPostProcessor 打印注册顺序:

@Component
class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) {
        System.out.println("Spring 注册的 Bean 顺序:");
        for (String name : factory.getBeanDefinitionNames()) {
            System.out.println("  -> " + name);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

验证结果:

  • 第一个例子注册顺序为:
  -> dependentBean
  -> myBeanFactoryPostProcessor
  -> simpleBean
Enter fullscreen mode Exit fullscreen mode
  • 第二个例子注册顺序为:
  -> a
  -> b
  -> myBeanFactoryPostProcessor
Enter fullscreen mode Exit fullscreen mode

这验证了我们前面的结论。


新问题:为什么只是简单的类名替换,注册顺序就变了?

猜想:Spring 注册 Bean 的时候是按名字进行字典序排序的!

验证方法:写一堆不同名字的类,并且打乱顺序:

@Component class Beta {}
@Component class Charlie {}
@Component class Alpha {}
@Component class Delta {}
@Component class Echo {}
@Component class Edho {}
@Component class Foxtrot {}
Enter fullscreen mode Exit fullscreen mode

输出注册顺序:

-> alpha  
-> beta  
-> charlie  
-> delta  
-> echo  
-> edho  
-> foxtrot  
-> myBeanFactoryPostProcessor
Enter fullscreen mode Exit fullscreen mode

完全符合字典序!


Spring 源码中哪里体现了字典序?

调用链:

new AnnotationConfigApplicationContext("com.example");
Enter fullscreen mode Exit fullscreen mode

Spring 实际用的是 ClassPathScanningCandidateComponentProvider 来加载类。而加载类时:

  • 用到 Java 的 File.listFiles()JarFile.entries()
  • 这些方法返回的就是按文件系统顺序(通常近似字典序)
  • 所以注册顺序不是 Spring 显式排序,而是受底层文件系统影响。

最后一个点:为什么销毁顺序和构造顺序相反?

举个例子:

@Component
class ConnectionManager {
    public void close() {
        System.out.println("关闭数据库连接");
    }
}

@Component
class UserService {
    @Autowired
    ConnectionManager connectionManager;

    @PreDestroy
    public void shutdown() {
        System.out.println("UserService 做一些资源释放");
    }
}
Enter fullscreen mode Exit fullscreen mode

如果先销毁 ConnectionManagerUserService 的销毁逻辑可能就无法正常执行(比如记录日志)。所以 Spring 必须保证:

先销毁使用别人的那一方,再销毁被依赖的一方。

这就是“后进先出”的管理原则。


✅ Spring Bean 生命周期与注册顺序:完整总结

  1. 创建容器时,Spring 会扫描指定包下的类(如 "com.origin")并注册为 Bean。
  • 注册顺序通常是按类名的字典序,由 ClassPathScanningCandidateComponentProvider 处理;
  • 实际行为也受到类加载器、文件系统、构建工具等因素影响,不建议依赖它做关键逻辑
  1. Spring 创建 Bean 的完整流程包括:
  • 实例化(通过反射构造函数);
  • 属性注入(字段或构造注入);
  • 初始化(执行 @PostConstructInitializingBean);
  • 缓存单例(加入容器);
  • 销毁(容器关闭时执行 @PreDestroyDisposableBean)。
  1. 构造顺序受“注册顺序 + 依赖”双重影响:
  • 如果主 Bean 比依赖先注册,就会先构造主 Bean,再顺带创建它的依赖;
  • 如果依赖先注册,自然就会先构造依赖,再构造主 Bean;
  • 所以构造顺序≠依赖顺序,尤其在使用字段注入时。
  1. 销毁顺序严格反向于创建顺序:
  • Spring 采用“后进先出”策略(LIFO),确保先销毁使用别人的 Bean;
  • 这是为了解决依赖在销毁阶段可能还要被用到的问题(比如日志、连接池等)。
  1. 验证注册顺序的实用技巧:
  • 实现 BeanFactoryPostProcessor,在 postProcessBeanFactory() 中打印 beanDefinitionNames
  • 可以看到 Spring 实际注册的顺序,有助于分析构造时序。
  1. 推荐:尽量使用构造注入,避免依赖注册顺序引发的混淆。
  • 构造注入明确表达依赖关系,有利于测试和重构;
  • 字段注入虽然简单,但行为更依赖 Spring 的“内部处理机制”。

🔚 总结一句话:

Spring 创建 Bean 的顺序虽然看似自动,但实则受多种因素影响:注册顺序、依赖关系、注入方式。理解这些内部机制,有助于我们在调试、排查 Bean 初始化异常时做到心中有数。


Top comments (0)