本专题导航
| 上级 | Spring 全家桶 |
| 同级 | Spring Boot · Spring Cloud |
| 篇 | 主题 |
|---|---|
| 本篇 | IoC 与依赖注入 |
| 下一篇 | AOP 与事务 |
| Spring MVC |
背景与动机
Spring Framework 的核心是 IoC(控制反转) 和 DI(依赖注入):对象的创建与依赖关系交给容器管理,业务类只关注逻辑。搞懂容器、Bean 和注解,是学 Spring Boot、Spring Cloud 的前置基础。
核心原理拆解
1. 什么是 Spring
轻量级 Java 开发框架,提供 IoC、AOP、事务、MVC 等能力,降低耦合、便于测试与扩展。Spring Boot 是在 Spring 之上的快速开发封装,不能替代对 Spring 核心的理解。
日常写业务时,你接触最多的是 @Service、@Autowired;但底层始终是:Spring 容器负责创建对象、维护依赖关系、在合适的时机调用初始化与销毁回调。
2. 传统写法 vs IoC
传统代码里,对象自己 new 依赖,创建权在业务类手里:
public class OrderService { private final OrderRepository orderRepository = new JdbcOrderRepository();}问题在于:换实现(Mock、Redis 缓存版)要改源码;依赖链变长后,new 的代码会散落各处,测试也难替换。
IoC(控制反转) 把「谁创建对象、谁组装依赖」这件事交给容器;DI(依赖注入) 是 IoC 最常见的实现方式——容器在创建 Bean 时,把所需依赖塞进去。
@Servicepublic class OrderService { private final OrderRepository orderRepository;
// 构造器注入:依赖在创建时就绪,字段可 final public OrderService(OrderRepository orderRepository) { this.orderRepository = orderRepository; }}上面 OrderService 不再 new 仓库;Spring 扫描到 @Service 后注册 Bean,发现构造器需要 OrderRepository,再去容器里找(或先创建)对应 Bean 并完成注入。
| 概念 | 一句话 |
|---|---|
| IoC | 控制权从业务代码反转到容器 |
| DI | 容器向对象提供其依赖的具体实现 |
| Bean | 由容器管理的对象实例 |
| 容器 | 负责 Bean 定义、创建、注入、生命周期 |
3. 容器:BeanFactory 与 ApplicationContext
| BeanFactory | ApplicationContext | |
|---|---|---|
| 定位 | 最基础的 IoC 容器 | BeanFactory 的超集 |
| 能力 | 延迟加载 Bean、基础 DI | 国际化、事件发布、资源加载、自动注册 BeanPostProcessor(AOP 代理依赖它) |
| 典型实现 | DefaultListableBeanFactory | AnnotationConfigApplicationContext、ClassPathXmlApplicationContext |
| 使用场景 | 框架内部、极少业务直接写 | 开发中常用 |
Spring Boot 启动时,本质是创建并刷新一个 ApplicationContext(常见为 AnnotationConfigServletWebServerApplicationContext),完成组件扫描、自动配置、Bean 注册后再对外提供服务。
容器启动(简化流程)
加载配置 / 扫描 @Component → 解析 BeanDefinition(类、作用域、依赖、初始化方法等元数据) → 实例化 Bean → 属性注入(DI) → BeanPostProcessor 前置/后置处理(AOP 代理常在此阶段生成) → 初始化回调(@PostConstruct、InitializingBean) → Bean 就绪,可被 @Autowired 使用 → 容器关闭时触发 @PreDestroy、DisposableBeanBean 定义在 Spring 内部以 BeanDefinition 描述,运行时实例存放在 Bean 工厂的单例池等结构中;面试里常说的「三级缓存」就出现在单例 Bean 创建与循环依赖阶段(见下文第 10 节)。
4. 注册 Bean 的三种方式
| 方式 | 示例 | 适用 |
|---|---|---|
| XML | <bean id="..." class="..."/> | 老项目、显式配置 |
| 组件扫描 | @Component + @ComponentScan | 主流:业务类 |
| Java 配置 | @Configuration + @Bean | 第三方类、复杂构造、条件装配 |
三者可以混用;Boot 项目通常是 扫描 + @Bean 补充。
组件注解(语义分层,容器行为相同)
| 注解 | 含义 |
|---|---|
@Component | 通用组件 |
@Service | 业务层 |
@Repository | 持久层(异常转译等额外处理) |
@Controller | Web 控制层 |
@RestController = @Controller + @ResponseBody,属于 MVC 层,见 Spring MVC。
包扫描
@Configuration@ComponentScan(basePackages = "com.example.app")public class AppConfig {}@SpringBootApplication 等价于 @Configuration + @EnableAutoConfiguration + @ComponentScan,默认扫描启动类所在包及其子包。类不在扫描路径下就不会成为 Bean——这是「加了注解却不生效」的首要原因。
5. Java 配置:@Configuration 与 @Bean
当类不在你的包内(如 RestTemplate、数据源工厂),或需要按条件组装时,用 @Bean 注册:
@Configurationpublic class AppConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); }
@Bean("primaryCache") @Primary public CacheManager cacheManager() { return new ConcurrentMapCacheManager(); }}要点:
- 方法返回值的类型注册为 Bean;方法名默认作为 bean 名称(可用
@Bean("name")覆盖)。 @Configuration类本身也是 Bean;Spring 会用 CGLIB 增强配置类,保证@Bean方法多次调用返回同一单例(@Bean方法内互相调用时尤其重要)。- 若只是普通
@Component里写@Bean方法,没有上述代理增强,多次调用可能得到不同实例。
6. 依赖注入方式
| 方式 | 说明 | 建议 |
|---|---|---|
| 构造器注入 | 通过构造器参数注入 | 首选:依赖不可变、必填、易测 |
| Setter 注入 | 通过 setter 方法 | 可选依赖、遗留代码 |
| 字段注入 | @Autowired 直接标在字段 | 简洁但不利于测试与不可变设计 |
@Autowired 解析规则(按类型)
- 容器中该类型只有一个 Bean → 直接注入。
- 有多个同类型 Bean → 再看字段/参数名是否与某个 bean 名称一致;仍不行则看
@Primary;再不行需@Qualifier("beanName")。 required = false(或构造器用Optional)→ 找不到 Bean 时不报错,注入null/ 空 Optional。
@Servicepublic class NotifyService { private final SmsSender smsSender; private final EmailSender emailSender;
public NotifyService( @Qualifier("aliSmsSender") SmsSender smsSender, @Primary EmailSender emailSender) { this.smsSender = smsSender; this.emailSender = emailSender; }}@Resource(JSR-250):默认先按名称再按类型,常用于指定 bean 名称且不想写 @Qualifier 时。
@Value:注入配置项或 SpEL 表达式,适合简单标量,复杂配置更推荐 @ConfigurationProperties(Boot 专题)。
@Value("${app.page-size:20}")private int pageSize;Spring 4.3+ 起,单个构造器的类即使不写 @Autowired,容器也会自动注入——因此推荐只保留构造器,去掉字段上的 @Autowired。
7. Bean 作用域
| 作用域 | 说明 |
|---|---|
singleton | 默认;整个容器一份实例 |
prototype | 每次 getBean 或注入到新 Bean 时新建 |
request / session / application | Web 容器内按请求、会话、ServletContext 划分 |
@Component@Scope("prototype")public class PrototypeTask { }注意
singletonBean 注入prototypeBean 时,prototype 不会在每次使用时重新创建——注入发生在 singleton 创建时,得到的是当时那一个 prototype 实例。若需要「每次调用都是新 prototype」,用ObjectProvider<PrototypeTask>、@Lookup或从容器手动getBean。singleton里若有可变成员字段,多线程下需自行保证线程安全;无状态 Service 通常只有方法局部变量,是安全的。
8. Bean 生命周期

