写点什么

深入剖析 spring 核心容器依赖注入类型和原理:Spring 依赖注入类型

  • 2023-06-16
    湖南
  • 本文字数:3407 字

    阅读完需:约 11 分钟

核心容器

本部分介绍在使用 Spring 容器时应该关注的最佳实践,并讨论 Spring 内核最重要的两个功能特性,即依赖注入和面向切面。我们将从功能的使用和原理的剖析两方面来展开讨论。


通过这一部分的学习,读者可以在日常使用 Spring 容器的过程中明确需要考虑的开发技巧,并掌握依赖注入的不同类型是如何影响框架的使用方式的,以及如何正确选择合适的 AOP 代理机制。

依赖注入类型和原理

依赖注入是 Spring Framework 提供的核心功能之一,也是开发人员使用 Spring Boot 框架的基本手段。

我们通过控制反转(Inversion of Control,IoC)机制获取所需的各种 Bean。这中间存在一些最佳实践和值得注意的开发技巧。选择合适的依赖注入类型可以提升系统性能,解决因为使用不当导致的 Bean 注入问题。


试想一下,在开发过程中,如果两个 Bean 之间存在循环依赖关系,那么选择的依赖注入类型是否合适就直接决定了 Bean 能否创建成功。再比方说,如果想要在 Spring 容器中注入大量的 Bean,那么采用不合适的注入类型可能会极大地影响应用程序的启动性能。


所以,本章就从 Spring 依赖注入的三种基本类型开始讨论,分析如何选择正确的依赖注入类型,以及如何使用依赖注入进行实战。


另外,不同的依赖注入类型还涉及 Bean 的作用域、生命周期等主题,这些主题都值得进一步分析,从而把握 Spring IoC 容器的运行原理。因此,本章还将从原理出发,深入剖析 Spring 中 Bean 的注入实现机制。

Spring 依赖注入类型

在前面,我们已经介绍了依赖注入的基本概念。Spring 为开发人员提供了三种不同的依赖注入类型,分别是字段注入、构造器注入和 Setter 方法注入。现在,假设我们有一个 HealthRecordService 接口以及它的实现类,如下所示:

public interface HealthRecordService {	public void recordUserHealthData();}public class HealthRecordServiceImpl implements HealthRecordService {	@Override	public void recordUserHealthData () {		System.out.println("HealthRecordService has been called.");	}}
复制代码

基于上述 HealthRecordServiceImpl 实现类,下面我们来具体讨论如何在 Spring 中完成该类的注入,并分析各种注入类型的优缺点。

字段注入

首先,我们来看字段注入。想要在一个类中通过字段的形式注入某个对象,就可以使用这个方式,示例代码如下所示:

public class ClientService {	@Autowired	private HealthRecordService healthRecordService;  public void recordUserHealthData() {		healthRecordService.recordUserHealthData();	}}
复制代码

可以看到,通过 @Autowired 注解,字段注入的实现方式非常简单而直接,代码的可读性也很高。事实上,字段注入是三种注入方式中最常用,也是最容易使用的一种,但它也是三种注入方式中最应该避免使用的。如果你使用过 IDEA,可能会遇到“Field injection is not recommended”这个提示,告诉你不建议使用字段注入。针对这一点,你可能会觉得很诧异。我们来分析一下原因。


  1. 字段注入的最大问题是对象的外部可见性。正如在前面的 ClientService 类中,我们通过定义一个私有变量 healthRecordService 来注入该接口的实例。显然,这个实例只能在 ClientService 类中被访问,脱离了容器环境就无法进行访问,如下所示:

ClientService clientService = new ClientService();clientService.recordUserHealthData();
复制代码

