FXJ Wiki

Back

再看 Spring Boot:自动配置不是魔法,是时序Blur image

一、引子:一个时序上说不通的注解#

Spring Boot 的自动配置,标准解释是「约定优于配置」:你不写配置,它给你一套合理的默认值;你写了,它就让开。

让开的机制是 @ConditionalOnMissingBean

@Bean
@ConditionalOnMissingBean          // 如果容器里没有 DataSource,我才创建
public DataSource dataSource() {
    return new HikariDataSource(...);
}
java

用起来毫无障碍。直到有人问我:

@ConditionalOnMissingBean 是在判断「容器里有没有这个 Bean」。

但如果自动配置先于我的 @Configuration 被处理,那判断的时候我的 Bean 还没注册,条件会认为「没有」,于是自动配置创建了自己的 DataSource——然后我的也创建了,容器里就有两个。

反过来,如果自动配置在我之后处理,那它怎么保证一定在之后?我可没写任何顺序声明。

我当时的回答是「Spring 内部会保证顺序」。这等于什么都没说。

而且这个问题还有一个更尖锐的版本:

就算顺序对了,@ConditionalOnMissingBean 判断的到底是「Bean 已经被创建出来了」还是「Bean 的定义已经登记了」?

如果是前者,那意味着判断时要先把我的 DataSource 实例化——可我的 DataSource 可能依赖别的还没准备好的东西。

这两个问题指向同一个我从没认真想过的东西:Spring 容器的启动不是一个连续的过程,它有明确分开的阶段,而条件注解的正确性完全依赖于「在哪个阶段执行」。

把这层想清楚之后,「魔法」这个词就消失了。剩下的只是一套相当直白的时序安排。

这篇文章拆三句话:

说法问题出在哪
自动配置是「约定优于配置」的魔法它是条件化的定义注册,靠时序保证正确
IoC 就是把对象交给容器管理容器有两个截然不同的阶段,混淆它们就理解不了循环依赖
starter 就是把依赖打个包它是一套 SPI 机制,且刚刚经历过一次不兼容迁移

二、自动配置:时序决定一切#

2.1 先把 @SpringBootApplication 拆开#

这个注解是三个注解的组合:

@SpringBootConfiguration      // 本质就是 @Configuration
@EnableAutoConfiguration      // 开启自动配置
@ComponentScan                // 扫描当前包及子包
public @interface SpringBootApplication { ... }
java

真正做事的是 @EnableAutoConfiguration,而它自己也只做一件事:

@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration { ... }
java

@Import 一个 ImportSelector,意思是「把这个类返回的那些类名,当作配置类导入进来」。

到这里为止都很直白。关键在于 AutoConfigurationImportSelector 实现的不是 ImportSelector,而是 DeferredImportSelector

这个 Deferred 就是引子里那个问题的全部答案。

2.2 DeferredImportSelector:延迟到最后#

Spring 在解析配置类时,用的是 ConfigurationClassParser。它的处理流程大致是:

1. 解析主配置类(你的 Application 类)
2. 处理它的 @ComponentScan → 扫出你写的所有 @Component / @Configuration
3. 递归解析这些配置类,处理它们的 @Bean、@Import ...
4. ─────── 以上全部完成之后 ───────
5. 处理所有 DeferredImportSelector → 这才轮到自动配置
text

第 4 步那条线是整个机制的核心。普通的 ImportSelector 在遇到时立即处理,DeferredImportSelector 被攒起来,等所有常规配置类都解析完了才统一处理。

于是顺序被强制成了:

你写的配置永远先被处理,自动配置永远最后。

这不是巧合,也不是「Spring 内部会保证」的模糊说法,而是 ConfigurationClassParser.parse() 方法里明确的两段式结构:先 processConfigurationClass() 循环,再 this.deferredImportSelectorHandler.process()

引子里的第一个问题就此解决。

2.3 两个阶段:定义 vs 实例#

引子里的第二个问题更微妙:条件判断的是「Bean 存在」还是「Bean 定义存在」?

要回答它,必须先把 Spring 容器启动的两个阶段分清楚。这是理解 Spring 最重要的一件事,可惜大部分教程把它们混在「IoC 容器启动」一句话里。

