1 简介
依赖注入是Spring 框架提供的核心功能之一,也是开发人员使用Spring Boot框架的基本手段。通过控制反转(IoC)机制获得所需的各种Bean。这中间存在一些最佳实践和值得注意的开发技巧。选择合适的依赖注入类型可以提升系统性能,解决因为使用不到导致的Bean注入问题。
试想一下,在开发过程中,如果两个Bean之间存在循环依赖关系,那么选择的依赖注入类型是否合适就直接决定了Bean能否创建成功。再比方说,如果想要在Spring容器中注入大量的Bean,那么采用不合适的注入类型可能会极大影响应用程序的启动性能。所以,本篇以及后续几个篇章就从Spring依赖注入的三种基本类型开始讨论,分析如何选择正确的依赖注入类型,以及如何使用依赖注入进行实战。
2 Spring依赖注入类型
Spring为开发人员提供了3种不同的依赖注入类型,分别是字段注入、构造器注入和Setter方法注入。现在假设有一个HealthRecordService接口以及它的实现类。代码如下:
java
public interface HealthRecordService {
public void recordUserHealthData();
}
public class HealthRecordServiceImpl implements HealthRecordService {
@Override
public void recordUserHealthData() {
System.out.println("HealthRecordService 已被执行");
}
}
基于上述 HealthRecordServiceImpl 实现类,下面来具体讨论如何在Spring中完成该类的注入,并分析各种注入类型的优缺点。
3 字段注入
首先来看字段注入。想要在一个类中通过字段的形式注入某个对象,就可以使用这个方式,示例代码如下:
java
public class ClientServer {
@Autowired
private HealthRecordService healthRecordService;
public void recordUserHealthData(){
healthRecordService.recordUserHealthData();
}
}
可以看到,通过@Autowired 注解,字段注入的实现方式非常简单而直接,代码的可读性也很高。事实上,字段注入是三种注入方式中最常用,也是最容易使用的一种,但它也是三种注入方式中最应该避免使用的。在IDEA里,可能会遇到"Field injection is not recommended" 这个提示,不建议使用字段注入。针对这一点,来分析一下原因。
1、字段注入的最大问题是对象的外部可见性。正如在前面ClientService类中,通过定义一个私有变量healthRecordService来注入该接口的实例。显然,这个实例只能在ClientService类中被访问,脱离了容器环境就无法进行访问 ,如下面代码:
java
private ClientServer clientServer = new ClientServer();
public void test() {
clientServer.recordUserHealthData();
}
执行这段代码的结果就是抛出一个空指针异常,原因是无法在ClientService的外部实例化HealthRecordService对象。采用字段注入,类与容器的耦合度过高,无法脱离容器来使用目标对象。如果编写测试用例来验证ClientService类的正确性,那么想要使用HealthRecordService对象,就只能通过反射的方式,这种做法实际上不符合JavaBean开发规范,而且可能导致一直无法发现空指针异常。
2、 字段注入的第二个问题是可能导致潜在的循环依赖。所谓循环依赖,就是两个类之间互相注入,代码如下:
java
public class ClassA {
@Autowired
private ClassB classB;
}
public class ClassB {
@Autowired
private ClassA classA;
}
显然,这里的ClassA和ClassB发生了循环依赖。上述代码在Spring中是合法的,容器启动时不汇报任何错误,而只有在使用到某个具体的ClassA或ClassB时才会报错。
3、字段注入的第三个问题是无法设置需要注入的对象为final,也无法注入那些不可变对象,因为这些字段必须在类实例化时进行实例化。
基于以上三点,IDEA以及Spring官方都不推荐开发使用字段注入这种方式。那么,推荐的注入方式是哪种呢?答案是构造器注入。
4 构造器注入
构造器注入的形式也很简单,就是通过类的构造函数来完成对象注入,示例代码如下:
java
public class ClientService1 {
private HealthRecordService healthRecordService;
@Autowired
public ClientService1(HealthRecordService healthRecordService){
this.healthRecordService = healthRecordService;
}
public void recordUserHealthData(){
healthRecordService.recordUserHealthData();
}
}
可以看到构造器注入能解决对象外部可见性的问题,因为HealthRecordService是通过ClientService1构造函数进行注入的,所以可以脱离ClientService而独立存在。
官方文档对构造器注入特性介绍如下:构造器注入能够保证注入的组件不可变,并且确保需要的依赖不为空。这里的组件不可变就意味着可以使用final关键词来修饰所依赖的对象,而依赖不为空是指所传入的依赖对象肯定是一个实例对象,从而避免出现空指针异常。
同时,基于构造器注入,也来讨论前面介绍的ClassA和ClassB之间的循环依赖关系,实现代码如下:
java
public class ClassA {
private ClassB classB;
@Autowired
public ClassA(ClassB classB){
this.classB = classB;
}
}
public class ClassB {
private ClassA classA;
@Autowired
public ClassB(ClassA classA){
this.classA = classA;
}
}
一旦采用构造器注入,在Spring项目启动的时候,就会抛出一个循环依赖异常,从而避免使用循环依赖。
通过上述分析,可以看到字段注入的三个大问题都可以通过使用构造器注入的方式来解决。但是,构造器注入的显著问题就是当构造函数中存在较多依赖对象时,大量的构造器参数会让代码冗长。这时候就引入Setter方法注入。
5 Setter注入
先来看一下Setter方法注入的实现代码:
java
public class ClientService {
private HealthRecordService healthRecordService;
@Autowired
public void setHealthRecordService(HealthRecordService healthRecordService){
this.healthRecordService = healthRecordService;
}
public void recordUserHealthData(){
healthRecordService.recordUserHealthData();
}
}
Setter方法注入和构造器注入看上去有点类似,但它比构造器函数更具有可读性,因为我们可以把多个依赖对象分别通过Setter方法逐一进行注入。而且,Setter方法注入对于非强制依赖项注入很有用,可以有选择地注入一部分依赖对象。
另外,Setter方法可以很好的解决应用程序中的循环依赖问题,如下面代码是可以正确执行的:
java
public class ClassA {
private ClassB classB;
@Autowired
public void setClassB(ClassB classB){
this.classB = classB;
}
}
public class ClassB {
private ClassA classA;
@Autowired
public void setClassA(ClassA classA){
this.classA = classA;
}
}
请注意,上述代码能够正确执行的前提是ClassA和ClassB的作用域都是Singleton。最后,通过Setter注入可以对依赖对象进行多次重复注入,这在构造器注入中是无法实现的。
作为总结,用一句话概括Spring所提供的三种依赖注入类型:构造器注入适合于强制对象注入;Setter注入适用于可选对戏注入;而字段注入是应该避免使用的,因为对象无法脱离容器而独立运行。