Spring核心IoC/DI
开篇:为什么 Spring 能统治 Java 后端?
想象一下你去一家餐厅吃饭。你不需要自己买菜、洗菜、炒菜,只需要坐下来点餐,厨师就会把做好的菜端到你面前。Spring 做的事情和餐厅一模一样——你不需要自己 new 对象、管理依赖,只需要"点菜"(声明需要什么),Spring 容器这个"厨师"就会把准备好的对象"端上来"。
这就是 Spring 能统治 Java 后端十多年的秘密武器:IoC(控制反转) 和 DI(依赖注入)。
本篇会从这两个核心概念出发,带你搞懂 Spring 容器的工作原理、Bean 的作用域和自动装配机制。所有内容都配有代码示例和对比表格,力求让你既能理解原理,也能应付面试。
一、IoC:控制反转
到底反转了什么?
先看一段没有 Spring 的代码:
class OrderService {
// 我自己 new,我自己控制
private OrderDao dao = new OrderDao();
public void createOrder() {
dao.insert(new Order());
}
}这段代码有什么问题?OrderService 亲手 new 了一个 OrderDao,它不仅知道 OrderDao 的存在,还知道怎么创建它。如果 OrderDao 的构造方法变了,比如加了个参数 new OrderDao(dataSource),那 OrderService 也得跟着改。这就是耦合。
再看有 Spring 之后:
@Service
class OrderService {
@Autowired // 容器帮我注入,控制权在容器手里
private OrderDao dao;
public void createOrder() {
dao.insert(new Order());
}
}OrderService 不再关心 OrderDao 是怎么来的,它只管用。对象的创建和装配全部交给了 Spring 容器。
对比一下:
| 维度 | 传统方式 | IoC 方式 |
|---|---|---|
| 对象由谁创建 | 自己 new | 容器创建 |
| 依赖由谁管理 | 自己组装 | 容器注入 |
| 控制权在谁手里 | 应用代码 | Spring 容器 |
所谓"控制反转",就是把对象创建和依赖管理的控制权从应用代码交给了 Spring 容器。原来是你控制一切,现在是容器控制一切——控制权被"反转"了。
IoC 解决了什么问题?
用三个真实场景来说明:
场景一:使用者不用关心实现细节。 假设 OrderDao 的构造需要四个参数 new OrderDao(dataSource, config, cache, logger),没有 IoC,每个用到 OrderDao 的地方都要知道这四个参数怎么来。有了 IoC,你只管 @Autowired,容器替你搞定。
场景二:避免重复创建对象。 没有 IoC,ServiceA 和 ServiceB 都用 OrderDao,各自 new 一个,内存里就有两份。有了 IoC,默认单例,全局只有一份。
场景三:修改对使用方透明。 OrderDao 从 MySQL 实现换成 PostgreSQL 实现,只需要改配置或者改实现类的注解,所有注入了 OrderDao 的地方完全不用动。
Spring 的 IoC 容器长什么样?
从使用者的角度看,IoC 容器就像一个大仓库:
配置元数据(注解、XML、Java 配置类)告诉容器有哪些 Bean、它们之间的依赖关系,容器负责创建和组装。
二、DI:依赖注入
IoC 是思想,DI 是实现。DI 的意思是:容器在创建 Bean 的时候,自动把它依赖的其他 Bean 注入进去。
Spring 支持三种注入方式,各有优劣。
构造器注入(官方推荐)
@Component
public class OrderService {
private final OrderDao orderDao;
private final InventoryService inventoryService;
@Autowired
public OrderService(OrderDao orderDao, InventoryService inventoryService) {
this.orderDao = orderDao;
this.inventoryService = inventoryService;
}
}好处:
- 依赖不可变(
final),对象创建完就是完整的,不存在"半成品"的状态。 - 方便单元测试,直接
new OrderService(mockDao, mockInventory)就行,不需要启动 Spring 容器。 - 如果参数太多,说明这个类职责太重,构造器的"臃肿"恰恰是一个重构信号。
从 Spring 4.3 开始,如果类只有一个构造函数,
@Autowired可以省略。
Setter 注入
@Component
public class OrderService {
private OrderDao orderDao;
@Autowired
public void setOrderDao(OrderDao orderDao) {
this.orderDao = orderDao;
}
}适合可选依赖。Spring 官方建议:强制依赖用构造器注入,可选依赖用 Setter 注入。
字段注入
@Component
public class OrderService {
@Autowired
private OrderDao orderDao;
}代码最简洁,但 IDEA 会给你一个警告:Field injection is not recommended。为什么?
- 破坏封装性:私有字段本不该被外部框架感知,但字段注入要求 Spring 通过反射访问私有成员。
- 构造阶段可能 NPE:Bean 的初始化顺序是
构造方法 → @Autowired,如果你在构造方法里用了被注入的字段,此时它还是null。 - 不利于单元测试:测试时必须启动 Spring 容器,无法直接
new。
三种方式对比
| 方式 | 不可变性 | 可测试性 | 代码量 | 官方推荐 |
|---|---|---|---|---|
| 构造器 | 支持 final | 好(直接 new) | 多 | 强制依赖推荐 |
| Setter | 不支持 | 一般 | 中 | 可选依赖推荐 |
| 字段 | 不支持 | 差(依赖容器) | 少 | 不推荐 |
@Autowired vs @Resource
这两个注解都能完成注入,日常开发中看起来效果一样,但底层行为有关键区别:
| 维度 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 框架(org.springframework) | JDK 标准 JSR-250(jakarta.annotation) |
| 匹配顺序 | 先 byType,再 byName | 先 byName,再 byType |
| 作用位置 | 构造器、字段、Setter | 字段、Setter(不支持构造器) |
| 可选性 | required = false 允许注入 null | 必须存在,否则报错 |
| 多实现消歧 | 配合 @Qualifier("beanName") | 直接 @Resource(name = "beanName") |
举个例子,当同一个接口有两个实现时:
@Component("mysqlDao")
class MysqlOrderDao implements OrderDao {}
@Component("pgDao")
class PgOrderDao implements OrderDao {}
// @Autowired 方式:先 byType 找到两个,再 byName 匹配字段名
@Autowired
private OrderDao mysqlDao; // 能匹配到 mysqlDao
// @Resource 方式:先 byName 匹配字段名
@Resource
private OrderDao mysqlDao; // 也能匹配到 mysqlDao简单来说:@Autowired 是 Spring 自家的,@Resource 是 JDK 标准的。如果你的项目可能迁移到其他 IoC 容器,用 @Resource 更安全。
三、BeanFactory vs ApplicationContext
Spring 容器有两个核心接口,它们的关系像"基础款"和"豪华款":
| 维度 | BeanFactory | ApplicationContext |
|---|---|---|
| 定位 | 最基础的容器接口 | BeanFactory 的增强子接口 |
| 加载时机 | 懒加载(第一次 getBean() 才创建) | 预加载(启动时就创建所有单例 Bean) |
| 事件机制 | 不支持 | 支持 ApplicationEvent 发布/监听 |
| 国际化 | 不支持 | 支持 MessageSource |
| AOP 集成 | 需要手动配置 | 自动集成 |
| 日常开发 | 基本不直接使用 | 天天在用 |
ApplicationContext 的预加载有一个好处:在启动阶段就能发现配置错误(比如缺少某个 Bean),而不是等到运行时才爆出来。
常见的 ApplicationContext 实现类:
ClassPathXmlApplicationContext:从 classpath 加载 XML 配置(传统方式)AnnotationConfigApplicationContext:基于注解/Java 配置(SpringBoot 主流)FileSystemXmlApplicationContext:从文件系统路径加载 XML 配置
BeanFactory vs FactoryBean
这两个名字长得像双胞胎,但完全不同:
| BeanFactory | FactoryBean | |
|---|---|---|
| 是什么 | Spring 容器的核心接口 | 一个特殊的 Bean 接口 |
| 做什么 | 管理所有 Bean 的生命周期 | 自己就是一个"工厂",getObject() 返回它生产的对象 |
| 使用方式 | context.getBean(name) | 当你从容器获取 FactoryBean 时,得到的是 getObject() 的返回值 |
FactoryBean 的典型场景:创建过程特别复杂的对象。
比如 Dubbo 的 ReferenceBean,它实现了 FactoryBean 接口。当你用 @DubboReference 注入一个远程服务时,实际上是 ReferenceBean.getObject() 返回的动态代理对象。所有的网络通信、序列化、服务发现细节都被封装在里面,你感知不到。
// Dubbo 中 ReferenceBean 的简化结构
public class ReferenceBean<T> implements FactoryBean<T> {
@Override
public T getObject() throws Exception {
// 创建远程服务的动态代理对象
return createProxy();
}
}什么时候需要从容器手动获取 Bean?
一般情况下,通过 @Autowired 自动注入就够了。但有些场景需要手动获取:
- 充血模型中的领域对象不是 Spring 管理的,但需要调用 Service 层方法
- 工具类/静态方法中无法使用注入
这时候可以写一个 SpringContextHolder:
@Component
public class SpringContextHolder implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext ctx) {
context = ctx;
}
public static <T> T getBean(Class<T> clazz) {
return context.getBean(clazz);
}
}
// 在非 Spring 管理的类中使用
public class OrderModel {
public void doSomething() {
OrderService service = SpringContextHolder.getBean(OrderService.class);
service.process(this);
}
}四、Spring 容器启动流程
Spring 容器启动的核心方法是 AbstractApplicationContext.refresh(),整个过程可以分为两大阶段:容器启动阶段和 Bean 实例化阶段。
关键步骤说明:
第 2 步:加载配置,解析为 BeanDefinition。 BeanDefinition 是 Bean 的"身份证",记录了类名、作用域、构造参数、依赖关系等信息。它由 BeanDefinitionReader 解析生成,不同格式的配置有不同的 Reader(XmlBeanDefinitionReader、AnnotatedBeanDefinitionReader 等)。
第 4 步:执行 BeanFactoryPostProcessor。 这是 Bean 实例化之前的最后一个扩展点。典型用途:
PropertySourcesPlaceholderConfigurer:替换${}占位符- 动态修改 BeanDefinition(比如修改某个 Bean 的作用域)
- 编程式注册新的 BeanDefinition
第 5 步:注册 BeanPostProcessor。 注意这里只是"注册",不是"执行"。BeanPostProcessor 会在后续 Bean 实例化过程中的初始化前后被调用。AOP 代理就是在 AbstractAutoProxyCreator(一个 BeanPostProcessor)的后置处理方法中创建的。
第 7 步:实例化所有非懒加载单例 Bean。 这是最耗时的一步,调用 beanFactory.preInstantiateSingletons(),遍历所有 BeanDefinition,逐个 getBean() 创建实例。
五、Bean 的作用域
Spring 的 Bean 默认是单例的,但它还支持其他几种作用域:
| 作用域 | 说明 | 创建时机 | 适用场景 |
|---|---|---|---|
singleton | 容器中只有一个实例(默认) | 容器启动时 | 无状态的 Service、Dao |
prototype | 每次获取都创建新实例 | 每次 getBean() 时 | 有状态的对象 |
request | 每个 HTTP 请求一个实例 | 每次 HTTP 请求 | Web 应用(请求级数据) |
session | 每个 HTTP Session 一个实例 | 每个新 Session | Web 应用(会话级数据) |
application | 整个 ServletContext 一个实例 | 应用启动时 | Web 应用(全局共享) |
用法很简单:
@Service
@Scope("prototype")
public class StatefulService {
private int count = 0;
}日常开发中 singleton 和 prototype 最常用,后面三个只在 Web 应用中有效。
单例 Bean 线程安全吗?
不一定。 关键看 Bean 有没有可变的成员变量。
无状态 Bean(如一般的 Controller、Service、Dao,方法内用局部变量):线程安全。所有线程共享一个实例,但每个线程各自操作自己的栈上变量,互不干扰。
有状态 Bean(有可变的成员变量):不安全。
@Service
public class CounterService {
private int count = 0; // 共享可变状态,线程不安全!
public int increment() {
return ++count;
}
}解决方案(按推荐程度排序):
- 消除可变状态:把成员变量改成方法参数或局部变量(最佳方案)
- 使用并发工具类:
AtomicInteger、ConcurrentHashMap等(性能好) - 使用 ThreadLocal:每个线程一份副本(适合线程级隔离)
- 改成 prototype:每次创建新实例(有性能开销,不推荐)
- 加锁 synchronized:保证互斥(影响并发性能,不推荐)
六、自动装配
自动装配就是 Spring 自动帮你把依赖的 Bean "接上线",不需要你手动指定每一个依赖关系。
四种自动装配策略
| 策略 | 说明 | 使用场景 |
|---|---|---|
byName | 按属性名匹配 Bean 名称 | XML 配置时代 |
byType | 按属性类型匹配 Bean | XML 配置时代 |
constructor | 按构造函数参数类型匹配 | XML 配置时代 |
@Autowired / @Resource | 注解驱动的自动装配 | 现代主流 |
在 SpringBoot 时代,我们基本只用注解方式。
同一类型多个 Bean 怎么办?
当一个接口有多个实现时,Spring 不知道注入哪一个,需要你消歧:
// 方式一:@Autowired + @Qualifier
@Autowired
@Qualifier("alipayService")
private PayService payService;
// 方式二:@Resource 按名称
@Resource(name = "alipayService")
private PayService payService;
// 方式三:@Primary 标记默认实现
@Primary
@Service("alipayService")
public class AlipayService implements PayService {}@Autowired 可以注入集合
一个非常实用的功能——@Autowired 可以一次性注入同一类型的所有 Bean:
// 注入 List:获取所有 PayService 的实现
@Autowired
private List<PayService> payServices;
// 注入 Map:key 是 Bean 名称,value 是实例
@Autowired
private Map<String, PayService> payServiceMap;
// 注入 Set:去重
@Autowired
private Set<PayService> payServiceSet;Map 注入在策略模式中特别好用:
@Service
public class PayRouter {
@Autowired
private Map<String, PayService> payServiceMap;
public void pay(String channel, BigDecimal amount) {
PayService service = payServiceMap.get(channel);
if (service == null) {
throw new IllegalArgumentException("不支持的支付渠道: " + channel);
}
service.pay(amount);
}
}创建 Bean 的几种方式
除了最常用的 @Component 系列注解,Spring 还支持多种方式创建 Bean:
| 方式 | 示例 | 适用场景 |
|---|---|---|
@Component 系列 | @Service、@Repository、@Controller | 日常开发最常用 |
@Bean 方法 | @Configuration 类中的 @Bean 方法 | 封装 Starter、引入第三方库的类 |
XML <bean> | <bean id="..." class="..."/> | 老项目维护 |
@Import | @Import(SomeConfig.class) | 模块化配置导入 |
| 编程式注册 | 实现 BeanDefinitionRegistryPostProcessor | 动态注册 Bean |
根据配置动态创建 Bean
实际项目中经常需要根据环境或配置动态创建不同的 Bean:
方式一:@Conditional(最灵活)
@Bean
@ConditionalOnProperty(name = "cache.type", havingValue = "redis")
public CacheService redisCacheService() {
return new RedisCacheService();
}
@Bean
@ConditionalOnProperty(name = "cache.type", havingValue = "local")
public CacheService localCacheService() {
return new LocalCacheService();
}方式二:@Profile(区分环境)
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new EmbeddedDatabaseBuilder().setType(H2).build();
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
return createProductionDataSource();
}方式三:@ConfigurationProperties(属性绑定)
@ConfigurationProperties(prefix = "app.datasource")
public class DataSourceProperties {
private String url;
private String username;
private String password;
// getter/setter
}七、常见面试题精选
Q1:IoC 和 DI 的关系是什么?
IoC 是一种设计思想(控制反转),DI 是这种思想的具体实现方式(依赖注入)。Spring 通过 DI 来实现 IoC。类比:IoC 是"让专业的人做专业的事"这个理念,DI 是"餐厅给你端菜"这个具体动作。
Q2:构造器注入和字段注入,选哪个?
Spring 官方推荐构造器注入。字段注入虽然代码少,但有三个问题:破坏封装性(Spring 要反射访问私有字段)、构造阶段可能 NPE(@Autowired 在构造方法之后才执行)、不利于单元测试(必须启动容器)。构造器注入的写法"臃肿"恰恰是一个提醒——如果参数太多,说明这个类违背了单一职责原则。
Q3:@Service、@Component、@Repository 有什么区别?
功能上几乎没有区别,都是 @Component 的特化形式。区别在于语义:@Service 标记业务层,@Repository 标记数据访问层(它还会自动把持久化异常转成 Spring 的 DataAccessException),@Controller 标记控制层。使用这些注解是为了让代码的分层更清晰。
Q4:Spring 中单例 Bean 是线程安全的吗?
不一定。无状态的单例 Bean(没有可变成员变量)是线程安全的。有状态的单例 Bean 不是线程安全的,需要通过 ThreadLocal、原子类或改成 prototype 作用域来解决。
Q5:BeanFactory 和 ApplicationContext 的区别?
ApplicationContext 继承了 BeanFactory,是它的增强版。核心区别:BeanFactory 懒加载、ApplicationContext 预加载;ApplicationContext 额外支持事件机制、国际化、AOP 自动集成等功能。日常开发中我们用的都是 ApplicationContext。
小结
| 概念 | 一句话总结 |
|---|---|
| IoC | 对象的控制权从代码交给容器——你不用自己 new,容器帮你造 |
| DI | 容器在创建 Bean 时自动注入依赖——三种方式中构造器注入最推荐 |
| BeanFactory | Spring 最基础的容器接口,懒加载 |
| ApplicationContext | BeanFactory 的增强版,预加载,支持事件/国际化/AOP |
| FactoryBean | 不是容器,是一个"工厂 Bean",getObject() 返回它生产的复杂对象 |
| Bean 作用域 | singleton(默认单例)和 prototype(每次新建)最常用 |
| 自动装配 | @Autowired 先 byType 再 byName,@Resource 反过来 |
| 容器启动 | refresh() 方法:加载配置 → BeanDefinition → 工厂后置处理 → 注册后置处理器 → 实例化 Bean |
Spring 的核心理念就一句话:你负责写业务逻辑,对象的创建和组装交给我。 理解了 IoC/DI,就抓住了 Spring 的灵魂。后面要讲的 AOP、Bean 生命周期、循环依赖,都是在这个基础上展开的。