这个阶段不创建任何业务对象,只是在容器里登记「有哪些 Bean,它们叫什么名字,是什么类型,怎么创建,依赖谁」。

登记的产物是 BeanDefinition——一个描述对象的元数据结构,可以理解成「造物说明书」。

// BeanDefinition 里存的东西(简化)
class BeanDefinition {
    String beanClassName;       // 类名(字符串,不是 Class)
    String scope;               // singleton / prototype
    boolean lazyInit;
    String[] dependsOn;
    ConstructorArgumentValues constructorArgs;
    MutablePropertyValues propertyValues;
    String factoryBeanName;     // @Bean 方法所在的配置类
    String factoryMethodName;   // @Bean 方法名
    // ...
}
java

执行者是 ConfigurationClassPostProcessor,它是一个 BeanDefinitionRegistryPostProcessor。上一节讲的配置类解析、@Import 处理、自动配置导入,全部发生在这个阶段

现在可以回答第二个问题了:

两个阶段分开的意义远不止于此:

  • AOP 能工作,是因为代理的织入发生在阶段二,而阶段一登记的只是原始类的定义;
  • @Value@ConfigurationProperties 能读到配置,是因为 BeanFactoryPostProcessor(如 PropertySourcesPlaceholderConfigurer)在两个阶段之间修改了定义;
  • 循环依赖能被解决(下一节),完全依赖于阶段二里的一个特殊设计。

分不清这两个阶段,Spring 的大部分行为都会显得像魔法。

2.4 条件注解为什么不会因为类不存在而崩溃#

还有一个细节值得单独说,因为它反直觉。

@ConditionalOnClass 是这样用的:

@Configuration
@ConditionalOnClass(RedisOperations.class)     // 类路径里有 Redis 才生效
public class RedisAutoConfiguration { ... }
java

问题是:如果项目里根本没引入 RedisRedisOperations 这个类不存在,那 JVM 在读取这个注解的时候不就 NoClassDefFoundError 了吗?注解的值是 Class<?> 类型,读它必然要加载类。

答案是:Spring 根本没用反射读这个注解。

它用的是 ASM 字节码解析(SimpleMetadataReaderFactory),直接从 .class 文件里把注解的元数据当字符串读出来:

反射读取:  getAnnotation(ConditionalOnClass.class).value()
            → 返回 Class<?>[],必须加载 RedisOperations → 类不存在就崩

ASM 读取:  直接解析 class 文件的常量池
            → 拿到字符串 "org.springframework.data.redis.core.RedisOperations"
            → 不加载任何类
text

拿到字符串之后,用 ClassUtils.isPresent(className, classLoader) 判断,内部是 Class.forName 包在 try-catch 里。加载失败就返回 false,条件不成立,这个自动配置类从头到尾都不会被加载

2.5 顺便说说启动慢#

spring-boot-autoconfigure 里有 100 多个自动配置类。启动时每一个都要走一遍条件判断,其中 @ConditionalOnClass 要做类加载尝试。这是 Spring Boot 启动耗时的主要来源之一。

Spring Boot 为此做了几层优化,可以当作性能设计的例子看:

第一层,spring-autoconfigure-metadata.properties 构建时把每个自动配置类的 @ConditionalOnClass 条件预先提取到一个 properties 文件里。启动时先读这个文件做一遍批量粗筛,直接排除掉大部分不可能生效的配置类,避免逐个加载它们的字节码。

第二层,条件判断的分组与短路。 OnClassCondition 会把候选列表对半拆分给两个线程并行判断(这是 Spring Boot 2.x 引入的),因为类加载尝试是这里最慢的一步。

第三层,最彻底的那个:AOT。 Spring Boot 3 的 AOT 处理把「条件判断」这件事整个挪到了构建期。构建时就确定哪些自动配置生效,直接生成对应的 Bean 注册代码,运行时不再有任何条件判断。这也是 GraalVM 原生镜像能工作的前提——原生镜像里没有动态类加载,条件判断根本没法在运行时做。

2.6 小结#

「自动配置是魔法」这句话里,被隐去的是一套很朴素的时序安排:

  1. DeferredImportSelector 保证自动配置在用户配置之后处理;
  2. 条件判断发生在 BeanDefinition 注册阶段,查的是定义不是实例;
  3. ASM 读取注解元数据,让引用不存在的类也不会崩
  4. @AutoConfigureBefore/After 解决自动配置彼此之间的顺序。

