Spring Boot 中 Bean 的注入方式

HYF Lv3

Spring 中,Bean 是由 Spring 容器管理的对象。这些对象可以是应用中的任何对象,它们被 Spring 容器初始化、装配、管理和销毁。

Bean 的作用主要体现在它们可以通过依赖注入的方式,将一个 Bean 注入到另一个 Bean 中,从而实现松耦合和可重用性。通过 Spring 容器管理 Bean,开发人员可以避免手动管理对象的复杂性,并利用 Spring 提供的各种功能,如 k、事务管理等来增强应用程序的功能性和可维护性。

依赖注入(Dependency Injection)

DI 的基本概念

DI(Dependency Injection) 的意思是”依赖注入”,它是 IoC(控制反转)的一个别名。在早些年,软件开发教父 Martin·Fowler 在一篇文章中提到将 IoC 改名为 DI原文地址 其中有这样一段话,如下图所示
image

意思是:他认为需要为该模式(IoC)指定一个更具体的名称。因为控制反转是一个过于笼统的术语,所以人们会感到困惑。他与 IoC 的倡导者进行了大量讨论之后,然后他们决定使用依赖注入这个名称。也就是在这时 DI(依赖注入)这个词被大家知晓。IoCDI 其实是同一个概念,只是从不同的角度描述罢了(IoC 是一种思想,而 DI 则是一种具体的技术实现手段)。

这是我们在其它地方看到的一句话,这句话真的是醍醐灌顶,一句话就把其它人一大堆很难懂的话给说清楚了:IoC 是目的(它的目的是创建对象),DI 是手段(通过什么手段获取外部对象)。所以至此我们别再傻傻分不清楚 IoCDI 了。
(本小节摘自我初学 Spring Boot 时了解概念看到的一篇 blog,感觉受益匪浅,本次特从收(chi)藏(hui)夹中找出来,原文链接,力推!感兴趣的小伙伴可以去看看。另,本文旨在介绍 DI 注入方式,故 DIIOC 等理论知识不在此赘述)

依赖注入的主要方式

构造器注入

通过构造函数将依赖项传入目标对象。在创建目标对象时,必须提供其依赖项,这种方式确保了对象在创建时完全初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author -侑枫
* @date 2024/8/9 0:23:05
*/
@Component
public class MyService {
private final MyRepository repository;

@Autowired
public MyService(MyRepository repository) {
this.repository = repository;
}
}

属性注入

通过公共的 setter 方法将依赖项注入目标对象。这种方式允许在对象创建后对其进行依赖注入,但对象可能在某些情况下处于未完全初始化的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author -侑枫
* @date 2024/8/9 0:29:53
*/
@Component
public class MyService {
private MyRepository repository;

@Autowired
public void setRepository(MyRepository repository) {
this.repository = repository;
}
}

接口注入

通过实现特定接口将依赖项注入目标对象。虽然这种方式不常见,但它通过接口提供了一种明确的依赖注入契约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author -侑枫
* @date 2024/8/9 0:37:24
*/
public interface RepositoryAware {
void setRepository(MyRepository repository);
}

@Component
public class MyService implements RepositoryAware {
private MyRepository repository;

@Override
public void setRepository(MyRepository repository) {
this.repository = repository;
}
}
依赖注入是实现松耦合和提高系统灵活性的重要设计模式。通过将依赖关系交给容器或外部系统管理,开发人员可以专注于业务逻辑的实现,而无需担心对象的创建和依赖管理问题。

Spring Boot 中的 Bean 管理

Spring Boot 中,Bean 管理是核心功能之一。Spring Boot 通过其强大的自动配置机制和简化的配置方式,使得 Bean 的定义和管理变得更加直观和高效。

@ComponentScan 和 @Component 注解

@ComponentScan

@ComponentScan 注解用于指定 Spring Boot 容器要扫描的包路径,以寻找 Spring Boot 管理的 BeanSpring Boot 的自动配置默认会扫描主应用类所在的包及其子包下的所有组件。
image
image

@Component

@Component 注解标记一个类,使其成为 Spring Boot 容器中的一个 BeanSpring Boot 的自动配置会自动检测并注册被@Component@Service@Repository@Controller注解的类。

@Component:通用组件注解

@Service:业务逻辑层的组件

@Repository:数据访问层的组件

@Controller:表示控制器的组件,用于处理 HTTP 请求

@Autowired 注解

@Autowired 注解用于自动注入依赖。Spring Boot 通过此注解自动满足对象的依赖关系,无需手动创建或配置 Bean 的实例

特点及参数

按类型注入@Autowired 默认是按类型注入的。如果容器中存在多个相同类型的 Bean,需要使用 @Qualifier 注解来指定具体的 Bean

