作为 Java程序员都知道,没有依赖注入,Spring 框架就是无法实现的,那么,在 Spring 框架中,常见的依赖注入方式有哪些呢?我们该如何选择?这篇文章来聊一聊。
从整体上来看,Spring 的依赖注入有 4种方式:
- 构造器注入
- setter 方法注入
- 字段注入
下面,我们将分别分析它们的原理,以及它们的优缺点。
1. 构造器注入
构造器注入(Constructor Injection)是指通过构造函数传入依赖的对象。Spring 容器在创建 bean 时会调用它的构造函数,并将所需的依赖项作为参数传入。
如下示例展示如何通过构造器注入 bean。
@Service public class AService { private final BService bService; @Autowired public AService(BService bService) { this.bService = bService; } }
如果类只有一个构造方法,那么 @Autowired
注解可以省略;如果类中有多个构造方法,那么需要添加上 @Autowired
来明确指定到底使用哪个构造方法。
优点:
- 强制性依赖性:在对象创建时,所有必要的依赖项都必须提供,减少了在运行时出现空指针的风险。
- 不可变性:被注入的依赖可以声明为 final,使得一旦初始化后对象的状态不可更改,从而增强了对象的安全性。
- 便于单元测试:构造器参数很容易模拟(mock)或替代,方便测试。
缺点:
- 复杂性:如果一个类有很多依赖,则构造函数可能变得非常复杂,导致可读性差。
- 潜在的构造函数过载:当添加新的依赖时,可能需要重载多个构造函数,增加了维护成本。
构造器注入是工作中比较推荐的一种方式,因为它编译期行为,可以减少空指针。但是,如果一个类需要很多依赖,构造器注入会导致代码比较臃肿。
2. Setter 注入
Setter 注入(Setter Injection)是指通过 setter 方法注入依赖的对象。Spring 在创建 bean 后,通过调用 setter 方法来设置依赖项。
如下示例展示如何通过 Setter注入 bean。
@Service public class BService { AService aService; @Autowired public void setaService(AService aService) { this.aService = aService; } }
优点:
- 灵活性:可以选择性地注入依赖项,允许在对象创建后进行注入,适合可选的依赖。
- 清晰的配置:可以通过 setter 方法明确地配置并查看依赖关系。
缺点:
- 非强制性依赖:在对象创建后如果没有设置必需的依赖,可能导致运行时的空指针异常。
- 可变性:依赖可以在对象生命周期内被更改,可能导致不一致的状态。
3. 字段注入
属性注入是大家最为常见也是使用最多的一种注入方式了,代码如下:
字段注入(Field Injection)是直接将依赖注入到类的字段中,通常使用反射和注解。通常会 使用 Spring 提供的注解(如 @Autowired, @Inject, @Resource)。这种方式较为简洁,但是会有 NPE的风险。
如下示例展示如何通过字段注入 bean。
@Service public class BService { @Autowired AService aService; //... }
不过不知道小伙伴们有没有留意过,在 IDEA 里边,使用属性注入,会有一个警告:
不推荐属性注入!
原因我们后面讨论。
优点:
- 简洁性:代码量较少,使用反射和注解可自动完成依赖注入,易于理解。
- 方便快捷:无需编写构造函数或 setter 方法。
缺点:
- 不易于测试:由于字段是私有的,通常不容易替换依赖项进行测试。
- 依赖性不明显:依赖关系并不明确,影响代码的可读性和维护性。
- 不支持不可变性:不能将字段声明为 final,这可能导致不一致的状态。
4.实例注入方式大 PK
上面给大家列出来了三种注入方式,那么三种注入方式各自有何区别呢?
结合 Spring 官方文档,我们来分析下。
翻出了 12 年前的 Spring3.0 的文档(https://docs.spring.io/spring-framework/docs/3.0.x/reference/beans.html),里边有如下一段话:
我来简单翻译下(意译):
使用构造方法注入还是使用 set 方法注入?由于构造方法注入和 set 方法注入可以混合使用,因此,如果需要强制注入,我们可以使用构造方法注入的方式;如果是可选注入,则我们可以使用 set 方法注入的方式。当然,我们在 setter 上使用 @Required 注解可以让 set 方法注入也变为强制性注入。Spring 团队通常提倡 setter 注入,因为当属性特别多的时候,构造方法看起来会特别臃肿,特别是当属性是可选的时(属性可选意味着没必要通过构造方法注入)。Setter 方法注入还有一个好处就是可以使该类的属性可以在以后重新配置或重新注入。一些纯粹主义者喜欢基于构造函数的注入,这样意味着所有的属性都被初始化了,缺点则是对象变得不太适合重新配置和重新注入。另外在一些特殊的场景下,如一个第三方类要注入到 Spring 容器,但是该类没有提供 set 方法,那么此时你就只能使用构造方法注入了。
英文水平有限,大概翻译了下。小伙伴们重点看加粗部分,也就是说在 Spring3.0 时代,官方还是提倡 set 方法注入的。
不过从 Spring4.x 开始,官方就不推荐这种注入方式了,转而推荐构造器注入。
我们来看看 Spring4.x 的文档怎么说(https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#beans-setter-injection):
这段内容我就不一一翻译了,大家重点看第二段第一句:
The Spring team generally advocates constructor injection
这句话就是说 Spring 团队倡导通过构造方法完成注入。才一个大版本更新,Spring 咋就变了呢?别急,人家也给出用构造方法注入的理由,第二段翻译一下大概是这个意思:
通过构造方法注入的方式,能够保证注入的组件不可变,并且能够确保需要的依赖不为空。此外,构造方法注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态。
上面这段话主要说了三件事:
依赖不可变:这个好理解,通过构造方法注入依赖,在对象创建的时候就要注入依赖,一旦对象创建成功,以后就只能使用注入的依赖而无法修改了,这就是依赖不可变(通过 set 方法注入将来还能通过 set 方法修改)。
依赖不为空:通过构造方法注入的时候,会自动检查注入的对象是否为空,如果为空,则注入失败;如果不为空,才会注入成功。
完全初始化:由于获取到了依赖对象(这个依赖对象是初始化之后的),并且调用了要初始化组件的构造方法,因此最终拿到的就是完全初始化的对象了。
在 Spring3.0 文档中,官方说如果构造方法注入的话,属性太多可能会让代码变得非常臃肿,那么在 4.0 文档中,官方对这个说法也做了一些订正:如果用构造方法注入的时候,参数过多以至于代码过于臃肿,那么此时你需要考虑这个类的设计是否合理,这个类是否参杂了太多的其他无关功能,这个类是否做到了单一职责。
这是构造方法注入和 set 方法注入的问题,那么上面我们还提到不推荐属性注入,这又是咋回事呢?
属性注入其实有一个显而易见的缺点,那就是对于 IOC 容器以外的环境,除了使用反射来提供它需要的依赖之外,无法复用该实现类。因为该类没有提供该属性的 set 方法或者相应的构造方法来完成该属性的初始化。换言之,要是使用属性注入,那么你这个类就只能在 IOC 容器中使用,要是想自己 new 一下这个类的对象,那么相关的依赖无法完成注入
5. 总结
本文,我们分析了 Spring的 3种注入方式,在选择依赖注入方式时,我们应该根据具体情况和项目来决定。根据工作经验,建议如下:
- 构造器注入是最推荐的方式,特别是在需要强制性依赖和不可变性的场景下。
- 如果无法通过构造器注入,再选择 setter注入。
- 如果上述两者方式都不适用,字段注入则是最后的选择,虽然使用简单,但是容易产生NPE。