在AI的帮助下理解spring的启动过程

spring的启动过程

面试的时候怎么说?

首先要明确的一点是spring的启动过程是围绕bean进行的。再然后是spring是个容器。所以首先我们要创建这个容器。也就是applicationContext,这是个接口。通常我们在启动类中会有相关的实现类,比如说

复制代码
new AnnotationConfigApplicationContext(AppConfig.class);
// 或
new ClassPathXmlApplicationContext("application.xml");

这里也把我们的配置文件给加载了进来。

现在容器有了,这时候就进入到源码中spring核心的refresh()方法。容器是用来放bean的。那就需要创建bean。怎么创建呢?spring是通过BeanFactory生产bean的。那就需要创建beanFactory。

factory有了后就需要加载bean了。但是要知道去哪找找bean。这时候就需要通过比如@ComponentScan等方式扫描指定文件,生成beanDefinition,也就是bean的公共定义,像类的名称、作用域、是否懒加载、依赖的bean等。加载完了后,就开始实例化bean、属性填充、初始化。bean都创建完了,spring也就启动完了。

这就是spring的启动流程。

整体流程启动完后,再补充下几点细节

各个阶段预留了那些接口

其实对于开发者来说,我们怎么能在spring启动过程中插入我们自己的操作呢?也就是spring有预留给开发者哪些接口呢?这里统计以下,基本上每个阶段都有接口供我们个性化操作。

bash 复制代码
| 接口                                            | 介入时机                         | 用途                                                                      |
| --------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------- |
| **`ApplicationContextInitializer`**           | 容器 `refresh()` 之前            | 在容器刷新前修改 `Environment` 或容器状态                                            |
| **`BeanFactoryPostProcessor`**                | BeanDefinition 加载后,Bean 实例化前 | 修改 BeanDefinition(如 `PropertySourcesPlaceholderConfigurer` 处理 `@Value`) |
| **`BeanPostProcessor`**                       | Bean 初始化前后                   | 干预 Bean 创建(AOP、@Autowired、@PostConstruct 都基于此)                          |
| **`InstantiationAwareBeanPostProcessor`**     | Bean 实例化前后                   | 甚至可以自定义返回代理实例替代正常实例化                                                    |
| **`Aware` 接口族**                               | 属性填充后,初始化前                   | 注入容器基础设施(`BeanFactoryAware`、`ApplicationContextAware`)                  |
| **`InitializingBean` / `@PostConstruct`**     | 属性填充后                        | 自定义初始化逻辑                                                                |
| **`DisposableBean` / `@PreDestroy`**          | 容器关闭时                        | 自定义销毁逻辑                                                                 |
| **`ApplicationListener`**                     | 事件发生时                        | 监听 `ContextRefreshedEvent` 等                                            |
| **`ApplicationRunner` / `CommandLineRunner`** | 容器完全就绪后                      | 执行启动后业务任务                                                               |

其中为我们经常最关注的是beanPostProcessor。这里能够进行AOP前面操作,还有常用的@PostConstruct 是在初始化的时候操作的,基本上实在bean创建完成时执行的了。

下面是bean实例化过程的步骤,记住先进行属性填充再初始化。
bash 复制代码
1. 从 singletonObjects 缓存中查找
   ↓ 未找到
2. 标记该 Bean 正在创建中(解决循环依赖的关键)
   ↓
3. 合并 BeanDefinition(处理父子 BeanDefinition)
   ↓
4. 检查依赖的 Bean,递归 getBean()
   ↓
5. 【createBean()】
   ├── 5.1 实例化(调用构造函数):`createBeanInstance()`
   ├── 5.2 属性填充:`populateBean()` → 执行 @Autowired 注入
   └── 5.3 初始化:`initializeBean()`
           ├── 执行 Aware 接口回调(BeanNameAware、ApplicationContextAware)
           ├── 执行 BeanPostProcessor.postProcessBeforeInitialization()
           ├── 执行 @PostConstruct / InitializingBean.afterPropertiesSet()
           └── 执行 BeanPostProcessor.postProcessAfterInitialization()
   ↓
