从业务的角度来说说Spring为什么不推荐字段注入

在我们的日常开发中,会发现一些代码中充斥着 @Autowired,这种做法在我们的业务代码中非常的常见,但是如果仔细观察,你就会发现 IntelliJ 会出现一个 warning

image-20221022151757174

不推荐使用字段进行注入???我用了这么长时间的注入方式,竟然被 Spring 不推荐了?

然后我们的其他同学就放弃了 @Autowired 注解,转而使用 @Resource 注解,虽然说 IDEA 不再告警,但是从业务的角度上来说,还是不推荐这样处理。

看到这里,顺带提一句,Spring 有哪几种构造方式?这个是经典八股了,如果没有背熟的,各打一个大板,答案是:

  1. field
  2. setter
  3. constructor

既然 Field injection 已经被标记为不推荐,那么 setter 和 constructor 呢?

image-20221022152417264

可以看到非常的干净,没有一个告警,那么起码说明这种方式,Spring 没说不推荐,可以用,构造器注入也一样,在这里就不贴代码了,感兴趣可以自己去试试。

从官方文档中,可以看到,对于 setter 和 constructor 的注入方式,Spring 优先使用 constructor。

Spring5.3.23-core

The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null.

即通过构造器注入的 Bean,是可以确保一定不是为 null 的。

那么有同学可能会说,我一个类里面几十个 bean,那通过构造器的注入不会就导致几十个入参吗。Spring 也考虑过这个情况,而且给出的建议是让你重构这个类。

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.

正常的情况下,如果一个类依赖于太多的 bean,那么说明该类的职责太多,如果不重构,后续肯定会成为一个超级复杂的类,然后成为无人敢动的屎山。

下面从两个角度来说明下,为什么还是在代码中使用构造器注入

构造器注入的好处

1)阻止业务类循环依赖

虽然说通过字段注入的时候,Spring可以帮助我们解决循环依赖的问题,但是一旦业务中两个类产生了循环依赖,那就说明设计不合理,需要进行业务的拆分。

用构造器注入,如果产生了循环依赖,可以在启动的时候直接报错,让业务人员进行优化,从而避免坏味道的模块。

2)防止业务类大杂烩

当习惯通过字段注入以后,很容易就形成一个超级复杂的单体类,在业务中也经常会发现一些偷懒的情况,类的职责远远大于命名。

例如一个类的名称是 OrderService,负责处理下单、取消订单、退货。

如果用的是字段注入,很容易就会把一些额外的功能引入进来,比如支付逻辑,保价逻辑等等,到最后就会越来越臃肿。

当然也不是说构造器注入可以避免这一种情况,但是最起码在加入另一个 Bean 的时候,可以考虑下是不是有必要放到这一个类。

3)可以写出优秀的单元测试代码

在业务开发的过程中,一般核心业务都会对单元测试有要求,至少核心的方法必须要跑单元测试,整个项目的单元测试覆盖率必须要达到一定的指标。

单元测试的麻烦点在于数据的构造,如果被单测的类依赖于 DB 或者 NoSQL 中的数据,本地开发的时候还好,可以有 DEV 环境进行单测。

而如果测试环境,禁止访问容器外部的网络,比如我们的 UAT 环境,在跑单元测试的时候,是禁止访问 docker 容器外部的网络,以避免单元测试对于测试环境产生干扰。

这个时候,如果跑单元测试覆盖率就会失败。从而影响整体的代码覆盖率,间接的可能影响到你的工资。。

3.1)Mockito单元测试

为了解决上述的问题,我们在单元测试的时候,是直接进行 Mock 返回结果进行处理,例如如下 Demo

image-20221022160718868

这个类有两个方法,其中一个就是简单的进行加法计算,而另一个需要调用第三方服务进行支付,然后返回真实的支付价格。这个时候针对这个类写一个单元测试。

image-20221022160849926

add 方法可以看到是没问题的,1 + 2 = 3,但是如果需要对 getPayFee 进行 Mock 测试,那么我们就需要 Mock 一个 payService。

这个类需要在不访问外部网络的情况下,返回我们所需要的数据。

image-20221022161253322

果不其然,直接报错了,那么我们就需要 Mock 一个 payService。

这个时候,问题来了,因为 FeeService 是采用字段注入的,我们怎么才能将 PayService 注入进去呢?

不优雅的方案

直接反射进行注入:

image-20221022162018399

可以看到,我们在这边直接将 mock 出来的 payService 通过反射注入到 FeeService 以后,单测可以跑通了,但是非常的不优雅。

优雅的解决方法

构造器注入

image-20221022162227601

image-20221022162313669

可以看到单测已经没问题了。相比于字段注入,这个方式的优点是不需要额外的反射处理

总结

如果是以前的旧项目,如果没必要可以不用进行重构,但是对于新的业务项目,如果有机会,可以尝试用构造器进行注入。

如果是核心项目或者对单元测试覆盖率有要求的话,建议直接要求必须使用构造器注入,方便单元测试的编写。

从业务的角度来说说Spring为什么不推荐字段注入

https://somersames.github.io/2022/10/22/why-spring-recommended-construction-inject/

作者

Somersames

发布于

2022-10-22

更新于

2022-10-22

许可协议

评论