什么是 Spring IOC:倒过来让容器帮你 new,而不是你到处 new
我学的第一个 Spring 概念就是 IOC,当时看完定义——“控制反转(Inversion of Control),是一种设计思想,将对象的创建权交给容器管理”——完全没感觉。背是背下来了,但不知道它到底解决了什么问题。
直到我在一个项目里手动维护了几十个 Service 之间的依赖关系,一个类改动了构造函数,所有new的地方都得跟着改,我才真正理解 IOC 的价值。
IOC 不是什么神秘概念,它就是一句话:以前你 new 对象,现在容器帮你 new。
没有 IOC 的世界:一个改动,连锁崩溃
假设一个订单服务:
publicclassOrderService{privateUserServiceuserService=newUserService();privateInventoryServiceinventoryService=newInventoryService();privatePaymentServicepaymentService=newPaymentService();publicvoidcreateOrder(Orderorder){Useruser=userService.findById(order.getUserId());inventoryService.deduct(order.getProductId());paymentService.charge(user,order.getAmount());}}看起来没什么问题。直到有一天PaymentService要加一个参数:
publicclassPaymentService{privatePaymentGatewaygateway;publicPaymentService(PaymentGatewaygateway){this.gateway=gateway;}}OrderService里new PaymentService()报错了——缺参数。不只是OrderService,所有new了PaymentService的地方都得改。项目里有十几个地方引用了它,改得我心态爆炸。
这就是“硬编码依赖”的代价:对象自己负责创建它所依赖的对象,创建逻辑散落在代码各处,任何变更都会引爆一堆编译错误。
IOC 怎么解决:倒过来
IOC 的思路很简单:对象不自己创建依赖,依赖由外部注入。
没有 IOC:OrderService 自己 new UserService、new InventoryService、new PaymentService (谁用谁创建 → 紧耦合) 有 IOC: IOC 容器创建 UserService、InventoryService、PaymentService 然后把它们注入到 OrderService 里 (容器创建、容器注入 → 松耦合)用 Spring 改写上面那段代码:
@ServicepublicclassOrderService{privatefinalUserServiceuserService;privatefinalInventoryServiceinventoryService;privatefinalPaymentServicepaymentService;// 构造函数注入:依赖由 Spring 容器在创建时传进来publicOrderService(UserServiceuserService,InventoryServiceinventoryService,PaymentServicepaymentService){this.userService=userService;this.inventoryService=inventoryService;this.paymentService=paymentService;}}OrderService不再关心PaymentService怎么创建、需要什么参数。它只声明"我需要这仨",Spring 容器负责凑齐了塞给它。PaymentService加参数也好、改实现也好,OrderService的代码一行不动。
这就是 IOC 的核心价值:调用方和被调用方解耦,创建逻辑集中管理。
IOC 容器是什么
Spring 的 IOC 容器本质上是一个巨大的 Map:
Map<String, Object> beanMap = new ConcurrentHashMap<>(); beanMap.put("userService", new UserService()); beanMap.put("orderService", new OrderService(userService, ...)); ...当然,实际实现比这复杂得多——有作用域管理、生命周期回调、循环依赖检测、AOP 代理等——但核心思想就是"一个帮你存对象、帮你注入依赖的 Map"。
IOC 容器的两个核心接口:
| 接口 | 定位 | 特性 |
|---|---|---|
BeanFactory | 基础容器 | 懒加载,访问时才创建 Bean |
ApplicationContext | 高级容器 | 启动时预创建所有单例 Bean,额外提供事件发布、国际化、资源加载 |
面试里这两者的区别是高频考点。简单记:ApplicationContext是BeanFactory的增强版,Spring Boot 项目里默认用的就是它。
IOC 和 DI 的关系
这是一个经典的面试坑:IOC 和 DI 是一回事吗?
不是。IOC 是思想(控制权反转),DI 是实现方式(依赖注入)。
IOC(控制反转):对象的创建权从"调用方"转移到了"容器" ↓ 怎么实现? DI (依赖注入):容器在创建对象时,把它的依赖"注入"进去 ↓ 怎么注入? 构造函数注入 / Setter 注入 / 字段注入类比一下:IOC 是"我不开车了,我打车"(控制权从你这儿交给了平台),DI 是"平台派了辆出租车接你"(具体的实现方式)。
三种注入方式
| 方式 | 写法 | 优点 | 缺点 |
|---|---|---|---|
| 构造函数注入 | public X(A a, B b) {...} | 强依赖一目了然、不可变(final)、Spring 官方推荐 | 参数多时构造函数很长 |
| Setter 注入 | setA(A a) {...} | 可选依赖、可后续修改 | 依赖不明确、可能 NPE |
| 字段注入 | @Autowired private A a; | 代码最简洁 | 无法测试、隐藏依赖、@Autowired被标记为不推荐 |
我最开始写 Spring 全用@Autowired字段注入——代码少,看着清爽。后来写单元测试时发现没法 mock 依赖,因为依赖是通过反射注入的私有字段。改造成构造函数注入后,测试直接传 mock 进去就行了。现在我的写法是:必选依赖用构造注入 +final,可选依赖用 Setter。
Bean 在 IOC 容器里的一生
一个 Bean 从定义到被销毁,走的是一条完整的生命周期链:
1. 实例化 (instantiate) ↓ 2. 属性赋值 (populate) ↓ 3. BeanNameAware / BeanFactoryAware (回调感知) ↓ 4. BeanPostProcessor.postProcessBeforeInitialization() (前置处理) ↓ 5. @PostConstruct / InitializingBean.afterPropertiesSet() (初始化) ↓ 6. BeanPostProcessor.postProcessAfterInitialization() (后置处理 → AOP 在这里生成代理) ↓ 7. Bean 就绪,存入容器 ↓ 8. @PreDestroy / DisposableBean.destroy() (销毁)我踩过一个坑:在@PostConstruct方法里调用了另一个@PostConstruct还没执行完的 Bean,结果拿到的是个半成品——属性还没注入完。原因就是没搞清楚@PostConstruct的执行顺序不是按依赖优先级来的。从那以后,初始化的跨 Bean 依赖全部放到ApplicationListener<ContextRefreshedEvent>里,等所有 Bean 就绪了再干活。
面试高频追问
Q:IOC 解决了什么问题?
三个:① 解耦(调用方不直接 new 依赖),② 集中管理(所有对象的创建和生命周期在容器里),③ 便于测试(可以注入 mock 对象)。说白了就是把到处散落的new收拢到一个地方。
Q:Spring 怎么知道哪些类要放进 IOC 容器?
四个方式:XML 配置(<bean>标签)、@Component/@Service/@Repository/@Controller注解、@Configuration+@Bean方法、以及 Spring Boot 的自动配置(@EnableAutoConfiguration扫描spring.factories)。
Q:循环依赖怎么解决?
Spring 用三级缓存解决构造注入之外的循环依赖:singletonObjects(成品)、earlySingletonObjects(半成品)、singletonFactories(工厂)。A 依赖 B、B 依赖 A 时:A 创建后暴露出半成品引用 → B 创建时拿到 A 的半成品 → B 完成后注入到 A → A 完成。但构造注入的循环依赖无法解决,会抛BeanCurrentlyInCreationException。
Q:单例 Bean 的线程安全问题?
IOC 容器不保证线程安全。单例 Bean 意味着只有一个实例,多线程同时操作时,如果 Bean 内部有可变状态(成员变量),就会出现线程安全问题。所以 Controller / Service 里不要定义可变成员变量,状态放方法局部变量或用ThreadLocal。
IOC 容器内部的组织方式
Spring IOC 容器的内部结构可以简化为:
ApplicationContext └── BeanFactory (DefaultListableBeanFactory) ├── beanDefinitionMap: Map<String, BeanDefinition> (Bean 的定义信息) ├── singletonObjects: Map<String, Object> (成品单例 Bean) ├── earlySingletonObjects: Map<String, Object> (早期半成品,解决循环依赖) └── singletonFactories: Map<String, ObjectFactory> (Bean 工厂,创建半成品)BeanDefinition里记录的不是对象本身,而是"这个 Bean 怎么创建"——类名、作用域、依赖关系、初始化方法、是否懒加载、@Value的默认值等。容器启动时先解析所有BeanDefinition,再按依赖顺序逐个创建。
说到底,IOC 就是一句话:对象不再自己管自己的依赖,而是声明"我需要什么",让容器替你凑齐了送过来。你从一个到处new的包工头,变成了一个填清单等快递的甲方。这个设计思想贯穿了整个 Spring 生态——Spring MVC、Spring Boot、Spring Cloud——全部建立在 IOC 之上。理解了 IOC,Spring 就入门了一半。
