本文将简单介绍Spring三种注入方式:字段注入、setter注入、构造器注入,对比和使用场景。
一、字段注入
字段注入是通过在字段上直接使用@Autowired注解来实现的,Spring容器会利用反射机制直接设置字段的值。仅适用于简单场景。使用示例如下:
java
@Component
public class UserService {
@Autowired // 字段注入
private UserRepository userRepository;
@Resource(name = "emailService") // 字段注入
private EmailService emailService;
}
@Autowired和@Resource 对比
@Autowired默认按类型查找
如果找到多个,按以下顺序尝试:1、查找有 @Primary 注解的bean;2、查找有 @Priority 注解的bean(值越小优先级越高)
如果都没有找到,则按属性名匹配(Spring 4.3+);如果仍然有歧义,抛出异常
@Resource 默认按name属性名称查找(即使类型不完全匹配,只要名称对就注入) → 找不到则抛出异常
如果未指定 name 属性:则按字段/方法名查找
| 特性 | @Autowired | @Resource |
|---|---|---|
| 所属规范 | Spring 框架原生注解 | JSR-250 (Java EE 标准) |
| 默认装配方式 | 按类型 (byType) | 按名称 (byName) |
| 是否支持构造器注入 | ✅ 支持 | ❌ 不支持 |
| 是否支持可选依赖 | ✅ (required=false) |
✅ (找不到则跳过) |
| 是否支持集合注入 | ✅ 支持 | ❌ 不支持 |
| 与 @Qualifier 配合 | ✅ 需要配合使用 | ✅ 自身可指定 name |
| Spring 推荐度 | ✅ 推荐 | ⚠️ 可用但非首选 |
优缺点:
**优点:**1、简洁性:代码简洁,减少样板代码;2、易于理解:依赖声明和使用在同一位置
**缺点:**1、不可变性:无法声明为final,线程安全性需额外注意;2、依赖隐藏:从类的外部无法直观看出依赖关系;3、测试困难:需要反射或Spring容器才能注入依赖;4、违反单一职责:容易导致类拥有过多依赖;5、循环依赖风险:容易掩盖设计问题
二、Setter注入
通过setter方法在对象创建后注入依赖。
可以在对象创建后重新注入依赖;可选依赖:适合非必需依赖的注入;可读性强:方法名明确表示设置的是什么依赖
使用示例如下:
java
// 1. XML配置方式
<bean id="orderService" class="com.example.OrderService">
<property name="paymentService" ref="paymentService"/>
<property name="timeout" value="5000"/>
</bean>
// 2. 注解方式
@Service
public class OrderService {
private PaymentService paymentService;
private int timeout;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
@Value("${order.timeout:3000}")
public void setTimeout(int timeout) {
this.timeout = timeout;
}
// 也支持在字段上直接注解
@Autowired
private NotificationService notificationService;
}
优缺点:
**优点:**1、灵活性:可以在对象创建后重新注入依赖;2、可选依赖:适合非必需依赖的注入;3、可读性强:方法名明确表示设置的是什么依赖
**适用场景:**1、可选依赖;2、需要重新配置依赖的场景;3、遗留代码或需要遵循JavaBean规范的场景
三、构造器注入(首推依赖注入方式)
通过类的构造方法来注入依赖,在对象创建时即完成所有依赖的注入。
优点 :不可变性 :依赖可声明为 final,确保线程安全;完全初始化 :对象创建时即获得所有必需依赖,保证可用性;明确的依赖关系:从构造器签名即可看出类的依赖需求;便于测试 :无需容器即可进行单元测试;循环依赖检测:Spring能早期发现构造器循环依赖问题
使用示例如下:
java
// 1. 传统XML配置方式
<bean id="userService" class="com.example.UserService">
<constructor-arg ref="userRepository"/>
<constructor-arg value="defaultUser"/>
</bean>
// 2. Java配置类方式
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService(userRepository(), "defaultUser");
}
}
// 3. 注解方式(最常用)
@Service
public class UserService {
// final字段确保依赖在构造时就可用
private final UserRepository userRepository;
private final String defaultUser;
public UserService(UserRepository userRepository,
@Value("${default.user}") String defaultUser) {
this.userRepository = userRepository;
this.defaultUser = defaultUser;
}
}
三种注入方式对比
| 特性 | 字段注入 | Setter注入 | 构造器注入 |
|---|---|---|---|
| 不可变性 | ❌ 不支持 | ❌ 不支持 | ✅ 支持 |
| 循环依赖检测 | ⚠️ 运行时检测 | ⚠️ 运行时检测 | ✅ 早期检测 |
| 测试友好性 | ❌ 困难 | ✅ 良好 | ✅ 优秀 |
| 代码简洁性 | ✅ 优秀 | ✅ 良好 | ⚠️ 一般 |
| 明确依赖关系 | ❌ 隐蔽 | ✅ 明确 | ✅ 明确 |
| Spring官方推荐 | ❌ 不推荐 | ⚠️ 特定场景 | ✅ 推荐 |
为什么推荐构造器注入,而不是@Autowired
1、在对象的生命周期里,**构造方法永远优先于依赖注入。**这意味着在构造方法执行时,依赖的bean还没有被注入,因此如果在构造方法中使用了依赖的bean,会导致NullPointerException)。如果使用构造器注入,那么依赖的bean会在调用构造方法之前由Spring容器准备好,并作为参数传入,因此在构造方法中可以使用这些依赖。
2、@Autowired 采用三级缓存会掩盖 循环依赖的问题,而构造方法会直接暴露出来
3、测试过程中,@Autowired注入方式通常需要Mock注入的方式,构造器注入易于测试
注入实现方式示例
混合使用策略
java
@Component
public class MixedInjectionService {
// 必需依赖:构造器注入
private final RequiredService requiredService;
private final ConfigProperties config;
// 可选依赖:Setter注入
private OptionalService optionalService;
public MixedInjectionService(RequiredService requiredService,
ConfigProperties config) {
this.requiredService = requiredService;
this.config = config;
}
@Autowired(required = false)
public void setOptionalService(OptionalService optionalService) {
this.optionalService = optionalService;
}
}
使用Lombok简化注入
java
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final EmailService emailService;
// Lombok自动生成包含所有final字段的构造器
// 无需手动编写构造器代码
}