四条合起来,@ConditionalOnMissingBean 的正确性就是可推导的,不需要当成约定去记。


三、循环依赖:三级缓存的第三级到底为什么存在#

3.1 标准答案,以及它没回答的部分#

循环依赖是 Spring 面试题里的经典。标准答案是「三级缓存」:

// DefaultSingletonBeanRegistry 里的三个 Map
Map<String, Object> singletonObjects;              // 一级:完整的单例
Map<String, Object> earlySingletonObjects;         // 二级:早期暴露的半成品
Map<String, ObjectFactory<?>> singletonFactories;  // 三级:生产半成品的工厂
java

流程也能背:A 依赖 B,B 依赖 A。创建 A 时先把 A 的 ObjectFactory 放进三级缓存,然后去创建 B;B 需要 A,从三级缓存拿到工厂、调用它得到半成品 A、升级到二级缓存;B 创建完成,A 继续注入,完成。

这个描述是对的。但它回答不了一个问题:

两级缓存不就够了吗?

创建 A 之后直接把半成品 A 放进二级缓存,B 来了直接取。为什么要多一层工厂?

我以前的答案是「为了延迟创建」。但这里没有什么可延迟的——半成品 A 的实例已经通过构造器创建出来了,放进 Map 和放进工厂的成本一模一样。

真正的答案和 AOP 有关。

3.2 如果 A 需要被代理#

假设 A 上有 @Transactional,那么容器最终要交付的不是 A 本身,而是A 的代理对象

代理是在哪里生成的?在 initializeBean() 里,由 AbstractAutoProxyCreator 这个 BeanPostProcessor初始化之后完成。也就是说,正常流程下代理对象在 Bean 生命周期的末尾才出现。

现在把循环依赖叠加上去:

创建 A → 构造出原始的 A(还没到 initializeBean,代理还不存在)
       → 注入依赖,发现需要 B
       → 创建 B
           → B 需要 A,此时只能拿到「原始的 A」
           → B 注入了原始 A,B 创建完成
       → A 继续走完 initializeBean → 生成代理 A'
       → 容器里注册的是 A'
text

问题出现了:容器里的 A 是代理 A’,但 B 持有的是原始 A。

B 调用 A 的方法时不会走事务增强。这是一个静默的错误——不报异常,只是事务莫名其妙没生效。

所以需要一个机制:当 A 被提前暴露给别人时,如果 A 需要代理,那就提前把代理造出来给它。

3.3 第三级缓存的真正职责#

三级缓存里放的 ObjectFactory,它的实现是这样的:

// AbstractAutowireCapableBeanFactory.doCreateBean 里
addSingletonFactory(beanName, () -> getEarlyBeanReference(mbd, beanName, bean));

// getEarlyBeanReference 会遍历 SmartInstantiationAwareBeanPostProcessor
// AbstractAutoProxyCreator 实现了它:如果这个 Bean 需要代理,现在就生成代理返回
java

关键在于:这个工厂被调用时,才决定要不要生成代理。

于是:

  • 没有循环依赖:工厂从头到尾没被调用过,代理照常在 initializeBean 阶段生成,一切按正常流程;
  • 有循环依赖:B 来取 A,工厂被调用,此时提前生成代理 A’ 并放进二级缓存。A 后续走到 initializeBean 时,AbstractAutoProxyCreator 发现这个 Bean 已经提前代理过了(内部有个 earlyProxyReferences 记录),就跳过重复代理,直接返回之前那个 A’。

最终 B 持有的 A’ 和容器里的 A’ 是同一个对象。

现在可以回答「为什么不能两级」了:

3.4 Spring 解决不了的循环依赖#

三级缓存不是万能的。有几种情况它无能为力,而这几种情况的边界恰好能反过来验证上面的理解。

类型能否解决原因
字段注入 / setter 注入的单例对象已构造,可以提前暴露引用
构造器注入不能对象还没构造出来,没有东西可以暴露
prototype 作用域不能不进单例缓存,每次都要新建,必然无限递归
@Async 标注的 Bean通常不能它的代理由 AsyncAnnotationBeanPostProcessor 在初始化后创建,不参与提前代理

