Skip to content

依赖注入方式对比

Spring Framework提供了多种依赖注入方式,每种方式都有其适用场景和优缺点。本文将对比不同的依赖注入方式,以帮助开发者选择最适合的方式。

三种主要的依赖注入方式

Spring主要支持以下三种依赖注入方式:

  1. 构造器注入(Constructor Injection)
  2. Setter方法注入(Setter Injection)
  3. 字段注入(Field Injection)

构造器注入

java
@Service
public class UserServiceImpl implements UserService {
    
    private final UserRepository userRepository;
    private final EmailService emailService;
    
    @Autowired // 在Spring 4.3+中,单构造函数可以省略@Autowired
    public UserServiceImpl(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
}

XML配置:

xml
<bean id="userService" class="com.example.service.UserServiceImpl">
    <constructor-arg ref="userRepository"/>
    <constructor-arg ref="emailService"/>
</bean>

优点

  1. 不可变性:依赖一旦被注入,就不可更改,促进了不可变设计
  2. 必要依赖明确:构造函数清楚地表明了所有必需的依赖
  3. 完全初始化:对象创建后即可使用,不会处于部分初始化状态
  4. 促进测试:更容易进行单元测试,测试代码可以显式地提供所有依赖
  5. 循环依赖检测:在启动时就能检测到循环依赖问题

缺点

  1. 构造器臃肿:当依赖项较多时,构造函数会变得臃肿
  2. 循环依赖:无法解决某些循环依赖问题(Spring使用提前曝光机制可解决setter注入的循环依赖)

Setter方法注入

java
@Service
public class UserServiceImpl implements UserService {
    
    private UserRepository userRepository;
    private EmailService emailService;
    
    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Autowired
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
}

XML配置:

xml
<bean id="userService" class="com.example.service.UserServiceImpl">
    <property name="userRepository" ref="userRepository"/>
    <property name="emailService" ref="emailService"/>
</bean>

优点

  1. 灵活性:可以在任何时候更改依赖
  2. 可选依赖:更适合处理可选的依赖
  3. 循环依赖:能够解决某些循环依赖问题
  4. 可读性:当依赖较多时,setter方法可能比长构造函数更可读

缺点

  1. 部分初始化:对象可能处于部分初始化状态
  2. 不能保证依赖注入:没有强制机制确保依赖被注入
  3. 线程安全性:在多线程环境下可能引发问题,因为对象在构造后可能被修改

字段注入

java
@Service
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private EmailService emailService;
}

优点

  1. 简洁:代码最为简洁,不需要构造函数或setter方法
  2. 减少样板代码:无需编写注入方法

缺点

  1. 不可测试性:难以在单元测试中注入mock对象(需要使用反射)
  2. 隐藏依赖:依赖不明确,需要查看字段才能了解
  3. 与容器耦合:强依赖于IoC容器,无法在容器外使用
  4. 不能用于final字段:无法注入到final字段中

@Resource与@Autowired对比

除了注入方式的选择,Spring还提供了不同的注入注解:

  1. @Autowired:Spring提供的注解,默认按类型注入
  2. @Resource:JSR-250提供的注解,默认按名称注入

@Autowired特点

java
@Autowired
private UserRepository userRepository; // 按类型注入

@Autowired
@Qualifier("mainUserRepository")
private UserRepository userRepository; // 结合@Qualifier按名称注入
  1. 默认按类型(Type)匹配
  2. 结合@Qualifier可按名称注入
  3. 可以注入Optional类型
  4. 可以注入集合、数组、Map等
  5. Spring特有的注解

@Resource特点

java
@Resource
private UserRepository userRepository; // 默认按字段名称查找

@Resource(name = "mainUserRepository")
private UserRepository userRepository; // 显式指定名称
  1. 默认按名称(Name)匹配,如果找不到再按类型匹配
  2. JSR-250标准的一部分,不是Spring特有的
  3. 可以通过name属性显式指定Bean名称

实践推荐

根据Spring核心开发团队和最佳实践的建议:

  1. 首选构造器注入:特别是对必要依赖,构造器注入是最推荐的方式
  2. Setter注入用于可选依赖:当某些依赖是可选的时候,可以使用setter注入
  3. 避免字段注入:虽然简洁,但引入了上述问题,不推荐在生产代码中使用
  4. 选择一种注解风格并保持一致:团队应该统一使用@Autowired或@Resource

示例:混合使用注入方式

实际应用中,可以混合使用不同的注入方式以获得最佳效果:

java
@Service
public class UserServiceImpl implements UserService {
    
    // 必要的依赖使用构造器注入
    private final UserRepository userRepository;
    private final SecurityService securityService;
    
    // 可选依赖使用setter注入
    private EmailService emailService;
    private AuditService auditService;
    
    @Autowired
    public UserServiceImpl(UserRepository userRepository, SecurityService securityService) {
        this.userRepository = userRepository;
        this.securityService = securityService;
    }
    
    @Autowired
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
    
    @Autowired(required = false)
    public void setAuditService(AuditService auditService) {
        this.auditService = auditService;
    }
}

总结

注入方式推荐用途主要优点主要缺点
构造器注入必要依赖不可变性、完全初始化处理循环依赖问题有限
Setter注入可选依赖灵活性、可解决循环依赖部分初始化状态
字段注入简单原型或测试代码简洁难以测试、隐藏依赖

选择适当的依赖注入方式不仅会影响代码的质量,还会影响应用程序的可维护性和可测试性。在大多数情况下,优先考虑构造器注入,并根据特定需求选择性地使用其他方式。