在使用Spring Boot时,最强大的功能之一是依赖注入(DI)。DI是一种设计模式,它允许开发人员通过注入类所需的依赖,而不是由类自身实例化依赖,从而构建松耦合、易于测试和维护的应用程序。在Spring中,可以通过多种方法实现DI,其中最常用的方法包括@Autowired
和构造器注入。
在这本文中,我们将深入探讨@Autowired
注解以及构造函数注入,在不同场景下比较它们,讨论最佳实践并强调每种方法的优点和缺点,突出它们的优点和缺点。通过本指南结束时,您将对这两种方法有全面的理解,并知道如何在Spring Boot应用程序中有效地应用它们。
在深入了解@Autowired
和构造函数注入之前,先要理解清楚什么是依赖注入以及它在软件开发中的重要性。
依赖注入是一种技术,其中对象的依赖由外部实体提供和注入,而不是由对象自己创建。这个外部实体通常是像 Spring 这样的框架,它管理对象(也就是 bean)的生命周期,并在运行时注入它们的依赖。
- 解耦:DI 使应用程序中组件的松耦合得以实现,而不是在类中硬编码依赖关系,框架会来管理它们。这种解耦让修改、替换或扩展组件更加容易,而不影响应用程序的其他部分。
- 可测试性:DI 提高了代码的可测试性,在注入依赖关系时,你可以在测试期间轻松地模拟它们,从而实现更隔离且有效的单元测试。
- 可维护性:DI 有助于编写更易于维护的代码。通过明确地定义依赖关系,更容易理解不同组件是如何协作的,从而减少在修改时引入错误的可能性。
Spring 提供了几种依赖注入功能,包括如下:
- 字段注入:直接在字段上使用
@Autowired
注解。 - setter 注入:在 setter 方法上使用
@Autowired
注解。 - 构造器注入:通过构造函数传递依赖项。
每种方法都有其优势和适用场合,知道何时使用哪种方法对编写干净且有效的Spring Boot应用非常重要。
自动装配注解@Autowired
在依赖注入中的作用
@Autowired
是一个 Spring 特定的注解,用于自动注入依赖。它可以应用于字段、setter 方法和构造器,让 Spring 能在创建 bean 对象的时候自动解析并注入所需的依赖。
@Autowired
的自动装配
字段注入或许是注入依赖最简单和最直接的方式。通过在字段上使用 @Autowired
注解,Spring 在创建包含该字段的 Bean 时,会自动注入所需的 Bean。
字段注入的例子.
package com.medium;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // 用户仓库: 负责用户数据的存储和检索
public void createUser(User user) {
userRepository.save(user); // 创建用户: 将用户信息保存到数据库
}
}
使用 @Autowired
注解将 UserRepository
依赖注入 UserService
类的字段,如示例所示。
- 简洁:字段注入易于使用,所需的样板代码很少。你无需编写构造函数或setter方法来注入依赖。
- 易于阅读:依赖项在类的顶部清晰可见,使你一目了然该类依赖于什么。
- 测试性:字段注入会使单元测试更麻烦。由于依赖项是私有字段,你需要使用反射在测试用例中设置这些字段,这可能会让人感到繁琐,并容易出错。
- 不可变性问题:使用字段注入时,注入的字段仍然是可变的,若在对象构建之后修改这些字段,可能会导致意外的副作用,从而引起问题。
- 隐藏的依赖性:通过字段注入的依赖项对于阅读代码的人来说并不明显,这会使代码更难理解及维护。依赖项注入在背后悄然进行,可能会使代码流程变得模糊。
- 循环依赖:字段注入可能会掩盖循环依赖(其中两个或多个bean互相依赖)。虽然Spring可以通过字段注入来解决循环依赖问题,但这通常表示需要解决的设计问题,而不是依赖框架来处理它。
构造器注入是依赖注入的一种替代方式,其中依赖项通过类的构造函数传递。这种方法通常比字段注入更受青睐,因为它提供了多个明显的优势,尤其是在不可变性、可测试性以及确保依赖项正确注入方面。
构造注入示例:
package com.medium;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository userRepository;
// 在构造函数中注入UserRepository
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 创建一个用户
public void createUser(User user) {
userRepository.save(user);
}
}
在这个例子中,UserRepository
依赖项通过构造函数注入,并且 userRepository
属性被标记为 final
,这表明该属性在对象创建后无法更改。
- 不可变性:构造函数注入允许你将依赖声明为
final
,确保对象创建后依赖项不会被修改。这促进了不可变性,这是构建可靠且可预测的软件的关键原则。 - 强制依赖:构造函数注入强制所有必需的依赖项在对象创建时必须提供。这可以防止因未设置所需依赖项而导致的
NullPointerException
等问题。 - 可测试性:构造函数注入显著提升了可测试性。你可以轻松地使用模拟依赖项创建类的实例,而无需依赖反射或其他变通方法。这使得单元测试更干净、更简单。
- 清晰的依赖:使用构造函数注入,所有依赖项都在构造函数中明确列出,使类的依赖关系清晰易懂。这种明确性有利于代码维护和协作,因为可以立即看出类需要哪些依赖才能正常工作。
- 避免循环依赖:构造函数注入更难以创建循环依赖。如果两个类互相依赖,尝试通过构造函数注入它们会导致编译错误,从而促使你重新考虑设计。
- 样板代码:对于依赖项很多的类,构造函数注入可能会导致大量的样板代码。不过,可以使用 Lombok 这样的工具自动生成构造函数来减轻这种情况。
- 长参数列表:如果一个类有太多的依赖项,构造函数的参数列表会变得很长且难以处理。这可能是类的功能过于复杂的迹象,可能需要将其重构为更小、更专注的类。
- 处理可选依赖项:如果一些依赖项是可选的,构造函数注入会变得繁琐。虽然可以在构造函数中使用
Optional
,但这会使代码更难理解。
构造器注入的一个缺点是可能会导致大量的样板代码,尤其是在那些依赖项很多的类中。Lombok 库提供了一个解决方案,即使用 @RequiredArgsConstructor
注解,该注解会自动生成一个只包含类中所有 final
字段的构造器。
@RequiredArgsConstructor
示例
下面是如何使用 Lombok 的 @RequiredArgsConstructor
,来简化这个过程:
构造器注入
包 com.medium;
导入 lombok.RequiredArgsConstructor;
导入 org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public void createUser(User user) {
userRepository.save(user);
}
}
解释:
**@RequiredArgsConstructor**
注解:这个Lombok注解会为类中的所有final
字段生成一个带有参数的构造函数。在这个例子中,Lombok会自动生成一个接受UserRepository
作为参数的UserService
构造函数,这使得初始化UserService
时更加方便。- 减少冗余代码:使用
@RequiredArgsConstructor
可以省去手动编写构造函数的麻烦,减少冗余代码,使类定义更加简洁。
@RequiredArgsConstructor
的好处
- 更整洁的代码:Lombok 的
@RequiredArgsConstructor
减少了样板代码,使你的代码更简洁易读。 - 保持一致性:如果你在整个项目中使用构造函数注入,Lombok 通过自动生成所有具有最终依赖项的类的构造函数来帮助保持一致性。
- 更专注于业务逻辑:通过自动生成构造函数,你可以更专注于应用程序的业务逻辑,而不是重复地编写构造函数。
- 隐藏的魔法:虽然 Lombok 减少了样板代码,但它有时会掩盖程序内部发生的事情。不熟悉 Lombok 的开发人员可能觉得代码更难理解,因为构造函数没有被明确写出。
- 编译时依赖:Lombok 需要在编译时进行注解处理,这意味着你的项目依赖于 Lombok 在构建系统中正确配置和可用。
@Autowired
构造器注入
Spring 允许你通过构造函数注入使用 @Autowired
,但这并不总是必需的。从 Spring 4.3 开始,如果某个类只有一个构造函数,你可以不用写 @Autowired
注解,Spring 会自动注入这些依赖。
这里有一个使用@Autowired
的例子:
package com.medium;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void createUser(User user) {
userRepository.save(user);
}
}
示例不使用@Autowired
package com.medium;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository userRepository;
// 用户服务类,用于创建用户
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 创建用户方法
public void createUser(User user) {
userRepository.save(user);
}
}
在两个例子中,Spring 在创建 UserService
bean 时会注入 UserRepository
依赖。而第二个例子更加简洁,并且更推荐使用 Spring 4.3 或更高版本时。
@Autowired
进行构造器注入的好处
- 更简洁的代码:对于只有一个构造函数的类,省略
@Autowired
可以让代码更简洁,更加清爽。 - 一致性:如果你遵循使用构造函数注入的约定,并且你在 Spring 4.3 或更高版本上,省略
@Autowired
有助于保持代码的一致性。 - 自动依赖解析:Spring 能够根据构造函数参数自动解析依赖关系,简化了依赖注入的操作,减少了显式注解的需求。
让我们仔细看看这两个概念,即字段级别的注入和构造器注入之间的差异,从不可变性、可测试性以及代码清晰度等方面来考察一些关键方面。
1. 不可变性- 通过字段的依赖注入:通过字段注入的依赖关系是可变的,意味着在对象创建之后它们可以被修改。如果代码中的其他地方修改了依赖的状态,这可能导致意外的副作用。
- 构造器注入:通过构造器注入的依赖可以标记为
final
,使其不可变。这确保了对象一旦创建,其依赖关系就不能被更改,从而确保行为更可预测和可靠。
例子:
- 字段注入:
UserService
中的userRepository
字段在对象创建后可以被重新赋值为其他对象。 - 构造函数注入:
UserService
中的userRepository
字段是final
,因此在创建对象后不允许重新赋值。
- 字段依赖注入:测试使用字段注入的类可能会更具挑战,因为你需要使用反射来注入模拟依赖。这会使测试变得复杂且难以阅读。
- 构造注入:构造函数注入使得测试更加简单。可以直接使用模拟依赖来实例化类,从而使单元测试更简单且更有效。
例子:
- 字段注入(Field Injection):要测试
UserService
,你可能需要用反射来注入一个模拟的UserRepository
。 - 构造函数注入(Constructor Injection):在创建
UserService
的实例时,你可以轻松地传入一个模拟的UserRepository
对象。
- 字段注入(Field Injection):依赖被隐式注入,这使得理解一个类依赖于什么更加困难。这种不明确性使得代码更难维护,尤其是在大型代码库中。
- 构造函数注入:在构造函数中明确列出依赖,使得清楚地知道该类依赖什么。这种明确性不仅增强了代码的可读性,还提高了可维护性,使与其他开发者的合作更加容易。
例子
- 字段注入方式:仅从类的定义中很难立即看出哪些依赖项被注入。
- 构造函数注入方式:构造函数清楚地表明了类所需的各种依赖。
- 属性注入:有可能在未设置其所需依赖的情况下实例化一个类,这可能导致例如
NullPointerException
的运行时错误。 - 构造器注入:只有在提供所有必需依赖的情况下才能实例化该类,从而避免了运行时错误。
例子:
- 属性注入:如果在实例化
UserService
时不注入UserRepository
,将会抛出NullPointerException
。 - 构造器注入:如果不提供
UserRepository
,则不能被实例化UserService
。
- 字段注入:Spring 可以通过字段注入解决循环依赖关系问题,但这通常表明设计存在问题。循环依赖会导致潜在问题,并使代码更难理解。
- 构造函数注入:构造函数注入难以处理循环依赖关系问题,因为这会导致编译错误。这会迫使你重新考虑设计并寻找更好的方法解耦类。
例子:
- 字段注入:Spring 可以解决循环依赖问题,但这意味着设计不佳,应避免这种情况。
- 构造函数注入:循环依赖会导致即时编译错误,这会促使你重构代码。
虽然有缺点,场注入在某些情况下仍然很有用:
- 原型开发:当你快速开发一个应用程序的原型或编写小型工具类时,字段注入可以节省时间和减少样板代码的编写。
- 遗留代码:在构造函数注入需要大量重构的遗留代码库中,字段注入可能是一个更实用的选择方案。
- 循环依赖:如果你正在处理一个具有内在循环依赖的复杂系统(尽管这种情况应尽可能避免),字段注入可能是解决这些依赖关系最简单的方式。
构造器注入通常是推荐的做法,尤其是在以下几种情况:
- 构建新应用:从一开始就使用构造函数注入有助于确保代码的整洁、可维护性和可测试性。
- 强制不可变性:如果你想在类中强制不可变性,构造函数注入是最好的选择,因为它允许你将依赖项标记为
final
。 - 提高可测试性:如果你正在编写单元测试并希望轻松模拟依赖,构造函数注入是最直接的方式。
- 明确声明依赖:在大型项目中,当代码清晰和可维护性至关重要的时候,构造函数注入可以明确每个类所需的依赖。
这里有一些在 Spring Boot 中进行依赖注入时可以参考的最佳实践:
- 更喜欢构造器注入而不是字段注入:构造器注入通常是最佳选择,因为它促进了不可变性,提升了可测试性,并使依赖关系更明确。
- 谨慎使用
**@Autowired**
注解:如果你使用的是Spring 4.3或以上版本,并且你的类只有一个构造器,可以省略@Autowired
。当你的类有多个构造器或需要使用setter注入时,再使用它。 - 避免生产代码中的字段注入:虽然在某些特定场景下字段注入可能有用,因为上述原因,最好在生产代码中避免使用字段注入,转而使用构造器注入。
- 谨慎处理可选依赖关系:如果你有可选依赖关系(即类A依赖类B,类B又依赖类A),考虑在构造器中使用
Optional
或使用setter注入。这确保了你的代码能够优雅地处理这些情况,而不增加不必要的复杂性。 - 重构循环依赖关系:如果你遇到循环依赖关系,考虑重构代码以消除它们。循环依赖关系常常表明你的类过于紧密耦合,需要分解成更小、更专注的组件。
- 使用Lombok减少重复代码:如果你的类有很多依赖关系,构造器参数列表过长,可以考虑使用Lombok的
@AllArgsConstructor
或@RequiredArgsConstructor
来自动生成构造器并减少重复代码。 - 记录依赖关系:在使用构造器注入时,记录构造器中的依赖关系是一个良好的实践。这使得其他开发人员(包括未来的你自己)能够清楚地了解每个依赖关系为何必要。
结论
在 Spring Boot 中,@Autowired
和构造器注入都是管理依赖关系的有价值的工具。虽然 @Autowired
提供了简单的实现和快速的集成,但它可能会让代码难以维护和测试,因为它隐藏了依赖关系和可变的成员变量。
相比之下,构造器注入通常被推荐。它通过将依赖项设为 final
来促进不可变性,通过允许轻松模拟来提高可测试性,并通过明确列出所有依赖项来提高代码的清晰度,避免隐式依赖。这种方法使应用程序更可预测、更易维护和更健壮。在 Spring Boot 中,建议将构造器注入作为依赖管理的默认方法,仅在确实需要时才使用 @Autowired
。
了解更多关于Spring及Java开发的相关内容,
快来提升你的技能水平吧!我们为你精选了多篇文章供你参考。
- Java Lambda 表达式:高级开发者技巧 (2024年6月19日):深入探讨 Java 中 Lambda 表达式的高级用法。阅读更多
- 精通 Spring Security:角色和权限管理要点 (2024年6月19日):学习 Spring Security 中角色和权限管理的要点。阅读更多
- 发布您的 Java 库到 Maven 中心:一步一步指南 (2024年3月25日):一份全面的指南,让您的 Java 库可以被全球数百万开发者使用。阅读更多
- 单元测试的艺术:用有效的模拟提高代码质量 (2024年3月13日):一份完整的指南,介绍单元测试中模拟的使用,强调组件隔离的优势。阅读更多
- 精通 Spring Beans (2023年12月17日): 解锁高级应用程序开发技巧。阅读更多
- JSON 转 Java 映射 (2023年12月17日): 简化您的数据处理。阅读更多
-
Spring Boot 官方指南
访问链接:https://spring.io/projects/spring-boot -
Baeldung — Spring 中的控制反转和依赖注入
访问链接:https://www.baeldung.com/inversion-control-and-dependency-injection-in-spring -
Spring 依赖注入文档
访问链接:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies -
Spring 中的字段注入(Baeldung)
访问链接:https://www.baeldung.com/spring-injecting-collections - Spring 中的构造器注入(Baeldung)
访问链接:https://www.baeldung.com/constructor-injection-in-spring