6. 放入 singletonObjects 缓存
用到了哪些设计模式
  • 模板方法模式 :refresh() 定义了固定启动步骤,具体某些步骤由子类实现(如 onRefresh() 启动 Web 服务器)。就像开业流程固定,但"迎宾方式"可由各餐厅自己定。

  • 工厂模式 :BeanFactory 和 ApplicationContext 就是生产 Bean 的工厂

  • 单例模式:Spring 管理的 Bean 默认是单例

  • 观察者模式 :ApplicationEvent + ApplicationListener 启动过程有很多event类,都是监听器。

  • 代理模式:AOP 的核心。三级缓存里提前暴露的半成品,就是用动态代理包了一层,实现事务、日志等功能

三级缓存

三级缓存讲了很多遍了,但是总感觉差点意思。问了下kimi.回答的真好

主要点是如果只是解决循环依赖,二级缓存也可以。但是如果要用到aop代理就必须要用到三级缓存了。因为A完成实例化后的对象在一级缓存中是A的proxy_A,但是B引用的还是A的真实对象。导致AOP失效。像使用了@Async\@Transactional的都是需要aop的类。

怎么解决aop失效的问题呢?就是在三级缓存时,调用getEarlyBeanReference方法,aop发现A需要代理,就提前生成a_proxy,把代理后的类放到B中。总之,是通过判断按需处理的。

Spring 三级缓存与代理类 ------ 深度问答


Q1:三级缓存是哪三级?分别存了什么?

表格

缓存名称 级别 存储内容 代码位置
singletonObjects 一级缓存 成品 Bean(已实例化、已注入、已初始化) ConcurrentHashMap<String, Object>
earlySingletonObjects 二级缓存 早期暴露的 Bean(已实例化,但未完成属性注入和初始化) ConcurrentHashMap<String, Object>
singletonFactories 三级缓存 ObjectFactory 函数式接口(一个"工厂",调用后返回早期 Bean 引用) HashMap<String, ObjectFactory<?>>

核心设计思想:一级缓存存成品,二级缓存存半成品,三级缓存存"能生成半成品的工厂"。


Q2:三级缓存是在什么时候被使用的?(循环依赖场景)

假设 A 依赖 B,B 又依赖 A:

plain

复制代码
创建 A
  ├── 实例化 A(调用构造函数,此时 A 是个空壳,属性未注入)
  ├── 将 A 的 ObjectFactory 放入三级缓存(提前暴露)
  ├── 属性填充:发现需要 B → 开始创建 B
  │       ├── 实例化 B
  │       ├── 属性填充:发现需要 A → 从缓存找 A
  │       │       ├── 一级缓存:没有(A 还没初始化完)
  │       │       ├── 二级缓存:没有
  │       │       └── 三级缓存:有!调用 ObjectFactory.getObject() 拿到早期 A 引用
  │       │               └── 将早期 A 放入二级缓存,清空三级缓存
  │       ├── 继续 B 的初始化(此时 B 里的 A 是早期引用)
  │       └── B 创建完成,放入一级缓存
  ├── 回到 A,继续属性填充(B 已就绪)
  ├── A 初始化完成
  └── A 放入一级缓存

Q3:如果只是解决循环依赖,两级缓存(一级 + 二级)够吗?

够,但前提是:没有 AOP 代理。

如果没有代理,实例化后直接把原始对象放入二级缓存即可,B 注入的就是这个原始对象,等 A 初始化完成后再放入一级缓存。

但有了 AOP 代理后,两级缓存就出问题了。


Q4:代理类为什么会让两级缓存失效?问题出在哪?

核心矛盾 :AOP 代理是在 Bean 初始化之后 才生成的(BeanPostProcessor.postProcessAfterInitialization())。

场景:A 被 AOP 代理,B 循环依赖 A

plain