上图:按「创建 → 初始化 → 运行/销毁」三阶段记忆;AOP 代理多在 BPP 后置阶段生成。
完整流程可简化为:
实例化(构造器) → 属性赋值(@Autowired / @Value) → BeanNameAware、BeanFactoryAware、ApplicationContextAware 等 Aware 回调 → BeanPostProcessor.postProcessBeforeInitialization → 初始化:@PostConstruct → InitializingBean.afterPropertiesSet → 自定义 init-method → BeanPostProcessor.postProcessAfterInitialization(AOP 代理多在此之后) → 使用中 → 销毁:@PreDestroy → DisposableBean.destroy → 自定义 destroy-method代码里常用 @PostConstruct / @PreDestroy(JSR-250),不依赖 Spring 接口:
@Servicepublic class CacheWarmupService { @PostConstruct public void warmUp() { // Bean 依赖已全部注入,适合做一次性初始化 }
@PreDestroy public void shutdown() { // 容器关闭时释放连接、刷缓存等 }}BeanPostProcessor 是容器级扩展点:所有 Bean 初始化前后都会经过它;AOP 创建代理类就依赖这一机制(详见 AOP 与事务)。
9. 延迟加载 @Lazy
默认情况下,单例 Bean 在容器启动阶段就会创建(除非标记懒加载)。@Lazy 可标在类或 @Bean 方法上,首次使用时才初始化,用于加快启动或打破部分循环依赖场景,但不能靠它解决构造器循环依赖。
10. 循环依赖(单例)
典型:A 依赖 B,B 又依赖 A。
| 注入方式 | 单例能否解决 | 原因 |
|---|---|---|
| 构造器注入 | 不能 | 创建 A 时尚无 B 实例,反之亦然,启动即失败 |
| Setter / 字段注入 | 通常能 | 先实例化对象,再放入三级缓存,再填充属性 |
Spring 对单例循环依赖的处理思路(简化):提前把「半成品」Bean 暴露到缓存,让另一方先引用,再回头补全属性。这是框架的补救机制,不是设计目标——业务上应通过拆分职责、引入中间层、事件驱动等方式避免循环依赖。
11. 最小可运行示例(纯 Java 配置)
不启动 Boot,仅用 Spring Framework 也能演示 IoC:
@Configuration@ComponentScan("com.example.demo")public class DemoConfig {}
public class Main { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(DemoConfig.class); OrderService service = ctx.getBean(OrderService.class); service.placeOrder(1L); }}AnnotationConfigApplicationContext 构造时会立即执行 refresh(),完成扫描与 Bean 创建;这与 Boot 启动后容器就绪是同一套机制。
常见陷阱与错误示例
1. 同一接口多个实现未指定 @Qualifier / @Primary
@Autowired 按类型匹配到多个 Bean 会报 NoUniqueBeanDefinitionException。解决:业务上标 @Primary,或注入点用 @Qualifier / @Resource(name=...)。
2. 在非 Spring 管理的对象上使用 @Autowired
new OrderService() 出来的对象不会被注入。必须由容器创建(扫描、@Bean、或从 ApplicationContext.getBean 获取)。
3. 组件不在扫描包路径下
启动类在 com.example.app,Bean 写在 com.other.module 且未 @ComponentScan → 容器里根本没有该 Bean。
4. 把 @Configuration 当成普通类用
在 @Configuration 的 @Bean 方法里调用另一个 @Bean 方法,依赖 CGLIB 代理保证单例;若改成 @Component + @Bean,可能每次调用都 new 一个新对象。
5. prototype 注入 singleton 的「作用域错配」
以为 @Scope("prototype") 会在每次调用 Service 方法时新建,实际上注入进 singleton 的 prototype 实例是固定的;需要 ObjectProvider 或 @Lookup。
6. 静态字段 / 构造器里过早使用 Bean
容器尚未完成 refresh 时就去 getBean,或静态块里访问容器,容易 NPE 或拿到不完整 Bean。初始化逻辑放在 @PostConstruct 或 ApplicationRunner(Boot)更合适。
面试高频问题
更多速查见 Spring 面试题。
IoC 和 DI 有什么区别?
IoC 是思想:控制权反转给容器。DI 是实现手段:容器把依赖注入对象。常一起说,但 IoC 范围更广(还包括工厂模式等变体)。
BeanFactory 和 ApplicationContext 的区别?
ApplicationContext 是 BeanFactory 子接口,功能更全;开发中几乎只用 ApplicationContext。后者在启动时会注册 BeanPostProcessor 等,与 AOP、事件、国际化集成更好。
Spring 如何解决循环依赖?
仅针对单例 + setter/字段注入:三级缓存提前暴露早期引用。构造器注入的循环依赖无法解决,应改设计。
为什么推荐构造器注入?
依赖明确、字段可 final、便于单元测试(直接 new 并传入 Mock)、避免部分「半初始化」对象被使用;Spring 官方也推荐构造器注入作为首选。
@Component 和 @Bean 的区别?
@Component 标在自己写的类上,由扫描注册;@Bean 标在配置类的方法上,方法的返回值注册为 Bean,常用于第三方类或需要编程式组装的场景。
一句话总结
Spring 核心 = 容器管 Bean + 注入依赖;理解 ApplicationContext 启动与 Bean 生命周期,优先构造器注入,分清 @Component 扫描与 @Configuration + @Bean,并避免循环依赖与作用域误用。