分析 Spring 的依赖注入模式

依赖注入 (Dependency Injection, DI) 是 Spring 实现控制反转概念的重要手段。Spring 提供了数种 DI patterns,其中最方便、最常用的是 field injection,它应该是许多人第一次写 Spring 专案时所使用的 pattern,虽然这方式简单易用,却有不少缺点。

例如你会发现, IntelliJ IDEA 会很贴心地告诉我们:

Field Injection is not recommended.

Spring Team recommends: "Always use constructor based dependency injection in tour beans. Always use assertions for mandatory dependencies".

kaisheng714.github.io - Field Injection is not recommended

为何 constructor injection 优于 field injection 呢?接下来我会解析这两种 pattern. (虽然 Spring 还有其他种注入方式,但我比较不常用,所以就不在此介绍了)

Field Injection

这种注入方式顾名思义,就是直接在 field 加上 @Autowired

@Componentpublic class HelloBean {     @Autowired private AnotherBean anotherBean;     @Autowired private AnotherBean2 anotherBean2;     // ...

优点

简单方便易用,只要短短一行即可完成。程式码最少,读起来真舒服

缺点

不易维护,因为简单方便,更容易产生 code smell 而不自知,例如 God Object不好写单元测试,测试环境需要透过 DI container 并加上许多 @Annotation 来初始化,看起来更像整合测试了。而且编译、执行时会多一些 overhead。不好理解测试,以下程式为例
@RunWith(MockitoJUnitRunner.class)public class HelloBeanTest {    @Mock    private AnotherBean anotherBean;        @Mock    private AnotherBean2 anotherBean2;        ...        @Mock    private AnotherBean10 anotherBean10;        @InjectMocks    private HelloBean helloBean;        @Before    public void setup() {        ...    }        // Test cases...}

这是相当常见的 Mockito+Junit 单元测试写法,但容易造成疑问:

@RunWith(MockitoJUnitRunner.class) 是什么意思 ?@InjectMocks 做了什么 ?是否需要将待测物件 HelloBean 实体化呢 ?如果有两个 AnotherBean 类型的依赖怎么办 ?

只有短短几行就让人产生诸多疑问,因此理解成本较高。虽然这种注入方式很简单方便,但写单元测试时就得还债了。若使用 constructor injection 则不易产生此问题,我们接着看下去:

Constructor Injection

此方式最大的特点是: Bean 的建立与依赖的注入是同时发生的

@Componentpublic class HelloBean {    private final AnotherBean anotherBean;   private final AnotherBean2 anotherBean2;   // ...      @Autowired   public HelloBean(AnotherBean anotherBean, AnotherBean2 anotherBean2, ...) {       this.anotherBean = anotherBean;       this.anotherBean2 = anotherBean2;       // ...   }      // ...}

优点1. 容易发现 code smell

假设我们需要注入十几个 dependecies,对比 field injection 的方式,这种方式暴露了 constructor 中含有过多的参数 (Long Parameter List),这是个很好的臭味侦测器,正常的开发者看到这么多参数肯定是会头痛的,这就表示我们需要想办法重构它,尽可能使它符合单一职责原则 (Single Responsibility Principle)。

优点2. 容易釐清依赖关係

一看到 constructor 就可以让开发者釐清这个物件所需要的 dependency,且缺一不可,进而缩小该物件在专案中的使用範围,事物的範围越窄,就越容易理解与维护。另外,我们也可以透过 constructor 注入假的依赖,进而容易写单元测试。

优点3. 容易写单元测试

一个简单的範例:

public class HelloBeanTest {        private HelloBean helloBean;        @Before    public void setup() {        AnotherBean anotherBean = mock(AnotherBean.class);        AnotherBean2 anotherBean2 = mock(AnotherBean2.class);        // ...        helloBean = new HelloBean(anotherBean, anotherBean2, ...);    }      // Test cases...}

相较前面的例子,这种注入方式不需要太多 @Annotation,让测试程式码看起来更乾净了,我们也能轻鬆的用 new 来实体化待测物件、注入假依赖,整体而言看起来更 清楚、好理解,就算是不熟 Java 或 Mockito 的开发人员应该也能看得懂七八成,对于新人也比较好上手,而且也比较不会有误用 @Annotation 所产生额外成本,优秀的单元测试就应该如此。

优点4. Immutable Object

意思是 Bean 在被创造之后,它的内部 state, field 就无法被改变了。不可变意味着唯读,因而具备执行绪安全 (Thread-safety) 的特性。此外,相较于可变物件,不可变物件在一些场合下也较合理、易于了解,而且提供较高的安全性,是个良好的设计。因此,透过 constructor injection,再把依赖宣都告成 final,就可以轻鬆建立 Immutable Object。

缺点:循环依赖

只有在使用 constructor injection 时才会造成此问题。

举个简单的例子,若依赖关係图: Bean C → Bean B → Bean A → Bean C ,则会造成造成此问题,程式在 Runtime 会抛出BeanCurrentlyInCreationException,更白话来说,这就是鸡生蛋 / 蛋生鸡的问题,而 Spring 容器初始化时无法解决这样的窘境,因此抛出例外并中断程式。

循环依赖问题 Circular dependency issues

但是,Circular dependency 其实算是一种 Anti-Pattern,所以如果能够即时发现它,提早让开发人员意识到该问题重新设计此 bean,我个人认为这点反而蛮好的。

总结

本文介绍了两种依赖注入模式,它们各有好坏,也都能达到同样的目的,而比较常见的是 field injection,但不幸的这种方式较可能会写出 code smell。另外,Spring 官方团队建议开发者使用 constructor injection,虽然可能会有循环依赖异常,但无论在开发、测试方面,总体而言都是利大于弊,我也一直遵循这个模式。

References

本文转录自 https://kaisheng714.github.io/articles/analyzing-dependency-injection-patterns-in-spring

更多你可能会感兴趣的文章

如何提高程式码的可测试性 (Testability)Spring + Maven + IntelliJ 多环境 (Profile) 整合技巧

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章