自动装配的可选性@Autowired 注解的 required 属性可以设置为 false,表示这个依赖项不是必需的

1
2
3
4
5
6
/**
* @author -侑枫
* @date 2024/8/9 1:13:52
*/
@Autowired(required = false)
private MyOptionalService optionalService;

构造器注入

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author -侑枫
* @date 2024/8/9 1:13:52
*/
@Component
public class MyService {
private final MyRepository repository;

@Autowired
public MyService(MyRepository repository) {
this.repository = repository;
}
}

字段注入

1
2
3
4
5
6
7
8
9
/**
* @author -侑枫
* @date 2024/8/9 1:13:52
*/
@Component
public class MyService {
@Autowired
private MyRepository repository;
}

Setter 注入

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author -侑枫
* @date 2024/8/9 1:13:52
*/
@Component
public class MyService {
private MyRepository repository;

@Autowired
public void setRepository(MyRepository repository) {
this.repository = repository;
}
}

@Resource 注解

@ResourceJava EE 的注解,属于 JSR-250 规范,并且是 Spring 的一种兼容注解。@Resource 注解通过名称来进行依赖注入,并且可以用于字段和 setter 方法。

特点和参数

按名称注入@Resource 默认是按名称注入的。它会尝试查找与字段名相同的 Bean 名称进行注入。如果没有找到匹配的名称,它会回退到按类型注入。

注入的名称匹配@Resource 可以通过 name 属性明确指定要注入的 Bean 名称

1
2
3
4
5
6
7
8
9
/**
* @author -侑枫
* @date 2024/8/9 1:13:52
*/
@Component
public class MyService {
@Resource(name = "myRepositoryBean")
private MyRepository repository;
}

选择什么注入方式?

字段注入

字段注入方式是使用最多的,原因是这种方式使用起来非常简单,代码更加简洁。但随之带来的缺点也很多,例如:使用字段注入,很难将字段声明为 final,这意味着依赖可能在对象生命周期中被改变。不可变性对于线程安全和代码可靠性很重要;字段注入更容易导致循环依赖问题,虽然 Spring Boot 可以帮我们处理大部分循环依赖,但这通常表明设计存在问题;使用字段注入的类在不使用 DI 容器的情况下很难进行单元测试。所以,不太推荐使用字段注入的方式。

Setter 注入

在 Spring 3.x 刚推出的时候,Spring 官方在对比构造器注入和 Setter 注入时,推荐使用 Setter 方法注入 原文链接

1
2
3
The Spring team generally advocates setter injection, because large numbers of constructor arguments can get unwieldy, especially when properties are optional. Setter methods also make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is a compelling use case.

Some purists favor constructor-based injection. Supplying all object dependencies means that the object is always returned to client (calling) code in a totally initialized state. The disadvantage is that the object becomes less amenable to reconfiguration and re-injection.

意思是说,当出现很多注入项的时候,构造器参数可能会变得臃肿,特别是当参数时可选的时候。Setter 方式注入可以让类在之后重新配置和重新注入

构造器注入

Spring 4.x 的时候,Spring 官方在对比构造器注入和 Setter 注入时,推荐使用构造器注入方式原文链接

1
2
3
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. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.

Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is therefore a compelling use case for setter injection.

因为使用构造器注入方式注入的组件不可变,且保证了需要的依赖不为 null。此外,构造器注入的组件总是能够在完全初始化的状态返回给客户端(调用方);对于很多参数的构造器说明可能包含了太多了职责,违背了单一职责原则,表示代码应该重构来分离职责到合适的地方。

构造器注入还是 Setter 注入?

Setter 注入应该被用于可选依赖项。当没有提供它们时,类应该能够正常工作。在对象被实例化之后,依赖项可以在任何时候被更改。可以实现可选依赖注入或者在需要依赖注入的对象在运行时可变的情况下更灵活。

构造器注入有利于强制依赖。通过在构造函数中提供依赖,您可以确保依赖对象在被构造时已准备好被使用。在构造函数中赋值的字段也可以是 final 的,这使得对象是完全不可变的,或者至少可以保护其必需的字段。减少了对象状态不一致的可能性,增强了线程安全性的同时更符合面向对象设计原则中的不可变对象概念。

构造器注入还可以避免 字段注入的循环依赖问题,比如 在 A 中注入 B,又在 B 中注入 A。如果使用构造器注入,在 Spring 启动的时候就会抛出 BeanCurrentlyInCreationException 提醒循环依赖。

两者循环依赖差异化原理分析

当使用 @Autowired 进行字段注入时,Spring 可以先创建 Bean 的实例,然后再注入依赖。如果两个 Bean 互相依赖,Spring 可以先创建一个 Bean 的实例,然后在后续注入过程中解决依赖关系;而当使用构造函数注入时,Spring 必须在创建 Bean 实例之前解决所有依赖。如果两个 Bean 互相依赖,Spring 无法在创建一个 Bean 实例之前获取到另一个 Bean,导致循环依赖问题。