构造器注入那一行是最能说明问题的:三级缓存的前提是「对象实例已经存在,只是还没填充完」。构造器注入意味着 A 的构造函数需要 B,B 的构造函数需要 A——两个对象谁都没法先被 new 出来,缓存里根本没有可放的东西。

这也是为什么 Spring 官方推荐构造器注入:它让循环依赖在启动时就暴露成错误,而不是被悄悄化解掉。

3.5 顺便:Bean 生命周期的完整顺序#

既然讲到实例化阶段,把完整顺序列一遍。这个顺序在排查「为什么我的初始化逻辑没执行 / 执行早了」时非常有用:

1.  实例化           createBeanInstance()      调用构造器
2.  属性填充         populateBean()            @Autowired / @Value 注入
3.  Aware 回调       BeanNameAware → BeanClassLoaderAware → BeanFactoryAware
4.  前置处理         BeanPostProcessor.postProcessBeforeInitialization()
                     └─ @PostConstruct 在这里执行(由 CommonAnnotationBeanPostProcessor 触发)
5.  初始化           InitializingBean.afterPropertiesSet()
                     然后 @Bean(initMethod = "...")
6.  后置处理         BeanPostProcessor.postProcessAfterInitialization()
                     └─ AOP 代理在这里生成
7.  ── 使用中 ──
8.  销毁前           @PreDestroy
9.  销毁             DisposableBean.destroy()
                     然后 @Bean(destroyMethod = "...")
text

有两点值得注意:

@PostConstruct 在第 4 步,早于 AOP 代理生成(第 6 步)。 所以在 @PostConstruct 里调用自己的 @Transactional 方法,事务不会生效——那时代理还不存在,this 就是原始对象。

ApplicationRunner / CommandLineRunner 在所有 Bean 都完成之后才执行。 需要「容器完全就绪后做一件事」,用它们,而不是往某个 Bean 的 @PostConstruct 里塞。


四、starter:一次正在进行中的迁移#

4.1 starter 里其实没有代码#

spring-boot-starter-web 这个依赖,很多人以为它包含了 Web 相关的实现。打开看一眼:

<!-- spring-boot-starter-web 的完整内容,几乎就是这些 -->
<dependencies>
    <dependency>spring-boot-starter</dependency>
    <dependency>spring-boot-starter-json</dependency>
    <dependency>spring-boot-starter-tomcat</dependency>
    <dependency>spring-web</dependency>
    <dependency>spring-webmvc</dependency>
</dependencies>
xml

没有一行 Java 代码。 它就是一个 POM,作用是把一组版本兼容的依赖凑在一起。

真正的自动配置代码在 spring-boot-autoconfigure 这个 jar 里,而它是 spring-boot-starter 的传递依赖——也就是说,只要你用了任何一个 starter,那 100 多个自动配置类就全都在类路径上了

所以 starter 的职责其实是两件事,而且都不是「提供功能」:

  1. 版本管理:保证凑在一起的依赖版本互相兼容;
  2. 触发条件:把某些类带进类路径,从而让对应的 @ConditionalOnClass 成立。

第 2 点是理解 starter 的钥匙。加入 spring-boot-starter-data-redis,它带来了 RedisOperations 这个类,于是 RedisAutoConfiguration 上的 @ConditionalOnClass(RedisOperations.class) 成立,Redis 的自动配置就生效了。

4.2 自动配置是怎么被发现的#

自动配置类分散在各个 jar 里,Spring 怎么知道有哪些?

这需要一套 SPI(Service Provider Interface)机制:约定一个位置,让每个 jar 把自己提供的东西登记进去,框架启动时统一扫描。

Spring Boot 用过两代方案,而这次迁移是理解这套机制的好例子。

Spring Boot 1.0 到 2.7 使用 META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
properties

格式是「接口全限定名 = 实现类列表」,一个文件可以登记多种扩展点,不止自动配置。

它的问题:

  • 一个文件混装多种类型EnableAutoConfigurationApplicationListenerEnvironmentPostProcessor 全挤在一起,解析时要全部读进来再按 key 过滤;
  • 行尾反斜杠续行,几百个类名连成一行,改动时 diff 极其难看,合并冲突频繁;
  • properties 格式对类名里的特殊字符不友好,内部类的 $ 需要注意转义;
  • 无法被构建工具有效校验,写错一个类名要到运行时才发现。