复制代码
时间线:
T1: A 实例化(原始对象 A_raw)
T2: A 属性填充 → 需要 B → 创建 B
T3: B 属性填充 → 需要 A → 从二级缓存拿 A_raw(此时 A 还没初始化)
T4: B 创建完成
T5: 回到 A,A 初始化 → AOP 介入 → 生成代理对象 A_proxy
T6: A 放入一级缓存(存的是 A_proxy)

问题:B 中注入的是 A_raw(原始对象),但一级缓存中最终是 A_proxy(代理对象)
结果:B 里的 A 不是代理对象,AOP 失效!

这就是"早期暴露原始对象 vs 后期生成代理对象"的冲突。


Q5:三级缓存是怎么解决"循环依赖 + AOP 代理"问题的?

三级缓存存的不是对象,而是 ObjectFactory(工厂):

java

复制代码
// 实例化后,放入三级缓存的代码(AbstractAutowireCapableBeanFactory)
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

getEarlyBeanReference() 是关键:

java

复制代码
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    // 遍历所有 SmartInstantiationAwareBeanPostProcessor
    // 其中就包括 AnnotationAwareAspectJAutoProxyCreator(AOP 处理器)
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
            // 如果该 Bean 需要被代理,这里就会提前生成代理对象!
            exposedObject = ((SmartInstantiationAwareBeanPostProcessor) bp)
                              .getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

流程变化:

plain

复制代码
T1: A 实例化(A_raw)
T2: 将 () -> getEarlyBeanReference("A", mbd, A_raw) 放入三级缓存
T3: A 属性填充 → 需要 B → 创建 B
T4: B 属性填充 → 需要 A
        └── 调用三级缓存的 ObjectFactory.getObject()
            └── 触发 getEarlyBeanReference()
                └── AOP 处理器发现 A 需要代理 → 提前生成 A_proxy
                └── 返回 A_proxy 给 B 注入
T5: B 创建完成
T6: 回到 A,继续初始化
        └── 初始化完成后,postProcessAfterInitialization() 再次检查 AOP
        └── 但 AOP 处理器发现 A 已经代理过了(earlyProxyReferences 中有记录)
        └── 直接返回原始对象(不做二次代理)
T7: A 放入一级缓存

结果:B 注入的是 A_proxy,一级缓存存的也是 A_proxy,完全一致!


Q6:代理类在三级缓存中的具体作用是什么?

表格

作用 说明
延迟生成代理 三级缓存存的是工厂,不是对象。只有在真正发生循环依赖、需要提前暴露时,才触发代理生成。如果没有循环依赖,代理仍按正常流程在初始化后生成。
保证单例唯一 通过 earlyProxyReferences 集合记录哪些 Bean 已经提前代理了,避免初始化后重复生成代理。
解耦实例化与代理 实例化阶段不直接生成代理(因为属性还没注入),但工厂可以在被调用时"按需"生成代理。
打破时间差 解决了"循环依赖需要早期引用"和"AOP 代理需要后置处理"之间的时间差矛盾。

Q7:为什么三级缓存的工厂只调用一次?

java

复制代码
// DefaultSingletonBeanRegistry.getSingleton()
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);      // 查一级
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        singletonObject = this.earlySingletonObjects.get(beanName);  // 查二级
        if (singletonObject == null && allowEarlyReference) {
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); // 查三级
            if (singletonFactory != null) {
                singletonObject = singletonFactory.getObject();        // 调用工厂
                this.earlySingletonObjects.put(beanName, singletonObject); // 放入二级
                this.singletonFactories.remove(beanName);              // 清空三级
            }
        }
    }
    return singletonObject;
}

关键动作:一旦调用工厂拿到对象,立即:

  1. 将结果放入 二级缓存

  2. 从 三级缓存移除

这保证了:

  • 同一个 Bean 的早期引用始终只有一个

  • 后续再需要该 Bean,直接从二级缓存拿,不会重复生成代理


Q8:如果 A 和 B 互相依赖,且都需要代理,会生成几个代理对象?

答案:各一个,且都是正确的代理对象。

plain