使用什么方式的注入,取决于代码的设计和需求。在日常开发业务中,我个人更为推荐使用构造器注入方式。

使用 Lombok 简化构造函数

Java 开发中,特别是在使用 Spring Boot 等框架时,编写构造函数和样板代码往往变得繁琐。Lombok 是一个能够简化这些重复性代码的库,其中 @RequiredArgsConstructor 注解是其强大功能的一部分,用于自动生成构造函数,从而减少样板代码。

@RequiredArgsConstructor 注解

自动生成带有 final 修饰的成员变量的构造函数
@RequiredArgsConstructor 注解用于自动生成一个构造函数,这个构造函数会包含所有用 final 修饰的成员变量(建议)以及被 @NonNull 注解标记的字段。这个构造函数确保了这些字段在对象创建时被初始化,从而促进了不变性和更好的代码设计。

示例:
使用 @RequiredArgsConstructor 简化构造函数之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @author -侑枫
* @date 2024/8/9 2:38:09
*/
@Service
public class MyService {

private final OneService oneService;
private final TwoService twoService;
private final ThirdService thirdService;
private final FourthService fourthService;
private final FifthService fifthService;

/**
* 手动编写的构造函数
*/
public MyService(
OneService oneService,
TwoService twoService,
ThirdService thirdService,
FourthService fourthService,
FifthService fifthService) {
this.oneService = oneService;
this.twoService = twoService;
this.thirdService = thirdService;
this.fourthService = fourthService;
this.fifthService = fifthService;
}

/**
* 其他方法
*/
}

image

可以在 Structure 查看类的结构,发现所有的依赖都已成功注入到类中

使用 @RequiredArgsConstructor 之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author -侑枫
* @date 2024/8/9 2:38:09
*/
@Service
@RequiredArgsConstructor
public class MyService {

private final OneService oneService;
private final TwoService twoService;
private final ThirdService thirdService;
private final FourthService fourthService;
private final FifthService fifthService;

/*
自动生成的构造函数将包含所有 final 字段
无需手动编写构造函数
*/

/**
* 其他方法
*/
}

image

Structure 查看类的结构,发现所有的依赖跟手动编写构造器注入的结果一致,都为成功注入

解决 @RequiredArgsConstructor 导致的循环依赖

从上文两者循环依赖差异化原理分析 中我们可以得知,使用 @Autowired 注解进行字段注入时,Spring 容器会在创建 Bean 实例之后,通过反射将依赖注入到字段中。这意味着在注入依赖之前,Bean 实例已经创建完成,可以解决一些简单的循环依赖问题。而使用构造函数注入时,Spring 容器在创建 Bean 实例时需要立即解析所有依赖项。所以导致当我们使用构造函数注入方式在 A 中注入 B,且在 B 中注入 A 时,就会产生循环依赖。
那么我们的解决方法有以下三种

避免循环依赖:尽量避免设计中出现循环依赖。如果无法避免,可以考虑通过重构代码来打破循环依赖

重构代码:将依赖关系抽象到第三方类中,避免直接依赖

使用 @Lazy 注解:可以在注入依赖时使用 @Lazy 注解,延迟注入依赖,从而解决循环依赖问题

当代码出现循环依赖时,通常意味着设计上或者代码层面上存在不符合规范的问题。这里我们不讨论设计上的问题,但当我们无法立即解决设计问题的情况下,可以根据已知的构造函数注入引发的循环依赖原因,采取相应措施来解决问题。具体做法是延迟 Bean 的初始化,直到它第一次被访问,这样可以推迟特定依赖的注入,从而解决循环依赖的问题。这就是 @Lazy 注解侧面解决循环依赖的原理。

我们仅需要在 @RequiredArgsConstructor 上加上 onConstructor = @__(@Lazy) 这个参数即可解决。当然,最好的情况还是从根源上解决问题。

结语

Spring Boot 中,依赖注入是管理和维护应用程序组件之间关系的核心机制。通过本文的讲解,我们不仅掌握了依赖注入的基本概念,还深入了解了 Spring Boot 中的 Bean 管理和不同的注入方式。通过使用 Lombok,我们能够简化构造函数的编写,进一步提升开发效率和代码质量。这些知识将帮助开发人员更有效地管理应用程序的依赖关系,并编写更清晰、更可维护的代码。

  • 标题: Spring Boot 中 Bean 的注入方式
  • 作者: HYF
  • 创建于 : 2024-08-08 23:56:29
  • 更新于 : 2024-08-09 03:28:28
  • 链接: https://youfeng.ink/dependency-injection-type-8907094df8a5/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。