Spring 依赖注入有哪几种实现方式(构造器注入、Setter 注入、接口注入)?
在 Spring 框架中,依赖注入(Dependency Injection, DI)主要有 3+1 种实现方式。你提到的构造器注入、Setter 注入和接口注入是理论上的三种经典方式。
但在 Spring 的实际应用中,接口注入基本被废弃/极少用于业务代码,而字段注入(Field Injection)虽然你没有提到,却是平时开发中最常见的。
下面为你详细解析 Spring 中依赖注入的这几种方式及其优缺点:
1. 构造器注入 (Constructor Injection) —— 🏆 官方最推荐
通过类的构造函数来完成依赖关系的设定。Spring 团队自 Spring 4.x 起强烈推荐这种方式。
- 实现方式: 将依赖项声明为
final属性,并通过构造方法传入。如果类只有一个构造方法,Spring 4.3 之后甚至可以省略@Autowired注解。 - 代码示例:(日常开发中,常配合 Lombok 的java
@Service public class UserService { private final UserRepository userRepository; // Spring 4.3+ 只有单个构造器时,@Autowired 可以省略 @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } }@RequiredArgsConstructor注解来简化代码) - 优点:
- 保证不可变性: 可以将依赖属性声明为
final,防止被意外修改。 - 保证完全初始化: 实例化对象时,必须传入所有参数,避免了出现 NullPointerException(对象创建后即刻可用)。
- 方便单元测试: 不需要依赖 Spring 容器,直接
new UserService(mockRepository)即可测试。 - 代码异味探测: 如果构造器参数过多,说明该类违反了单一职责原则(SRP),能及时提醒开发者重构。
- 保证不可变性: 可以将依赖属性声明为
- 缺点: 无法解决循环依赖问题(例如 A 依赖 B,B 依赖 A),启动时会直接抛出
BeanCurrentlyInCreationException(不过这反而促使开发者优化架构)。
2. Setter 注入 (Setter Injection)
通过属性的 Setter 方法来完成依赖的注入。
- 实现方式: 提供无参构造器(隐式或显式),并在 Setter 方法上添加
@Autowired注解。 - 代码示例:java
@Service public class UserService { private UserRepository userRepository; @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } } - 优点:
- 支持可选依赖: 可以配置
@Autowired(required = false),如果容器中没有对应的 Bean 也不报错。 - 支持解决循环依赖: Spring 可以先通过无参构造器实例化对象(提前暴露引用),然后再通过 Setter 方法注入依赖。
- 灵活性高: 可以在对象运行期间重新注入/修改依赖。
- 支持可选依赖: 可以配置
- 缺点:
- 不能将属性声明为
final。 - 对象可能处于不完整状态:如果忘了调用 Setter 方法(比如在非 Spring 环境下手动 new 对象),直接使用属性会导致空指针异常。
- 不能将属性声明为
3. 字段注入 (Field Injection) —— ⚠️ 最常见但官方不推荐
直接在类的私有属性上使用 @Autowired 或 @Resource 注解。这是前几年最流行的做法。
- 实现方式: 依赖 Spring 的反射机制直接给字段赋值。
- 代码示例:java
@Service public class UserService { @Autowired private UserRepository userRepository; } - 优点: 代码极其简洁,写起来最快,没有多余的模板代码。
- 缺点(为何官方和 IDEA 都警告不推荐):
- 违反封装原则: 破坏了类的封装性,外部无法通过常规手段(构造器/Setter)传入依赖。
- 难以单元测试: 脱离了 Spring 容器,你无法简单地
new出来并给私有属性赋值,必须使用反射或强依赖 Mockito 等测试框架。 - 容易掩盖代码缺陷: 因为加个
@Autowired太容易了,很容易在一个类里注入十几个依赖,导致类变得极其臃肿,违背了单一职责原则。 - 不能使用
final修饰符。
4. 接口注入 (Interface Injection) —— ❌ Spring 业务开发基本不用
接口注入是指:定义一个接口,里面包含注入依赖的方法,然后让需要依赖的类实现该接口。容器通过调用该接口的方法来注入依赖。
- 在 Spring 中的地位: Spring 不支持/不使用这种方式来注入业务对象的依赖。这种方式侵入性极强,违背了 Spring "非侵入式(POJO)" 的设计理念。
- Spring 的特例(Aware 接口):
虽然不用接口注入业务 Bean,但 Spring 框架内部提供了一系列Aware接口(如ApplicationContextAware、BeanFactoryAware)。如果你的业务类需要获取 Spring 容器底层的对象,可以通过实现这些接口,让 Spring 把自己注入进来。这可以看作是接口注入的一种变体。 - 代码示例(获取 ApplicationContext):java
@Service public class MyService implements ApplicationContextAware { private ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) { this.context = applicationContext; // Spring 容器会调用此方法注入 } }
总结与最佳实践建议
| 注入方式 | 是否支持 final |
是否支持可选依赖 | 循环依赖支持 | IDEA 警告 | 推荐度 |
|---|---|---|---|---|---|
| 构造器注入 | ✅ 是 | ❌ 否 | ❌ 报错 | 无 | ⭐⭐⭐⭐⭐ (强烈推荐) |
| Setter注入 | ❌ 否 | ✅ 是 | ✅ 支持 | 无 | ⭐⭐⭐ (适合可选依赖) |
| 字段注入 | ❌ 否 | ❌ 否 | ✅ 支持 | 有警告 | ⭐ (非常不推荐,但很普遍) |
| 接口注入 | ❌ 否 | ❌ 否 | - | - | 不适用于业务依赖 |
💡 目前的最佳实践(Best Practice):
强制依赖/核心依赖使用「构造器注入」,可选/可变依赖使用「Setter 注入」,彻底抛弃「字段注入」。
(如果嫌构造器注入写起来太长,建议使用 Lombok 的 @RequiredArgsConstructor,既优雅又符合规范)。