依赖注入 (Dependency Injection, DI) 是 Spring 实现控制反转概念的重要手段。 Spring 提供了多种依赖注入方式,其中最方便、最常用的是 field injection,它应该是许多人第一次写 Spring 项目时所使用的模式,虽然这方式简单易用,却有不少缺点。
例如你会发现, 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”.
为何 constructor injection 优于 field injection 呢? 接下来我会解析这两种模式。 (虽然 Spring 还有其他种注入方式,但我比较不常用,所以就不在此介绍了)
这种注入方式顾名思义,就是直接在 field 加上 @Autowired
@Component
public class HelloBean {
@Autowired private AnotherBean anotherBean;
@Autowired private AnotherBean2 anotherBean2;
// ...
@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 单元测试写法,但容易造成疑问:
只有短短几行就让人产生诸多疑问,因此理解成本较高。 虽然这种注入方式很简单方便,但写单元测试时就得还债了。 若使用 constructor injection 则不易产生此问题,我们接着看下去:
此方式最大的特点是: Bean 的建立与依赖的注入是同时发生的
@Component
public class HelloBean {
private final AnotherBean anotherBean;
private final AnotherBean2 anotherBean2;
// ...
@Autowired
public HelloBean(AnotherBean anotherBean, AnotherBean2 anotherBean2, ...) {
this.anotherBean = anotherBean;
this.anotherBean2 = anotherBean2;
// ...
}
// ...
}
假设我们需要注入十几个 dependecies,对比 field injection 的方式,这种方式暴露了 constructor 中含有过多的参数 (Long Parameter List),这是个很好的臭味侦测器,正常的开发者看到这么多参数肯定是会头痛的,这就表示我们需要想办法重构它,尽可能使它符合单一职责原则 ( Single Responsibility Principle)。
一看到 constructor 就可以让开发者厘清这个物件所需要的 dependency,且缺一不可,进而缩小该物件在项目中的使用范围,事物的范围越窄,就越容易理解与维护。 另外,我们也可以透过 constructor 注入假的依赖,进而容易写单元测试。
一个简单的范例:
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,让测试程式码看起来更干净了,我们也能轻松的用 来实体化待测对象、注入假依赖,整体而言看起来更 清楚、好理解,就算是不熟 Java 或 Mockito 的开发人员应该也能看得懂七八成,对于新人也比较好上手,而且也比较不会有误用 @Annotation 所产生额外成本 ,优秀的单元测试就应该如此。new
意思是 Bean 在被创造之后,它的内部 state, field 就无法被改变了。 不可变意味着只读,因而具备线程安全(Thread-safety)的特性。 此外,相较于可变对象,不可变对象在一些场合下也较合理、易于了解,而且提供较高的安全性,是个良好的设计。 因此,透过 constructor injection,再把依赖宣都告成 final,就可以轻松建立 Immutable Object。
只有在使用 constructor injection 时才会造成此问题。
举个简单的例子,若依赖关系图: Bean C → Bean B → Bean A → Bean C ,则会造成造成此问题,程序在 Runtime 会抛出,更白话来说,这就是鸡生蛋 / 蛋生鸡的问题,而 Spring 容器初始化时无法解决这样的窘境,因此抛出例外并中断程序。BeanCurrentlyInCreationException
但是,Circular dependency 其实算是一种 Anti-Pattern,所以如果能够实时发现它,提早让开发人员意识到该问题重新设计此 bean,我个人认为这点反而蛮好的。
本文介绍了两种依赖注入模式,它们各有好坏,也都能达到同样的目的,而比较常见的是 field injection,但不幸的这种方式较可能会写出 code smell。 另外,Spring 官方团队建议开发者使用 constructor injection,虽然可能会有循环依赖异常,但无论在开发、测试方面,总体而言都是利大于弊,我也一直遵循这个模式。