Spring Boot 3.0 完全移除了对 spring.factoriesEnableAutoConfiguration 键的支持。 这意味着任何自定义 starter 如果只有旧文件,在 Boot 3 下会静默失效——不报错,自动配置就是不生效。

4.3 写一个 starter 需要哪些部分#

把前面所有东西串起来,一个自定义 starter 的完整结构:

my-spring-boot-starter/
├── pom.xml                                  ← 只管依赖,不放代码
└── my-spring-boot-autoconfigure/            ← 实际的自动配置
    ├── MyAutoConfiguration.java
    ├── MyProperties.java
    └── src/main/resources/META-INF/
        ├── spring/
        │   └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
        └── spring-configuration-metadata.json    ← IDE 配置提示(可选)
text

自动配置类本身:

@AutoConfiguration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean          // 让用户能覆盖
    public MyService myService(MyProperties props) {
        return new MyService(props.getEndpoint(), props.getTimeout());
    }
}
java

四个要点,每一个都对应前面讲过的机制:

  1. @AutoConfiguration 替代旧的 @Configuration,它内置了 proxyBeanMethods = false(少一层 CGLIB 代理,启动更快)和排序支持;
  2. @ConditionalOnClass 让这个配置只在相关依赖存在时生效——这就是 starter 「开关」职责的实现;
  3. @ConditionalOnMissingBean 让用户的定义优先,靠的是第二节讲的 DeferredImportSelector 时序;
  4. @EnableConfigurationPropertiesapplication.yml 里的配置绑定成对象。

4.4 为什么这套机制值得单独理解#

自动配置 + SPI 这套组合,解决的是一个很普遍的问题:

框架怎么在「不知道用户会用什么」的前提下,提供开箱即用的默认行为,同时不妨碍用户自定义?

Spring Boot 的答案有三个要素,每一个都可以单独迁移到别的地方:

要素作用类比
SPI 登记文件让框架发现分散在各处的扩展Java 的 ServiceLoader、Node 的 package.json exports
条件化生效根据环境决定哪些扩展启用编译期的条件编译、特性开关
用户优先的时序保证自定义能覆盖默认CSS 的层叠、配置文件的优先级链

三个要素缺一不可:只有 SPI 没有条件,就得手动开关每个扩展;有条件但时序错了,用户就覆盖不掉默认值。

「约定优于配置」是这套机制产生的效果,不是它的实现方式。 把效果当成实现方式来记忆,就只能停留在「魔法」的层面。


五、方法论:怎么读一个框架#

5.1 先找「什么时候执行」,再找「执行了什么」#

这次拆解最大的收获是这条。

引子里那个问题之所以卡住我,是因为我一直在看 @ConditionalOnMissingBean 做了什么(查容器里有没有 Bean),而问题的答案在于它什么时候做(在所有用户配置解析完之后、任何实例化开始之前)。

框架代码和业务代码最大的区别就在这里:业务代码的执行顺序基本就是你写的顺序,而框架代码的执行顺序是框架决定的,通常和你写的位置毫无关系。

所以读框架时,先建立时间轴:

Spring Boot 启动的关键时刻:
  1. SpringApplication.run()
  2. 准备 Environment(读配置文件、命令行参数)
  3. 创建 ApplicationContext
  4. ─ BeanDefinitionRegistryPostProcessor ─ 配置类解析、自动配置导入
  5. ─ BeanFactoryPostProcessor ─ 修改 BeanDefinition(如占位符替换)
  6. 注册 BeanPostProcessor
  7. ─ finishBeanFactoryInitialization ─ 实例化所有非懒加载单例
  8. ApplicationRunner / CommandLineRunner
text

有了这条轴,很多问题变成了查表:

  • 「为什么我在 @PostConstruct 里拿不到某个 Bean」→ 第 7 步中间,依赖的 Bean 可能还没造;
  • 「为什么我改了 BeanDefinition 没生效」→ 你的代码在第 7 步之后跑,太晚了;
  • 「为什么 @Value 没被解析」→ 占位符替换在第 5 步,你的对象不是容器管理的。

5.2 区分「机制」和「策略」#

