Spring IoC 与依赖注入

笔记/Java/Spring 全家桶/Spring Framework/Spring IoC 与依赖注入

本专题导航#

上级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 时,把所需依赖塞进去。

@Service
public 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#

BeanFactoryApplicationContext
定位最基础的 IoC 容器BeanFactory 的超集
能力延迟加载 Bean、基础 DI国际化、事件发布、资源加载、自动注册 BeanPostProcessor(AOP 代理依赖它)
典型实现DefaultListableBeanFactoryAnnotationConfigApplicationContextClassPathXmlApplicationContext
使用场景框架内部、极少业务直接写开发中常用

Spring Boot 启动时,本质是创建并刷新一个 ApplicationContext(常见为 AnnotationConfigServletWebServerApplicationContext),完成组件扫描、自动配置、Bean 注册后再对外提供服务。

容器启动(简化流程)

加载配置 / 扫描 @Component
→ 解析 BeanDefinition(类、作用域、依赖、初始化方法等元数据)
→ 实例化 Bean → 属性注入(DI)
→ BeanPostProcessor 前置/后置处理(AOP 代理常在此阶段生成)
→ 初始化回调(@PostConstruct、InitializingBean)
→ Bean 就绪,可被 @Autowired 使用
→ 容器关闭时触发 @PreDestroy、DisposableBean

Bean 定义在 Spring 内部以 BeanDefinition 描述,运行时实例存放在 Bean 工厂的单例池等结构中;面试里常说的「三级缓存」就出现在单例 Bean 创建与循环依赖阶段(见下文第 10 节)。

4. 注册 Bean 的三种方式#

方式示例适用
XML<bean id="..." class="..."/>老项目、显式配置
组件扫描@Component + @ComponentScan主流:业务类
Java 配置@Configuration + @Bean第三方类、复杂构造、条件装配

三者可以混用;Boot 项目通常是 扫描 + @Bean 补充

组件注解(语义分层,容器行为相同)

注解含义
@Component通用组件
@Service业务层
@Repository持久层(异常转译等额外处理)
@ControllerWeb 控制层

@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 注册:

@Configuration
public 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 解析规则(按类型)

  1. 容器中该类型只有一个 Bean → 直接注入。
  2. 多个同类型 Bean → 再看字段/参数名是否与某个 bean 名称一致;仍不行则看 @Primary;再不行需 @Qualifier("beanName")
  3. required = false(或构造器用 Optional)→ 找不到 Bean 时不报错,注入 null / 空 Optional。
@Service
public 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 / applicationWeb 容器内按请求、会话、ServletContext 划分
@Component
@Scope("prototype")
public class PrototypeTask { }

注意

  • singleton Bean 注入 prototype Bean 时,prototype 不会在每次使用时重新创建——注入发生在 singleton 创建时,得到的是当时那一个 prototype 实例。若需要「每次调用都是新 prototype」,用 ObjectProvider<PrototypeTask>@Lookup 或从容器手动 getBean
  • singleton 里若有可变成员字段,多线程下需自行保证线程安全;无状态 Service 通常只有方法局部变量,是安全的。

8. Bean 生命周期#

img
img

上图:按「创建 → 初始化 → 运行/销毁」三阶段记忆;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 接口:

@Service
public class CacheWarmupService {
@PostConstruct
public void warmUp() {
// Bean 依赖已全部注入,适合做一次性初始化
}
@PreDestroy
public void shutdown() {
// 容器关闭时释放连接、刷缓存等
}
}

BeanPostProcessor 是容器级扩展点:所有 Bean 初始化前后都会经过它;AOP 创建代理类就依赖这一机制(详见 AOP 与事务)。

9. 延迟加载 @Lazy#

默认情况下,单例 Bean 在容器启动阶段就会创建(除非标记懒加载)。@Lazy 可标在类或 @Bean 方法上,首次使用时才初始化,用于加快启动或打破部分循环依赖场景,但不能靠它解决构造器循环依赖。

10. 循环依赖(单例)#

典型:A 依赖 BB 又依赖 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。初始化逻辑放在 @PostConstructApplicationRunner(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,并避免循环依赖与作用域误用。

文章目录

文章目录