为什么要停止在 SpringBoot 中使用字段注,改用构造器注入

停止在 SpringBoot 中使用字段注入!

本文为翻译文,同时加入了一些自己的理解,翻译来源:https://medium.com

在 Spring Boot 依赖项注入的上下文中,存在关于注入依赖项最佳实践的争论:字段注入、Setter注入和构造函数注入。

❝在本文中,我们将重点讨论字段注入的缺陷,并提出一个远离它的案例。❞

1 什么是字段注入?

字段注入涉及直接用 @Autowired 注释类的私有字段。这是一个例子:

@Component
public class OrderService {@Autowiredprivate OrderRepository orderRepository;public Order findOrderById(Long id) {return orderRepository.findById(id);}
}

2 为什么应该停止使用字段注入

2.1 可测试性

字段注入使组件的单元测试变得复杂。 由于依赖项直接注入到字段中,因此我们无法在 Spring 上下文之外轻松提供模拟或替代实现。

让我们以 sameOrderService 类为例。

如果我们希望对 OrderService 进行单元测试,那么在模拟 OrderRepository 时会遇到困难,因为它是一个私有字段。下面是对 OrderService 进行单元测试的方法:

@RunWith(SpringJUnit4ClassRunner.class)
public class OrderServiceTest {private OrderService orderService;@Mockprivate OrderRepository orderRepository;@Beforepublic void setUp() throws Exception {orderService = new OrderService();// This will set the mock orderRepository into orderService's private fieldReflectionTestUtils.setField(orderService, "orderRepository", orderRepository);}...
}

尽管可以实现,但使用反射来替换私有字段并不是一个很好的设计。它违背了面向对象的设计原则,使测试难以阅读和维护。

但是,如果我们使用构造函数注入:

@Component
public class OrderService {private final OrderRepository orderRepository;public OrderService(OrderRepository orderRepository) {this.orderRepository = orderRepository;}
}

我们可以在测试期间轻松提供模拟 OrderRepository:

OrderRepository mockRepo = mock(OrderRepository.class);
OrderService orderService = new OrderService(mockRepo);

2.2 不变性

字段注入使我们的 Bean 在构建后可变。而通过构造函数注入,一旦构造了一个对象,它的依赖关系就会保持不变。

举例来说:

字段注入类:

@Component
public class UserService {@Autowiredprivate UserRepository userRepository;
}

这里,userRepository 在创建对象后可以重新分配引用,这就打破了不变性原则。

如果我们使用构造函数注入:

@Component
public class UserService {private final UserRepository userRepository;public UserService(UserRepository userRepository) {this.userRepository = userRepository;}
}

该 userRepository 字段可以声明为最终字段,在构造完成后,就会一直保持不变。

2.2.1 这里的 @Autowired private UserRepository userRepository; 不能在private后加一个final吗?

在Spring框架中,使用字段注入时,你通常不会在被@Autowired注解的字段后加上final关键字。原因在于当Spring创建Bean的实例时,它需要能够设置这些没有通过构造函数提供的依赖关系。如果字段被声明为final,那么它必须在构造对象的时候初始化,之后就不能再改变了。

字段注入通常是通过反射来完成的,反射可以允许即使字段被声明为private,仍然能够被外部类修改。但是,如果你使用final来修饰字段,反射也不能用于改变其值,因为final字段在构造对象后就不可变了。这就是为什么你会在构造函数注入中看到final字段,而在字段注入中不会看到。

构造函数注入提供了不变性,因为你可以将所有依赖项声明为final。这意味着一旦构造了对象,这些依赖项就不能更改,从而可以避免很多因为状态改变导致的问题。这也符合不变性原则,是当前推荐的注入方式,因为它提高了代码的安全性和健壮性。

总结一下,@Autowired注解的字段不能声明为final,因为Spring需要在对象构造后设置这些字段。如果你想要不变性,你应该使用构造函数注入,并将依赖项声明为final

2.2.2 如果我使用构造函数注入一个bean,要怎么样使用他呢?

当你使用构造函数注入来注入一个Bean时,Spring容器会负责创建这个Bean的实例,并且会自动把构造函数需要的依赖项注入进去。这里是一个使用构造函数注入的基本步骤:

  1. 定义依赖项接口。
  2. 创建依赖项的实现类,并使用例如@Component注解将其标记为Spring管理的Bean。
  3. 在使用依赖项的类中,创建一个构造函数,该构造函数接受依赖项作为参数,并使用例如@Autowired注解(在Spring 4.3之后,如果类只有一个构造函数,可以省略@Autowired注解)。

以下是一个简单的示例:

// 依赖项接口
public interface UserRepository {// 定义所需要的操作,例如查找用户等
}// 依赖项的实现类
@Component
public class UserRepositoryImpl implements UserRepository {// 实现UserRepository接口中定义的方法
}// 使用依赖项的类
@Component
public class UserService {private final UserRepository userRepository;// 构造函数注入public UserService(UserRepository userRepository) {this.userRepository = userRepository;}// 类中的其他方法可以使用userRepositorypublic void performAction() {// 使用userRepository执行一些操作}
}

在这个例子中,UserService需要一个UserRepository的实例。Spring会自动找到匹配UserRepository类型的Bean(在这个例子中是UserRepositoryImpl的实例),然后创建一个UserService的实例,将UserRepositoryImpl作为参数传递给UserService的构造函数。

在Spring应用中,你不需要自己去创建UserService的实例。Spring容器会自动处理这一切。当你需要使用UserService时,你可以让Spring自动注入它,例如:

@RestController
public class UserController {private final UserService userService;// 在控制器中通过构造函数注入UserServicepublic UserController(UserService userService) {this.userService = userService;}@GetMapping("/users")public ResponseEntity<List<User>> getUsers() {// 使用userService来处理获取用户的请求}
}

在上面的控制器中,UserService将被自动注入到UserController中。这样,你就可以在控制器中使用UserService提供的方法来处理请求了。

2.3 与Spring更紧密的耦合

字段注入使我们的类与 Spring 耦合更紧密,因为它直接在我们的字段上使用 Spring 特定的注释 ( @Autowired)。这可能会在以下场景中出现问题:

「不使用 Spring 的情况」:假设我们正在构建一个不使用 Spring 的轻量级命令行应用程序,但我们仍然想利用 UserService 的逻辑。在这种情况下,@Autowired 注释没有任何意义,不能用于注入依赖项。我们就必须重构该类或实现繁琐的解决方法才能重用UserService.

「切换到另一个 DI 框架」:如果我们决定切换到另一个依赖注入框架,比如 Google Guice,Spring 特定的框架 @Autowired 就会成为一个障碍。那时我们必须重构使用 Spring 特定注释的每一个地方,这会是十分繁琐的。

「可读性和理解性」:对于不熟悉 Spring 的开发人员来说,遇到 @Autowired 注解可能会感到困惑。他们可能想知道如何解决依赖关系,从而增加学习成本(ps:虽然不熟悉 Spring 开发的Java程序员可能很少了)。

2.4 空指针异常

当类利用字段注入并通过其默认构造函数实例化时,依赖字段保持未初始化。

举例来讲:

@Component
public class PaymentGateway {@Autowiredprivate PaymentQueue paymentQueue;public void initiate (PaymentRequest request){paymentQueue.add(request);...}
}public class PaymentService {public void process (PaymentRequest request) {PaymentGateway gateway = new PaymentGateway();gateway.initiate(request);}   
}

通过上面的代码,我们不难看出,如果在运行时以这种状态访问PaymentGateway,则会发生 NullPointerException。在Spring上下文之外手动初始化这些字段的唯一方法是使用反射,反射机制的语法比较繁琐且易错,在程序可读性方面存在一定问题,所以不建议这样做。

2.4.1 为什么直接在非spring类中new一个spring的bean会报NPE?

在您提供的代码示例中,PaymentService类直接通过new关键字创建了PaymentGateway的实例,而不是通过Spring的依赖注入来获取。当直接使用new关键字创建实例时,Spring容器不会介入该对象的生命周期,这意味着Spring不会自动注入PaymentGateway中的依赖paymentQueue

由于paymentQueue没有被初始化(因为Spring没有注入它),当initiate方法被调用时,它尝试访问paymentQueueadd方法。因为此时paymentQueuenull,所以尝试调用其方法会导致NullPointerException(NPE)。

在Spring应用程序中,为了避免此类问题,应该总是通过Spring容器获取Bean实例,这样Spring就能自动管理Bean的生命周期和依赖注入。如果你需要在Spring管理的Bean中使用PaymentGateway,你应该让Spring注入它,而不是自己创建实例。

例如,改正后的PaymentService可能会看起来像这样:

@Service
public class PaymentService {private final PaymentGateway paymentGateway;@Autowiredpublic PaymentService(PaymentGateway paymentGateway) {this.paymentGateway = paymentGateway;}public void process(PaymentRequest request) {paymentGateway.initiate(request);}
}

在这个修改后的版本中,PaymentGateway由Spring通过构造函数注入到PaymentService中,这样就确保了PaymentGatewaypaymentQueue依赖会被Spring容器自动注入,从而避免了NPE。

2.5 循环依赖

字段注入可能会掩盖循环依赖问题,使它们在开发过程中更难被发现。
举例来讲:
考虑两个相互依赖的服务AService和BService:

@Service
public class AService {
@Autowired
private BService bService;
}

@Service
public class BService {
@Autowired
private AService aService;
}

以上可能会导致应用程序中出现意想不到的问题。

使用构造函数注入,Spring会在启动期间立即抛出 BeanCurrentlyInCreationException,让我们意识到循环依赖。不过,要解决循环依赖问题,可以使用@Lazy延迟加载其中一个依赖项。

2.5.1 我记得spring中是允许循环依赖的吧,如何解决的

是的,Spring框架确实支持循环依赖,但是这种支持仅限于字段注入(setter注入)和方法注入。Spring通过使用三级缓存来解决单例作用域下的循环依赖问题,使得在构造函数中注入循环依赖的Bean成为可能。

对于构造函数注入来说,Spring无法处理循环依赖,因为在调用构造函数之前,每个Bean的依赖必须先被解决。如果A需要B才能创建,而B同时也需要A才能创建,Spring就无法决定应该先创建哪个Bean,因此会抛出BeanCurrentlyInCreationException异常。

对于您提到的例子,如果两个服务A和B都通过构造函数相互注入,Spring会在应用程序启动时检测到循环依赖并抛出异常。如果使用字段注入,Spring可以通过先实例化一个Bean,然后在设置属性时实现注入,从而解决循环依赖的问题。

2.5.2 构造器注入如何解决循环依赖问题

如果想要在构造函数注入中解决循环依赖问题,可以使用@Lazy注解来延迟依赖项的加载。例如:

@Service
public class AService {private final BService bService;@Autowiredpublic AService(@Lazy BService bService) {this.bService = bService;}
}@Service
public class BService {private final AService aService;@Autowiredpublic BService(AService aService) {this.aService = aService;}
}

在上面的代码中,@Lazy注解确保了BServiceAService实例化时不会立即被创建,而是在首次访问BService时才创建。这样,Spring可以先完成AService的创建,然后在需要时创建BService实例,避免了循环依赖问题。

2.6 @Autowired注入不推荐,那么@Resource呢,能够避免@Autowired的这些问题吗?

@Resource 是 Java EE 6 中的注解,它可以用来注入依赖项,其行为略有不同于 Spring 的 @Autowired。下面是这两个注解的一些区别:

  1. 来源:

    • @Autowired 是 Spring 特有的注解。
    • @Resource 来自于 Java 的 javax.annotation 包。
  2. 注入方式:

    • @Autowired 默认按类型进行自动装配。当需要按名称装配时,可以结合 @Qualifier 注解使用。
    • @Resource 默认按名称进行注入,如果没有找到与名称匹配的bean,则会按类型进行注入。它有两个重要属性:nametype
  3. 兼容性:

    • @Autowired 与 Spring 紧密集成,支持Spring特有的功能,如 @Qualifier@Primary 等。
    • @Resource 是标准的 Java 注解,因此它不依赖于 Spring,并且可以用在任何兼容 Java EE 的容器中。

关于能否避免 @Autowired 的问题:

  • 循环依赖@Resource 并不能解决构造器注入时的循环依赖问题,这是因为循环依赖的问题与注入机制本身相关,而不是特定于某个注解。Spring容器需要创建bean实例时,必须解决所有必需的依赖,不管这些依赖是通过 @Autowired 还是 @Resource 注入的。

  • 依赖不明确@Resource 默认按名称注入,这使得它在有多个类型相同的bean时,可以通过名称指定具体依赖哪个bean,从而在某种程度上提高了注入的明确性。

  • 自动装配的灵活性:使用 @Resource 时,你失去了 Spring 提供的一些灵活性,比如使用 @Primary 注解来指定首选的bean。

总的来说,@Resource 注解提供了另一种依赖注入的方式,但它并不能解决所有 @Autowired 可能引发的问题。在Spring框架中,建议优先使用构造器注入(无论是通过 @Autowired 还是通过参数解析),因为它可以帮助你避免上述提到的大多数问题,并且还能提高代码的可测试性。

2.7 在互联网大厂中,哪一种注入用的多?

在互联网大厂中,构造器注入(Constructor Injection)通常是首选的依赖注入方法,原因如下:

  1. 不变性和安全性:构造器注入使得依赖项可以是 final 的,这意味着一旦构造了对象,其依赖就不能更改。这种不变性可以减少在多线程环境下的问题,并确保依赖项在使用前已经被完全初始化。

  2. 可测试性:使用构造器注入,可以在不启动整个Spring容器的情况下,更容易地进行单元测试。

  3. 明确的依赖:构造器参数强制要求在创建对象时提供依赖项,这使得依赖关系更加明确,避免了 null 引用的可能性。

  4. 框架无关性:构造器注入不依赖于Spring或者任何其他依赖注入框架,这使得代码更容易迁移和重构。

尽管构造器注入在很多情况下是更好的选择,但在实际开发中,还是会根据具体场景和需求来决定使用哪种注入方式。例如,当存在多个构造器参数,并且这些参数中的某些是可选的,或者在某些复杂的依赖场景中,开发者可能会选择字段注入(Field Injection)或者设值注入(Setter Injection)。

@Autowired@Resource 注解都用于自动装配Spring Bean,但由于 @Autowired 提供了与Spring更紧密的集成和更多的灵活性,它通常是更受青睐的选择。不过,实际使用哪一个还是要基于项目的具体需求、团队习惯以及编码规范来决定。在一些遵循严格的Java EE标准的项目中,可能会倾向于使用 @Resource

最终,无论哪种注入方式,重要的是保持一致性、清晰性和可维护性。大厂的代码规范会倾向于推崇这些原则,并通过代码审查、文档和团队培训来确保最佳实践的贯彻执行。

3 结论

虽然字段注入可能看起来更简洁,但它的缺点远远超过了它的简洁性。构造函数注入在应用程序的可测试性、不变性和整体稳健性方面提供了明显的优势。

它与 SOLID 原则非常一致,确保我们的 Spring Boot 应用程序可维护且不易出错。

所以,建议大家停止在 Spring Boot 中使用字段注入!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/134936.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SparkML

SparkML SparkML_lr_train &#xff1a;读取py处理后的train表用于训练&#xff0c;将训练模型保存好。 SparkML_lr_predict &#xff1a;读取训练好的模型&#xff0c;读取py处理后的test表用于预测。将预测结果写入normal_data中&#xff0c;根据id修改stream_is_normal的值。…

深入理解JVM虚拟机第十六篇:虚拟机栈存储结构和运行原理

大神链接:作者有幸结识技术大神孙哥为好友,获益匪浅。现在把孙哥视频分享给大家。 孙哥链接:孙哥个人主页 作者简介:一个颜值99分,只比孙哥差一点的程序员 本专栏简介:话不多说,让我们一起干翻JavaScript 本文章简介:话不多说,让我们讲清楚虚拟机栈存储结构和运行原理…

LabVIEW实现变风量VAV终端干预PID控制

LabVIEW实现变风量VAV终端干预PID控制 变风量&#xff08;VAV&#xff09;控制方法的研究一直是VAV空调研究的重点。单端PID控制在温差较大时&#xff0c;系统容易出现过冲。针对空调终端单端PID控制的不足&#xff0c;设计一种干预控制与PID控制耦合的控制方法。项目使用LabV…

23个优秀开源免费BI仪表盘

BI也称为商业智能&#xff0c;是收集、分析和展示数据以支持决策者做出明智的业务决策的过程。BI帮助组织将其原始的生产数据转化为有意义的见解或者知识&#xff0c;以推动其业务战略。BI能够为组织改善决策、提高效率和提升资源利用率。 BI仪表盘是BI系统的重要组成部分&…

【计算机网络】数据链路层-MAC和ARP协议

文章目录 1. 认识以太网2. MAC协议MAC帧的格式MAC地址和IP地址的区别MTU 3. 局域网通信原理碰撞检测和避免 4. ARP协议ARP数据报的格式ARP缓存 1. 认识以太网 网络层解决的是跨网络点到点传输的问题&#xff0c;数据链路层解决的是同一网络中的通信。 数据链路层负责在同一局域…

python多进程(二)一些基础属性和方法

import multiprocessing# 定义一个任务函数 def worker(num):print(fWorker {num} started)# 这里可以执行具体的任务逻辑# ...# 创建多个进程 if __name__ __main__:processes []for i in range(5):p multiprocessing.Process(targetworker, args(i,))processes.append(p)p…

Antd Procomponent 之 proForm - 高级表单

本文作者系360奇舞团前端开发工程师 ProForm 在原来的 Form 基础上增加一些语法糖和更多的布局设置&#xff0c;帮助我们快速的开发一个表单。同时添加一些默认行为&#xff0c;让我们的表单默认好用。分步表单&#xff0c;Modal 表单&#xff0c;Drawer 表单&#xff0c;查询表…

Positive Technologies:勒索软件运营商以泄露数据为由勒索受害者

“双重勒索”和“法律勒索”&#xff1a;勒索软件运营商的新手段。 Positive Technologies 专家提交了一份关于 2023 年第三季度当前网络威胁的研究报告。随着解码器的出现&#xff0c;勒索软件的比例正在下降&#xff0c;因为后者正在失去效力。现在&#xff0c;勒索软件团伙…

react:创建项目

一&#xff1a; 使用create-react-app // 默认创建reactjs的webpack打包项目 npm i create-react-app -g create-react-app 项目名// 创建ts项目打包项目 sudo npx create-react-app my-app --template typescript 二&#xff1a; 使用vite npm create vitelatest // 创建react…

吴恩达《机器学习》4-6->4-7:正规方程

一、正规方程基本思想 正规方程是一种通过数学推导来求解线性回归参数的方法&#xff0c;它通过最小化代价函数来找到最优参数。 代价函数 J(θ) 用于度量模型预测值与实际值之间的误差&#xff0c;通常采用均方误差。 二、步骤 准备数据集&#xff0c;包括特征矩阵 X 和目标…

设计模式之访问者模式

阅读建议 嗨&#xff0c;伙计&#xff01;刷到这篇文章咱们就是有缘人&#xff0c;在阅读这篇文章前我有一些建议&#xff1a; 本篇文章大概5000多字&#xff0c;预计阅读时间长需要5分钟。本篇文章的实战性、理论性较强&#xff0c;是一篇质量分数较高的技术干货文章&#x…

利用QT画图像的直方图

1.什么是直方图 直方图是一种图形化展示数据频率分布的方式。它将样本数据分成一系列相邻的区间&#xff0c;统计每个区间内数据所占比例或数量&#xff0c;并用矩形条形图表现出来。直方图可以反映样本数据的分布情况&#xff0c;例如它们的集中趋势、对称性和离散程度等。 …

Java2 - 数据结构

5 数据类型 5.1 整数类型 在Java中&#xff0c;数据类型用于定义变量或表达式可以存储的数据的类型。Java的数据类型可分为两大类&#xff1a;基本数据类型和引用数据类型。 byte&#xff0c;字节 【1字节】表示范围&#xff1a;-128 ~ 127 即&#xff1a;-2^7 ~ 2^7 -1 sho…

基于Kinect 动捕XR直播解决方案 - 硬件篇

Kinect-V2 硬件设备 一、Kinect介绍 1、Kinect for Windows 的开发配置 Kinect V2 操作系统&#xff1a;Windows 10&#xff08;必须&#xff09; Windows Surface Windows Surface 2 开发环境&#xff1a;Visual Studio 2017 .NET Framework 4.5 (.NET Framework 4.5) 硬…

VEX —— Quaternion|Euler Angle

目录 一&#xff0c;四元数相关概念 四元数 欧拉角 常用四元数相关函数 相互转换 二&#xff0c;案例 案例&#xff1a;沿面中心翻转 案例&#xff1a;路径导弹 案例&#xff1a;RBD刚体还原过渡 一&#xff0c;四元数相关概念 四元数 在vex内四元数为&#xff08;&am…

【C/PTA】数组练习(编程)

本文结合PTA专项练习带领读者掌握数组&#xff0c;刷题为主注释为辅&#xff0c;在代码中理解思路&#xff0c;其它不做过多叙述。 文章目录 7-1 计算最大值出现的次数7-2 求一批整数中出现最多的个位数字7-3 装箱问题7-4 数组-值钱的微信号7-5 数组-吹泡泡7-6 数组-数学鬼才 7…

代码随想录Day42-图论:力扣第417m、841m、463e题

417m. 太平洋大西洋水流问题 题目链接 代码随想录文章讲解链接 方法一&#xff1a; 用时&#xff1a;1h0m58s 思路 直接找哪些点既可以到达太平洋又可以到达大西洋比较麻烦&#xff0c;换个角度&#xff0c;找到太平洋可以逆流而上到达的点&#xff0c;再找到大西洋可以逆…

BigDecimal 类型的累加操作

BigDecimal 累加操作 .add操作

C语言:深入浅出qsort方法,编写自己的qsort完成冒泡排序

目录 什么是qsort&#xff1f; 函数原型 比较函数 compar 排序整型数组 排序结构体数组 根据成员字符排序 strcmp函数 根据成员整型排序 自定义qsort实现冒泡排序 qsort的实现原理 具体步骤 快速排序示例代码&#xff1a; 什么是qsort&#xff1f; qsort是 C …

geoserver发布同一字段的多值渲染

Geoserver之同一字段的多值渲染 有时候我们需要对一个shp的某一字段值中的不同值进行区分展示&#xff0c;但是一般的渲染都是按照统一图层展示的&#xff0c;因此为了更好的效果&#xff0c;我们选择使用uDig等工具处理。 文章目录 Geoserver之同一字段的多值渲染前言一共是分…