框架里的东西可以分成两类,混淆它们会导致学错重点:

机制是骨架,很少变化,理解它一劳永逸:

  • BeanDefinition 与实例的两阶段划分;
  • BeanPostProcessor 的扩展点模型;
  • DeferredImportSelector 的延迟处理。

策略是具体决策,会随版本变化:

  • 自动配置文件从 spring.factories 换成 AutoConfiguration.imports
  • 循环依赖从默认允许改成默认禁止;
  • 从运行时条件判断走向 AOT 构建期判断。

学机制,查策略。 机制理解了,策略变化只是查一下文档的事。反过来,只记住了策略(比如背下了 spring.factories 的写法),升个大版本就全废了。

判断标准很简单:这个东西如果变了,会不会影响我对系统的整体理解? 会,那是机制;不会,只是要改几行配置,那是策略。

5.3 用「如果没有它会怎样」来验证理解#

三级缓存那节用的就是这个方法。「为什么需要第三级」这个问题,等价于「如果只有两级会怎样」——推下去发现代理对象会不一致,第三级的必要性就出来了。

这个方法对框架里那些「看起来多余」的设计特别有效:

设计如果没有它
第三级缓存循环依赖 + AOP 时,注入的是未代理对象
DeferredImportSelector自动配置可能先于用户配置执行,@ConditionalOnMissingBean 失效
ASM 读注解元数据类路径缺少依赖时,读注解直接 NoClassDefFoundError
spring-autoconfigure-metadata启动时要逐个加载 100 多个类做条件判断

如果回答不上「没有它会怎样」,说明还没理解它解决的问题——这时候记住的只是它的存在,不是它的作用。

5.4 读源码从扩展点入手#

Spring 的源码量很大,从 SpringApplication.run() 一行行读下去会迷失。

更有效的入口是扩展点,因为扩展点是框架主动暴露给你的,它们的位置和时机都有明确定义:

扩展点时机典型用途
ApplicationContextInitializer上下文创建后、刷新前注册额外的属性源
BeanDefinitionRegistryPostProcessor定义注册阶段动态注册 Bean(MyBatis 的 Mapper 扫描)
BeanFactoryPostProcessor定义注册完成后批量修改 BeanDefinition
BeanPostProcessor每个 Bean 初始化前后AOP、@Autowired 注入
ApplicationListener各个生命周期事件启动完成后的初始化

顺着扩展点读,能同时看到「框架在哪个位置留了口子」和「框架自己怎么用这个口子」。 Spring 的很多核心功能(AOP、注解注入、配置绑定)都是用自己的扩展点实现的,读它们比读框架内部逻辑更有收获。

5.5 最后#

Spring Boot 被称为「魔法」,是因为它做了大量你没写的事。但拆开之后,这些事情的机制其实相当朴素:

  • 自动配置 = 条件化的 BeanDefinition 注册 + 严格的时序保证
  • 循环依赖 = 在实例化阶段提前暴露引用,并用一个 lazy 决策点兼容 AOP
  • starter = 一组依赖 + 一个 SPI 登记文件

没有一处需要「记住它就是这样」。每一处都能从「它要解决什么问题」推出来。

回到引子那个问题:@ConditionalOnMissingBean 为什么能算准?

因为它压根不需要「算」。时序已经保证了,轮到它执行的时候,用户的所有定义都已经登记完毕了。 它只是查一下表而已。


参考#

  • Spring Framework Reference, The IoC Container — 尤其是 BeanFactory 生命周期与 BeanPostProcessor 部分.
  • Spring Boot Reference, Creating Your Own Auto-configurationCondition Annotations.
  • Spring Boot 2.7 Release Notes, New AutoConfiguration.imports file.
  • Spring Boot 3.0 Migration Guide, Auto-configuration registrationAOT processing.
  • Johnson, Rod. Expert One-on-One J2EE Design and Development. Wrox, 2002.(IoC 思想的最初论述)
  • Fowler, Martin. Inversion of Control Containers and the Dependency Injection Pattern. 2004.

本站相关:

再看 Spring Boot:自动配置不是魔法,是时序
https://fxj.wiki/blog/rethinking-spring-boot
Author 玛卡巴卡
Published at 2026年8月12日
Comment seems to stuck. Try to refresh?✨