复制代码
创建 A_proxy
  ├── A 实例化
  ├── A 放入三级缓存(工厂)
  ├── A 属性填充 → 需要 B → 创建 B
  │       ├── B 实例化
  │       ├── B 放入三级缓存(工厂)
  │       ├── B 属性填充 → 需要 A
  │       │       └── 调用 A 的工厂 → 生成 A_proxy(如果 A 需要代理)
  │       │       └── B 注入 A_proxy
  │       ├── B 初始化 → 生成 B_proxy(如果 B 需要代理)
  │       └── B 完成,放入一级缓存(B_proxy)
  ├── A 属性填充(拿到 B_proxy)
  ├── A 初始化 → AOP 发现已提前代理,跳过
  └── A 完成,放入一级缓存(A_proxy)

两个代理都在各自需要提前暴露时(或初始化后)正确生成,且只生成一次。


Q9:Spring 为什么不允许构造器循环依赖?

因为三级缓存的介入时机是在"实例化之后"。

java

复制代码
// 循环依赖:A 的构造器需要 B,B 的构造器需要 A
创建 A
  ├── 调用 A 的构造函数 → 需要 B
  │       ├── 调用 B 的构造函数 → 需要 A
  │       │       └── A 还没实例化完,三级缓存还没有 A 的工厂!
  │       └── 死循环 / 报错

解决方式:

  • 用 @Lazy 延迟注入:@Lazy private B b;(注入的是代理占位符,真正使用时才创建)

  • 改用 Setter 注入或字段注入


Q10:一句话总结三级缓存与代理的关系

三级缓存的本质是"延迟代理"策略:通过 ObjectFactory 将代理对象的生成时机从"初始化后"推迟到"首次被循环依赖需要时",从而保证注入到其他 Bean 中的早期引用和最终成品是同一个代理对象。

springboot的启动

其实就是以spring为中心,前面加上对服务类型的判断,比如是否是web,然后加载不同环境的配置application。再者就是启动快结束时利用spring预留的钩子onRefresh()创建并启动内嵌的tomcat服务器,并把dispatcherServlet等关键的web bean注册到servlet容器中。

当前了标识springboot最关键的还是自动配置。

自动配置:其本质是额外的配置类。在加载bean的阶段,会到各个包的spring.factories文件中批量导入自动配置类。

springcloud的启动

springcloud是由一个个小的springboot组成的,把这些微服务放到一个框架中,大家共用一套配置。所以多了很多配置中心、注册中心的配置。启动时,会先有次序的加载环境配置。

bean的加载顺序

我们不用管,容器会根据bean之间的注入关系自动按顺序加载。

为啥要用构造器注入

构造器注入不解决训练依赖问题,会在启动时报错的。

"构造器注入是最佳实践,但如果出现循环依赖,你应该重新设计,而不是依赖 Spring 去修补。"

相关推荐
程序员Sunday1 分钟前
Flex 和 Grid 有什么区别?一维布局、二维布局怎么选?
前端·css·面试
Jlzn88884 分钟前
华南PACK线自动化率怎么统计才靠谱
前端·javascript·自动化
Rain的Java大神之路5 分钟前
Java 工程师做 AI,核心机会在「落地」而非「训练」
java·后端·ai编程
拖孩24 分钟前
AI 时代,个人开发者到底能不能靠一个小程序赚到钱
前端·后端·微信小程序
Hilaku25 分钟前
ERP选型时不问MCP,等于默认三年后再付一次集成费!
前端·javascript·程序员
程序人生88825 分钟前
几百张固定位置扫描件如何离线批量清?DocConverter Web 批量去水印链路
前端
deli00726 分钟前
一个汉字三种编码:UTF-8、GBK、UTF-16 逐字节对比实测
前端
庖丁解牛36 分钟前
第二课:把代码保存成网页 + 认识变量 (2)
前端
代码猎人36 分钟前
怎么开发一个Vue3项目
前端
梦里寻码36 分钟前
从 0 到 1 用 NestJS + LangChain 搭一套可售卖的 SaaS AI 客服:挂件一行代码接入,知识库秒变 SEO 帮助中心
前端·后端·github