执行这段代码的结果就是抛出一个 NullPointerException 空指针异常,原因是无法在 ClientService 的外部实例化 HealthRecordService 对象。采用字段注入,类与容器的耦合度过高,我们无法脱离容器来使用目标对象。如果我们编写测试用例来验证 ClientService 类的正确性,那么想要使用 HealthRecordService 对象,就只能通过反射的方式,这种做法实际上是不符合 JavaBean 开发规范的,而且可能导致一直无法发现空指针异常。


  1. 字段注入的第二个问题是可能导致潜在的循环依赖。所谓循环依赖,就是两个类之间互相进行注入,示例代码如下所示:

public class ClassA {	@Autowired	private ClassB classB;}public class ClassB {	@Autowired	private ClassA classA;}
复制代码

显然,这里的 ClassA 和 ClassB 发生了循环依赖。上述代码在 Spring 中是合法的,容器启动时并不会报任何错误,而只有在使用到具体某个 ClassA 或 ClassB 时才会报错。


  1. 字段注入的第三个问题是我们无法设置需要注入的对象为 final,也无法注入那些不可变对象,这是因为字段必须在类实例化时进行实例化。


基于以上三点,IDEA 以及 Spring 官方都不推荐开发人员使用字段注入这种方式。那么,我们推荐的注入方式是哪种呢?答案是构造器注入。

构造器注入

构造器注入的形式也很简单,就是通过类的构造函数来完成对象的注入,示例代码如下所示:

public class ClientService {	private HealthRecordService healthRecordService;	@Autowired	public ClientService(HealthRecordService healthRecordService) {		this.healthRecordService = healthRecordService;	}	public void recordUserHealthData() {		healthRecordService.recordUserHealthData();	}}
复制代码

可以看到构造器注入能解决对象外部可见性的问题,因为 HealthRecordService 是通过 ClientService 构造函数进行注入的,所以势必可以脱离 ClientService 而独立存在。


我们进一步引用 Spring 官方文档来解释构造器注入的特性:

The Spring team generally advocates constructor injection as it

enables one to implement application components as immutable

objects and to ensure that required dependencies are not null.

Furthermore constructor-injected components are always returned to

client (calling) code in a fully initialized state.


这段话的核心意思在于:构造器注入能够保证注入的组件不可变,并且确保需要的依赖不为空。这里的组件不可变就意味着我们可以使用 final 关键词来修饰所依赖的对象,而依赖不为空是指所传入的依赖对象肯定是一个实例对象,从而避免出现空指针异常。


同时,基于构造器注入,我们也来讨论前面介绍的 ClassA 和 ClassB 之间的循环依赖关系,实现方式如下所示:

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 方法注入。

Setter 方法注入

先来看一下 Setter 方法注入的实现代码,如下所示:

public class ClientService {	private HealthRecordService healthRecordService;	@Autowired  public void setHealthRecordService(HealthRecordService healthRecordService) {		this.healthRecordService = healthRecordService;	}	public void recordUserHealthData() {		healthRecordService.recordUserHealthData();	}}
复制代码

Setter 方法注入和构造器注入看上去有点类似,但它比构造函数更具可读性,因为我们可以把多个依赖对象分别通过 Setter 方法逐一进行注入。而且,Setter 方法注入对于非强制依赖项注入很有用,我们可以有选择地注入一部分依赖对象。换句话说,该方法可以实现按需注入,帮助我们只在需要时注入依赖关系。

另外,Setter 方法可以很好地解决应用程序中的循环依赖问题,如下所示的代码是可以正确执行的。

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。关于 Spring 中的对象作用域,我们在本章后续会具体讨论。


最后,通过 Setter 注入可以对依赖对象进行多次重复注入,这在构造器注入中是无法实现的。


作为总结,我们用一句话来概括 Spring 所提供的三种依赖注入类型:构造器注入适合于强制对象注入;Setter 注入适合于可选对象注入;而字段注入是应该被避免使用的,因为对象无法脱离容器而独立运行。

用户头像

加VX:bjmsb02 凭截图即可获取 2020-06-14 加入

公众号:程序员高级码农

评论

发布
暂无评论
深入剖析spring核心容器依赖注入类型和原理:Spring依赖注入类型_互联网架构师小马_InfoQ写作社区