在 Java 里,依赖注入(Dependency Injection,简称 DI)是一种设计模式,它能让对象之间的依赖关系从代码内部转移到外部配置,从而提升代码的可测试性、可维护性和可扩展性。常见的依赖注入方式有基于字段的依赖注入、基于 setter 方法的依赖注入以及基于构造函数的依赖注入,下面分别介绍这三种方式以及其优缺点。
依赖注入方式
三种依赖注入形式介绍
1. 基于字段的依赖注入
基于字段的依赖注入是指直接通过反射机制将依赖对象赋值给目标对象的字段。这种方式简洁直观,不过会降低代码的可测试性,因为无法在不使用反射的情况下为字段赋值。
示例代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
// 定义一个服务接口
interface MessageService {
String getMessage();
}
// 实现服务接口
@Service
class EmailService implements MessageService {
@Override
public String getMessage() {
return "This is an email message.";
}
}
// 使用基于字段的依赖注入
@Component
class UserService {
@Autowired
private MessageService messageService;
public String sendMessage() {
return messageService.getMessage();
}
}
代码解释
@Autowired
注解会让 Spring 框架自动查找合适的MessageService
实现类,并将其注入到UserService
的messageService
字段中。- 在
UserService
类里,messageService
字段被直接注入,无需通过构造函数或者 setter 方法。
2. 基于 setter 方法的依赖注入
基于 setter 方法的依赖注入是指通过调用目标对象的 setter 方法来注入依赖对象。这种方式能让对象在创建后再进行依赖注入,并且可在 setter 方法中添加额外的逻辑。
示例代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
// 定义一个服务接口
interface MessageService {
String getMessage();
}
// 实现服务接口
@Service
class EmailService implements MessageService {
@Override
public String getMessage() {
return "This is an email message.";
}
}
// 使用基于 setter 方法的依赖注入
@Component
class UserService {
private MessageService messageService;
@Autowired
public void setMessageService(MessageService messageService) {
this.messageService = messageService;
}
public String sendMessage() {
return messageService.getMessage();
}
}
代码解释
- 对于基于 setter 方法的依赖注入,Spring 容器在创建
UserService
实例后,会扫描带有@Autowired
注解的setter
方法。当找到这样的方法时,Spring 会在容器中查找与setter
方法参数类型(同样是MessageService
)匹配的 Bean 实例。一旦找到,就会调用该setter
方法,并将找到的 Bean 实例作为参数传入,从而完成依赖注入。 - 在
UserService
类中,依赖对象通过setMessageService
方法进行注入。
3. 基于构造函数的依赖注入
基于构造函数的依赖注入是指在创建目标对象时,通过构造函数将依赖对象传递进去。这种方式能确保对象在创建时就具备所有必需的依赖,使得对象在创建后处于可用状态。
示例代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// 定义一个服务接口
interface MessageService {
String getMessage();
}
// 实现服务接口
@Service
class EmailService implements MessageService {
@Override
public String getMessage() {
return "This is an email message.";
}
}
// 使用基于构造函数的依赖注入
@Component
class UserService {
private final MessageService messageService;
public UserService(MessageService messageService) {
this.messageService = messageService;
}
public String sendMessage() {
return messageService.getMessage();
}
}
代码解释
- 当 Spring 容器创建
UserService
实例时,会扫描带有@Autowired
注解的构造函数。Spring 会在容器中查找与构造函数参数类型(这里是MessageService
)匹配的 Bean 实例。如果找到合适的 Bean,就会将其作为参数传递给构造函数,从而完成依赖注入。在这个例子中,MessageService
的实例会在UserService
对象创建时被注入,确保UserService
对象在创建完成后就可以直接使用MessageService
。 - 在
UserService
类中,messageService
字段被声明为final
,这表明该字段在对象创建后不能再被修改。
存在问题:
1.代码书写问题。随着需要注入的类越来越多,使用基于构造函数的依赖注入的构造方法可能需要很多的参数。这使得代码看起来十分的冗余且难以管理。为了简单实现,可以使用使用Lombok的注解@AllArgsConstructor来自动生成有参构造函数
。
2.注入的依赖失效问题。当 MessageService
在其他类中也是一个Bean的话,此时在当前MessageService
上加上 @Qualifier("messageService")
是无法注入当前的Bean的。需要在构造函数的里面进行声明。但是如果使用Lombok的注解默认是不用写构造函数的,那也也就无法进行@Qualifier
的声明。此时需要将@AllArgsConstructor
换成@RequiredArgsConstructor
。该注解会给声明了final的属性进行构造函数。
示例代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// 使用基于构造函数的依赖注入,需要注入很多的参数
@Service
@AllArgsConstructor//使用Lombok的构造函数
class UserService {
//需要注入多个Bean
private final MessageService messageService;
private final MessageService2 messageService2;
private final MessageService3 messageService3;
//随着注入的依赖越多,入参也就越来越多。 可以使用Lombok的 @RequiredArgsConstructor注解,就可以省去对构造函数的书写
/*public UserService(MessageService messageService,MessageService2 messageService2,MessageService3 messageService3) {
this.messageService = messageService;
this.messageService2 = messageService2;
this.messageService3 = messageService3;
}*/
}
// 使用基于构造函数的依赖注入,需要注入很多的参数
@Service
@RequiredArgsConstructor //使用Lombok的构造函数
class UserService {
//不带final,需要手动生成构造函数
private MessageService messageService;
//可以装配自己定义的messageService
public UserService(@Qualifier("messageService")MessageService messageService) {
this.messageService = messageService;
}
}
三种注入方式的比较
基于字段的依赖注入
优点
- 代码简洁:无需编写额外的构造函数或 setter 方法,只需使用注解标记字段,就能完成依赖注入,使代码看起来更加简洁明了。
- 易于使用:对于开发人员来说,直接在字段上进行注入操作,操作简单,降低了代码的编写难度。
缺点
- 可测试性差:由于依赖是通过反射直接注入到字段中的,在进行单元测试时,无法直接通过构造函数或 setter 方法为字段赋值,需要使用反射来模拟依赖注入,增加了测试的复杂性。
- 违反单一职责原则:使用字段注入时,类的依赖关系不明确,可能导致类的职责不清晰,违反了单一职责原则。
- 对象状态不完整:在对象创建时,依赖字段可能未被初始化,对象可能处于不完整的状态,这可能会在后续使用过程中引发空指针异常等问题。
基于 setter 方法的依赖注入
优点
- 灵活性高:允许对象在创建后再进行依赖注入,可在对象的生命周期内动态地改变依赖关系。
- 可添加额外逻辑:可以在 setter 方法中添加额外的逻辑,如参数验证、日志记录等,增强代码的健壮性。
- 利于单元测试:在单元测试中,可以方便地调用 setter 方法为对象注入依赖,提高了代码的可测试性。
缺点
- 对象状态不明确:由于依赖是在对象创建后通过 setter 方法注入的,对象在创建时可能处于不完整的状态,需要在使用前确保所有必要的依赖都已注入。
- 代码冗余:需要为每个依赖字段编写对应的 setter 方法,增加了代码的冗余度。
基于构造函数的依赖注入
优点
- 依赖关系明确:通过构造函数注入依赖,使得类的依赖关系在创建对象时就一目了然,增强了代码的可读性和可维护性。
- 对象完整性:确保对象在创建时就具备所有必需的依赖,使对象在创建后处于可用状态,避免了空指针异常等问题。
- 符合设计原则:遵循了依赖倒置原则和单一职责原则,提高了代码的可扩展性和可测试性。
- 线程安全:由于依赖在对象创建时就已经确定,不会在对象的生命周期内发生变化,因此在多线程环境下更加安全。
缺点
- 代码复杂度增加:当类的依赖较多时,构造函数的参数列表会变得很长,增加了代码的复杂度。
- 不够灵活:一旦对象创建完成,依赖关系就无法再改变,不够灵活。如果需要动态改变依赖关系,需要重新创建对象。
推荐使用方式
一般情况下,优先推荐使用基于构造函数的依赖注入
。因为它能确保对象的完整性和可测试性,符合设计原则,在多线程环境下也更安全。但在某些特殊场景下,如需要动态修改依赖关系时,可以结合使用基于 setter 方法的依赖注入
。而基于字段的依赖注入由于其可测试性和可维护性较差,应尽量避免使用 ,除非在一些简单的、对可测试性要求不高的场景中。