Spring IoC 详解
Spring IoC 详解
本文基于某企业级薪资系统的真实代码整理(已脱敏,包名、类名、应用名均做泛化处理),所有示例保留了原始设计意图,可直接映射到日常开发中的同类场景。
目录
- 从没有 Spring 的世界说起
- Bean 与容器的本质
- 真实案例逐行解读:SequenceConfig
- 关联文件:一条完整的 IOC 链路
- @Configuration 是 AOP 切面吗?
- 单例住 Map,多例不进仓
- @Autowired 与单例是两个正交的概念
- 多环境下的 Bean
- 总结
一、从没有 Spring 的世界说起
假设你要用发号器生成一个 ID,最朴素的写法是:
public class OrderService {
public long createOrder() {
// 1. 自己 new 依赖
DbSequenceDao dao = new DbSequenceDao();
dao.setAppName("MY_APP");
dao.setDbGroupKeys(Arrays.asList("MY_DB_GROUP"));
dao.setRetryTimes(3);
dao.init(); // 2. 自己记得初始化
SequenceGenerator generator = new SequenceGenerator();
generator.setSequenceDao(dao); // 3. 自己组装依赖关系
return generator.getIdNext(...); // 4. 才能干活
}
}
问题:
- 每个用到发号器的地方都要重复这段"创建 + 配置 + 组装"代码;
dao.init()会连数据库,每个调用方都 new 一次,数据库连接就爆了;- 想换个实现(比如测试换 H2),所有 new 过的地方全得改。
"控制反转"反转的就是这件事:对象的创建权、配置权、组装权,从业务代码手里移交给了容器。
二、Bean 与容器的本质
2.1 Bean 就是一个普通 Java 对象
new DbSequenceDao() 得到的那个对象,本身没有任何神奇之处。它和普通对象的唯一区别是:这个对象不是你 new 的,是别人帮你 new 好、配置好、放在那里,你用的时候去领。
"Bean"这个词没有任何深意,就是 Spring 给"由我管理的对象"起的名字。就像仓库里的货物贴上标签叫"库存",货物本身没变,变的只是它归仓库管了。
2.2 容器 = 对象仓库 + 管理员
容器在物理上就是应用启动时创建的一个对象,内部最核心的东西是一张 Map:
Map<String, Object> 仓库 = new ConcurrentHashMap<>();
// "sequenceDao" → 那个配好的 DbSequenceDao 实例
// "sequenceGenerator" → 那个已注入 dao 的 SequenceGenerator 实例
// "orderService" → ...
// 应用里几百个对象,都在这张表里
管理员(容器的逻辑)在应用启动时干四件事:
- 盘点清单:扫描所有
@Component/@Service/@Repository/@Configuration,读所有@Bean方法,得到一张"需要生产哪些对象"的清单(BeanDefinition); - 按单生产:逐个 new 出来,填好配置;
- 组装关系:看到谁有
@Autowired,就从仓库里找出对应对象塞给它; - 入库:全部放进那张 Map,然后对外营业。
之后业务代码要用对象,再也不 new,而是从仓库领:
@Repository
public class SequenceGenerator {
@Autowired
private SequenceDao sequenceDao; // 启动时容器自动把仓库里的货塞进这个字段
}
2.3 Bean 的两层结构
一个 Bean 在容器里其实是两份东西:
- BeanDefinition(配方):记录"这个 Bean 叫什么、什么类、scope 是什么、initMethod 是什么、依赖谁",全部存在
beanDefinitionMap里; - Bean 实例(成品):按配方生产出来的对象。
2.4 餐厅类比
| 类比 | 对应概念 |
|---|---|
| 餐厅的中央厨房 + 冷库 | 容器 |
| 冷库里一份份做好的预制菜 | Bean |
| 菜谱(用什么料、怎么做、几份) | BeanDefinition |
| 服务员要菜时不自己做,直接去冷库拿 | @Autowired 注入 |
| "今天不做辣菜"这类当日规则 | @Profile / @Conditional(决定哪些菜进冷库) |
没有 Spring = 每个服务员接待客人前自己买菜、洗菜、炒菜;
有 Spring = 开业前厨房按菜谱统一做好入库,服务员随取随用。
三、真实案例逐行解读:SequenceConfig
业务背景:分库分表环境下不能用数据库自增主键,需要全局序列生成器。SequenceDao 是"序列号发号器的数据源"(来自分库分表中间件),这个配置类负责把它构造成 Spring Bean。
@Configuration // ★IOC 核心①:标记为"Java 配置类"
作用:告诉 Spring 容器这是一个 Bean 定义的来源。容器启动时会用 CGLIB 为该类生成子类代理(full 模式),保证类内多个
@Bean方法互相调用时拿到的仍是同一个单例。它本身也会被注册为一个 Bean。
public class SequenceConfig {
@Bean(initMethod = "init") // ★IOC 核心②:声明一个 Bean + 生命周期回调
@Bean:方法返回值对象将被注册进容器,Bean 名默认 = 方法名sequenceDao。这就是"控制反转"的第一层:对象的创建权从业务代码转移到了容器配置。initMethod = "init":容器完成该 Bean 的实例化和属性填充后,自动回调DbSequenceDao.init()(中间件用它连接数据库、加载序列表)。这是InitializingBean生命周期接口的注解式替代写法。
@ConditionalOnMissingBean // ★IOC 核心③:条件装配
作用:仅当容器中不存在
SequenceDao类型的 Bean 时才注册本 Bean。这是"默认实现 + 允许覆盖"的设计:生产环境用这份配置,测试环境可以塞一个自己的SequenceDao让它自动让位。
public SequenceDao sequenceDao() { // ★IOC 核心④:面向接口声明
返回类型是接口
SequenceDao而非实现类。容器里 Bean 的类型登记为接口类型,消费方按接口注入——将来换实现不需要改任何消费方代码。
DbSequenceDao sequenceDao = new DbSequenceDao(); // 手工 new 实现类并做参数化配置
sequenceDao.setAppName("MY_APP"); // 中间件应用名,用于定位序列元数据
sequenceDao.setAdjust(true); // 序列值异常时自动校正
sequenceDao.setDbGroupKeys(Arrays.asList("MY_DB_GROUP")); // 指定序列存储的数据库分组
sequenceDao.setRetryTimes(3); // 取号失败重试 3 次
if (EnvironmentUtils.isAcceptance()) { // 判断是否 UAT 验收环境(读 CURR_ENV 环境变量)
sequenceDao.setAppName("UAT_MY_APP"); // UAT 环境切换到独立应用名
sequenceDao.setDbGroupKeys(Arrays.asList("UAT_MY_DB_GROUP")); // 及独立库分组
}
return sequenceDao; // 返回对象,容器接管其后续生命周期
}
}
IOC 在这个例子中的四个维度
| IOC 维度 | 在本例中的体现 |
|---|---|
| 控制反转(创建权) | DbSequenceDao 不由使用方 new,而是配置类统一构建、配参,容器持有单例 |
| 依赖注入(DI) | 消费方 SequenceGenerator 通过 @Autowired 按类型拿到它,双方零耦合 |
| 生命周期托管 | initMethod="init" 由容器在合适时机回调,业务代码不关心初始化时序 |
| 可替换性 | 声明为接口 + @ConditionalOnMissingBean,测试环境可整体替换实现 |
四、关联文件:一条完整的 IOC 链路
这个例子实际涉及 4 个文件,构成:声明 Bean(SequenceConfig)→ 扫描注册(Application)→ 注入消费(SequenceGenerator)→ 条件替换(TestDataSourceConfig)。
4.1 消费方:SequenceGenerator(同包)
@Repository // 立体注解:被组件扫描注册为 Bean(IOC 的另一种入口,对比 @Bean 的"手工声明")
public class SequenceGenerator {
@Autowired
private SequenceDao sequenceDao; // ★字段注入:容器按【类型】找到 SequenceConfig 产出的 Bean 注入
...
}
- 它是全应用发 ID 的组件:
getIdNext(...)生成 16~18 位业务主键(日期前缀 + 序列值 + 分表后缀); - 内部把注入进来的 DAO 交给中间件的序列对象使用——它完全不知道也不关心底层是
DbSequenceDao还是别的实现,这就是面向接口注入的价值; - 学习点:
@Repository(扫描式注册)与@Bean(配置类式注册)是 IOC 容器的两条 Bean 来源路径,本例恰好各占一条。
4.2 条件替换的实证:TestDataSourceConfig(test 目录)
@TestConfiguration
public class TestDataSourceConfig {
@Bean
public SequenceDao sequenceDao(DataSource dataSource) { // 测试环境提供另一个 SequenceDao
DefaultSequenceDao sequenceDao = new DefaultSequenceDao(); // 实现类换成了基于本地 H2 的简单实现
sequenceDao.setDataSource(dataSource); // 绑定内嵌 H2 数据源
sequenceDao.setStep(1000);
return sequenceDao;
}
- 这正是
@ConditionalOnMissingBean存在的意义:单测加载此配置后,容器里先有了测试版SequenceDao,生产版SequenceConfig.sequenceDao()自动放弃注册,SequenceGenerator无感知地切换到 H2 实现; - 附带学习点:
sequenceDao(DataSource dataSource)演示了@Bean方法参数注入——容器先把DataSourceBean 找来作为参数传入,是构造期依赖注入的写法。
4.3 环境工具:EnvironmentUtils(公共工具包)
public static boolean isAcceptance() { // 被 SequenceConfig 调用
String env = Optional.ofNullable(System.getenv("CURR_ENV"))
.orElse(System.getProperty("CURR_ENV")); // 读部署时注入的环境变量/系统属性
return env != null && "acceptance".equalsIgnoreCase(env);
}
- 它是纯静态工具类,不是 Spring Bean——这是一个值得注意的对比:并非所有工具都要进容器,无状态、无依赖的环境探测用静态方法即可;
- 它来自团队内部的公共工具 starter,体现了业务工程对自定义 starter 的复用。
4.4 完整运行链路
- 启动类
Application的scanBasePackages = {"com.example.payroll", ...}触发组件扫描; SequenceConfig在com.example.payroll.infrastructure...包下被扫到,容器解析其@Bean方法,先检查@ConditionalOnMissingBean;- 条件通过 → 执行
sequenceDao()方法 → 实例入容器 → 回调init(); - 扫描同时发现
@Repository的SequenceGenerator,实例化时解析其@Autowired SequenceDao字段,按类型匹配到第 3 步的 Bean 并注入。
五、@Configuration 是 AOP 切面吗?
不是。 它和 AOP 只是"碰巧用了同一种底层技术(CGLIB 生成子类)",但机制、时机、目的完全不同:
| 对比维度 | AOP 切面 | @Configuration 增强 |
|---|---|---|
| 实现机制 | AbstractAutoProxyCreator(Bean 后处理器)按切点表达式匹配,把目标 Bean 包一层代理 |
ConfigurationClassPostProcessor(BeanFactory 后处理器)在注册阶段直接把这个类的 class 替换成 CGLIB 子类 |
| 发生时机 | Bean 实例化之后,返回给容器前包装 | Bean 实例化之前,改的是类本身(容器 new 出来的已经是子类对象) |
| 拦截规则 | 动态的 pointcut,匹配任意方法 | 固定的:只拦截 @Bean 方法,逻辑写死在 BeanMethodInterceptor 里 |
| 拦截后做什么 | 执行你写的横切逻辑(加锁、记日志…) | 检查"这个 Bean 容器是不是已经在创建了",是则直接从容器取单例返回,而不是真的执行方法体 new 一个新对象 |
| 目的 | 横切关注点 | 保证 @Bean 方法语义正确 |
它要解决的问题很具体:
@Configuration
public class AppConfig {
@Bean public A a() { return new A(); }
@Bean public B b() { return new B(a()); } // ← 这里调用 a()
}
如果没有 CGLIB 增强,b() 里的 a() 就是一次普通 Java 方法调用,会 new 出第二个 A 实例,和容器里那个单例不是同一个对象。增强后的子类拦截这次调用,改为"去容器里取已注册的 A 单例",从而保证单例一致性。
补充:@Configuration(proxyBeanMethods = false) 可以关闭这个增强(所谓 lite 模式),此时 @Bean 方法互调就是普通方法调用——Spring Boot 大量自动配置类用这种写法来省掉 CGLIB 开销。
六、单例住 Map,多例不进仓
所有初始化完成的单例 Bean,最终都住进同一个 Map:
// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 一级缓存
getBean("sequenceDao") 本质上就是 singletonObjects.get("sequenceDao")。一个 JVM 进程里,一个容器,一张表,按名字取对象——这就是 IOC 容器的物理形态。
两点补充:
- 三级缓存:为解决循环依赖,还有
earlySingletonObjects(二级,存提前暴露的半成品)和singletonFactories(三级,存对象工厂)。Bean 完成后统一搬进一级缓存; - 不是所有 Bean 都进这张表:prototype 作用域的 Bean 每次
getBean现做现给,容器不缓存也不管它的销毁;request/session 作用域存在各自的 Scope 里。
| 单例 Bean(singleton) | 多例 Bean(prototype) | |
|---|---|---|
| 容器里存什么 | 配方 + 成品都存 | 只存配方,不存成品 |
| 生产时机 | 启动时生产一次,放进 singletonObjects |
每次有人来要(getBean)才现场 new 一个 |
| 交付后 | 容器一直持有,应用关闭时统一销毁 | 交出去就不管了,不缓存、不调用销毁回调 |
| 多次获取 | 拿到同一个对象 | 每次拿到新对象 |
真实项目里就有 prototype Bean 的典型场景——导入数据校验器(每次导入任务需要一个独立实例来承载本次任务的状态):
@Component
@Scope("prototype") // ← 作用域写在【类】上,声明"我每次要新的"
public class ImportValidator extends AbstractImportValidator { ... }
它的使用方每次都是现场去容器要:
SpringContextUtils.getBeanOfType(ImportValidator.class) // 每调一次 = 新 new 一个
七、@Autowired 与单例是两个正交的概念
一个常见误解:"类里有 @Autowired,它就是单例 Bean"。错。
首先,@Autowired 根本不标注在类上——它只能标在字段、构造器、方法上。标在类上的是另一组注解:
@Repository // ← 标在类上:决定"这个类是不是 Bean"
public class SequenceGenerator {
@Autowired
private SequenceDao sequenceDao; // ← 标在字段上:决定"这个字段要不要注入东西"
}
一个类是不是单例 Bean,由两个独立的问题决定:
| 问题 | 由什么决定 | 默认值 |
|---|---|---|
| ① 它是不是 Bean? | 类上有没有 @Component/@Service/@Repository/@Configuration,或被某个 @Bean 方法声明 |
没有就不是 Bean,只是普通类 |
| ② 它是单例吗? | 类上的 @Scope |
不写 = singleton |
@Autowired 两个问题都不参与。对照两个真实类:
例 A:SequenceGenerator —— @Repository + 无 @Scope → 单例。它是单例的原因是"@Repository 注册 + 没写 @Scope",跟里面那个 @Autowired 无关。
例 B:ImportValidator —— @Component @Scope("prototype"),内部却有多个 @Autowired 字段:
@Component
@Scope("prototype") // 自己是多例
public class ImportValidator ... {
@Autowired private EmployeeRepository employeeRepository; // 照样注入
@Autowired private ThreadPoolTaskExecutor generalThreadPool; // 照样注入
}
含义是:容器每 new 一个新的校验器,都会把它的这些字段重新注入一遍(注入进来的 repository、线程池本身是单例,所以每次注的都是同一个)。@Autowired 只负责"接线",完全不决定单例与否。
经典陷阱:prototype 注入单例会退化
@Service // 我是单例,启动时创建一次
public class ImportService {
@Autowired
private ImportValidator validator; // ← prototype 被注入进单例
}
注入动作只在启动时发生一次,之后 ImportService 一直活着,字段里就永远是同一个 validator 实例——prototype 的"每次要新的"语义完全失效。
这就是为什么成熟项目里不用 @Autowired 注入这类校验器,而是在每次需要时调 SpringContextUtils.getBeanOfType(...) 现取——这是使用 prototype Bean 的正确姿势。若确实想注入式使用,还有 ObjectProvider<T>(每次 .getObject() 取新的)或 @Lookup 方法两种标准解法。
八、多环境下的 Bean
先明确前提:容器是进程级的。日常/预发/线上是不同机器上的不同 JVM,各自有独立的容器和独立的 singletonObjects,机器之间不共享任何 Bean。
所谓"多环境 Bean",指的是同一套代码制品,在不同环境启动时,容器决定哪些 BeanDefinition 保留、哪些丢弃。真实项目里有 4 种典型玩法:
① @Profile —— 按环境名整体启停(用得最多)
@Profile("!utest") // 远程服务/缓存的配置类:单测环境不注册真实远程调用 Bean
@Profile("beta1") // 流量回放、影子数据源等测试专属 Bean:只在 beta1 环境存在
@Profile({"!beta1","!beta2"}) // 正式实现:与上面互斥,实现"同一接口的环境专属实现"
激活方式:spring.profiles.active。常见做法是本地默认 spring.profiles.active=local,部署时用占位符(如 spring.profiles.active=${deploy.env})由发布平台在打包/启动时替换成真实环境——同一份代码,daily 机器上长出一套 Bean,线上机器上长出另一套。
② @ConditionalOnProperty —— 按配置项开关
如 @ConditionalOnProperty(name = "deploy.unit", havingValue = "center"):中心机房才注册这批远程服务消费者 Bean,单元机房直接不注册。
③ @ConditionalOnMissingBean —— 环境间"覆盖替换"
就是本文主例:测试环境 TestDataSourceConfig 提供 H2 版 SequenceDao,生产版自动让位。同一个 Bean 名/类型,不同环境装不同的豆。
④ 同一个 Bean 内部运行时分支
SequenceConfig 里 if (EnvironmentUtils.isAcceptance()) 改 appName——Bean 定义只有一个,但构造参数随环境变量(CURR_ENV)变化。
全景图
代码制品(所有环境的 @Component/@Bean 定义全集)
│ 启动
▼
环境信号注入:spring.profiles.active / 自定义配置项 / CURR_ENV 环境变量
▼
容器过滤:@Profile、@Conditional* 逐条判定 → 生成本环境专属的 BeanDefinition 集合
▼
实例化 + 依赖注入 + 生命周期回调
▼
singletonObjects(一级缓存,Map<String, Object>)← 这就是"豌豆荚"
多环境不是"一个容器里放多套 Bean 来回切",而是启动那一刻就按环境裁剪出唯一一套,之后容器里只有一套。
九、总结
- Bean 就是被容器托管的普通 Java 对象;容器物理上就是一张
Map<String, Object>加一套管理逻辑。SequenceConfig是写给容器看的生产说明书,SequenceGenerator是领货单,两者互不认识,全靠容器在启动时对接——这就是 IOC 的全部含义。 @Configuration不是 AOP 切面:它靠ConfigurationClassPostProcessor在注册期做 CGLIB 类替换,拦截@Bean方法保证单例语义;AOP 是在实例化后按切点包代理。同用 CGLIB,机制、时机、目的全不同。- 单例 Bean 全住一级缓存 Map,prototype 只存配方不进仓;三级缓存为循环依赖服务。
@Autowired只管"接线",不管"几个实例":是不是 Bean 看类上的立体注解,单例与否看@Scope(缺省即单例)。把 prototype 注入单例会退化成"伪多例",正确姿势是每次getBean或用ObjectProvider。- 多环境 Bean = 启动时按环境信号裁剪 BeanDefinition:
@Profile、@ConditionalOnProperty、@ConditionalOnMissingBean、运行时分支四种手段,容器里永远只有当前环境的唯一一套 Bean。
本文示例角色索引(脱敏)
| 角色 | 示例类 | 说明 |
|---|---|---|
| Bean 声明方 | SequenceConfig |
@Configuration + @Bean + @ConditionalOnMissingBean |
| Bean 消费方 | SequenceGenerator |
@Repository + @Autowired 按类型注入 |
| 测试覆盖方 | TestDataSourceConfig |
@TestConfiguration 提供 H2 实现,触发条件让位 |
| 非 Bean 工具 | EnvironmentUtils |
静态环境探测工具,不进容器 |
| 扫描入口 | Application |
@SpringBootApplication(scanBasePackages=...) |
| 多例 Bean | ImportValidator |
@Component + @Scope("prototype") |
| 多例正确取用 | 校验器链构建器 | 每次 getBeanOfType(...) 现取,而非字段注入 |

浙公网安备 33010602011771号