目录
1.简介
2.了解两种注入方式的全过程
2.1 Autowired字段注入
2.2 构造函数注入
3.使用autowired注解注入有以下问题
3.1空指针异常
3.2测试不友好
4.使用Lombok去简化构造函数注入的臃肿代码
5.小结
5.1注解注入
5.2构造函数注入
1.简介
使用Spring开发时,我们通常有两种依赖注入的方式,基于注解@Autowired的依赖注入和基于构造函数的依赖注入。
用IDEA开发过程中,如果使用@Autowired注入,通常会有如下警告:
这真的是我们代码写错了吗?其实不然,只是用AutoWired注解去进行注入会产生一些隐形问题。
2.了解两种注入方式的全过程
在Spring框架中,依赖注入(DI)是一种核心功能,它允许对象通过构造函数、setter方法或字段直接定义其依赖关系。这里,我们专注于两种常见的注入方式:字段注入(通过@Autowired注解)和构造函数注入。理解这两种注入方式的全过程对于编写可维护、可测试和健壮的Spring应用至关重要。
2.1 Autowired字段注入
字段注入是Spring允许的依赖注入的简便方式,它直接在类的字段上使用@Autowired注解。这种方式的注入流程相对简单直接:
注入全过程
- 启动阶段:当Spring应用启动时,Spring容器开始创建并管理bean。它扫描项目中的类,查找带有@Component、@Service、@Controller等注解的类,并为这些类创建bean。
- 依赖查找:在字段上使用@Autowired时,Spring容器在运行时自动检测系统中可用的匹配该字段类型的bean。
- 自动注入:容器将找到的bean直接注入到被@Autowired标记的字段中。这一过程通常在bean的构造函数执行之后发生,意味着新创建的对象的字段将在稍后的时间点被Spring自动填充。
- 后处理:一旦所有的字段被注入后,bean才被认为是完全初始化的,并且随后会触发任何回调方法,如标记有@PostConstruct的方法。
优点与缺点
优点:
- 简单易用:直接在字段上标注@Autowired,无需额外的构造函数或setter方法。
- 代码简洁:减少了模板代码,特别是在依赖数量不多时。
缺点:
- 不支持不可变性:由于字段是在对象创建后注入,不能声明为final。
- 降低可测试性:不使用Spring容器时,例如在单元测试中,很难替换依赖项。
- 违反了Spring推荐的最佳实践:构造函数注入是推荐的方式,因为它支持不可变性,并且依赖在使用前总是被初始化。
2.2 构造函数注入
构造函数注入是将依赖作为参数传递给类的构造函数。Spring容器使用这些参数来创建bean实例。
注入全过程
- 启动阶段:与字段注入相同,Spring首先创建所有的bean定义,并扫描标注了Spring注解的类。
- 依赖解析:对于构造函数注入,当创建类的实例时,Spring容器查看类的构造函数参数,确定需要注入哪些依赖。
- 依赖注入:Spring容器然后实例化这些依赖(如果它们尚未创建)并通过构造函数注入到正在创建的bean中。这确保了在对象完全构造之前所有必需的依赖都已经提供。
- 对象初始化:一旦所有构造函数参数被注入,对象被实例化,并且所有设置方法和回调都被调用。
优点与缺点
优点:
- 支持不可变性:依赖可以被声明为final,确保了一旦构造对象后不会改变。
- 促进了更好的软件设计:构造函数注入强制要求依赖在构造对象时必须存在,从而保证了bean的依赖不会是null。
- 提高可测试性:容易在测试中通过构造函数替换依赖,特别是使用Mock对象时。
缺点:
- 构造函数臃肿:如果一个类有多个依赖,构造函数可能会变得很长,这可能使代码更难阅读和维护。
- 可能需要更多配置:特别是在存在多个构造函数或需要特定的配置来选择适当构造函数时。
3.使用autowired注解注入有以下问题
3.1空指针异常
我们来看一下一个简单例子
public class Car {@Autowiredprivate Wheel wheel;public void run() {wheel.roll();}
}
假设测试代码如下,会出现以下异常:
Car car = new Car();
car.run();// -> NullPointerException
出现这种问题的关键在于,Car允许创建无状态的对象,也就是说在构建Car时允许Wheel为空。
我们来看看使用构造函数注入会不会出现这个问题
public class Car {private final Wheel wheel;public Car(Wheel wheel) {Assert.notNull(wheel,"Wheel must not be null");this.wheel = wheel;}public void run() {wheel.roll();}}
这样我们就有如下优点:
在创建Car对象的时候,强制依赖Wheel对象,确保创建Car对象时每个对象都是有效状态。
构造器中可以添加对象初始化的校验逻辑
可以清楚的区分对象是通过setter方法注入的(非final对象)还是通过强制依赖注入的(final对象)
构造注入代码变得臃肿?
或许有的读者可能会说,构造注入的话,如果依赖的对象很多,构造器参数就会很多,显得代码很臃肿。这种情况的话,就要考虑这个类是符合足单一职责原则了,将这个类拆分为多个类。
而且使用@Autowired的自动装配会让依赖对象变得很容易,随着项目的迭代,自动注入的对象可能会变得很多,但是使用构造注入,构造器就会变得很臃肿,提醒你代码里有bad smell了,需要拆分或重构代码了。
还有一个问题是@Autowired注入的对象无法使用final关键字,因为final对象必须在构造器中初始化。
3.2测试不友好
当我们使用注解去注入时,确实会十分方便和节省许多代码的编写,但是这样的话我们的单元测试该怎么写呢?
Wheel wheel = Mock(Wheel);Car car = new Car(wheel);car.run();
通过反射注入到Car对象里,我们的单元测试代码就会显得很繁琐,或者在Car对象里提供一个Wheel的setter方法?这样代码不是很优雅。
如果是构造注入,单元测试就会变成如下:
Wheel wheel = Mock(Wheel);Car car = new Car(wheel);car.run();
单元测试代码就会变得很优雅,而且在后续的开发中,如果Car对象添加了强制依赖的Tank对象,单元测试也不会出现没有设置的强制依赖项。
Spring 的DI设计模式,是将依赖关系的创建和类本身分离,将依赖关系创建的职责交给了类注入器做,允许程序设计的松耦合,并遵循单一职责原则和依赖反转原则。因此使用@Autowired自动装配的字段在Spring容器之外无法使用(不包含通过反射设置对象的方式)。
构造注入可以在受影响的类中轻松表明对象的依赖关系,但是@Autowired的自动装配其实对外隐藏了这些依赖关系,需要到对应的类中查看代码才能明确依赖。
4.使用Lombok去简化构造函数注入的臃肿代码
Lombok的依赖如下:
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
简化之后代码如下:
5.小结
最后我们来总结一下两者的优缺点
5.1注解注入
++ 写更少的代码
-- 代码变得不安全
-- 单元测试会比较复杂
-- 无法使用fianl对象
-- 违反单一职责原则变得很容易
-- 对受影响的类隐藏自己的依赖关系
5.2构造函数注入
++ 更安全的代码
++ 测试友好
++ 依赖添加代价较高,显式的表明代码的bad smell
++ 在受影响的类中显式的表明依赖关系
-- 需要写更多的业务代码(可以通过Lombok解决)