关于spring bean三种注入方式的优缺点对比,翻译自Spring DI Patterns: The Good, The Bad, and The Ugly,水平有限,如有错误请指正。
Spring开发者会很熟悉spring强大的依赖注入API,这些API可以让你用@Bean的注解让Spring实例化和管理Bean。Bean之间的任何依赖都会被spring解析和注入。
三种依赖于注解的注入方法
spring有三种注解的方式让你来声明类的依赖。
- 字段注入(坏的)
import org.springframework.beans.factory.annotation.Autowired;
public class MyBean {
@Autowired
private AnotherBean anotherBean;
//Business logic...
}
- 设值注入(丑的)
import org.springframework.beans.factory.annotation.Autowired;
public class MyBean {
private AnotherBean anotherBean;
@Autowired
public void setAnotherBean(final AnotherBean anotherBean) {
this.anotherBean = anotherBean;
}
//Business logic...
}
- 构造器注入(好的)
public class MyBean {
private final AnotherBean anotherBean;
public MyBean(final AnotherBean anotherBean) {
this.anotherBean = anotherBean;
}
//Business logic...
}
字段注入难以忽视的真相
这几种方式中最常用的就是字段注入,很有可能是因为这是最方便的方式。不幸的是,因为它的普遍性,开发者很少了解到其他两种方式相互之间的优缺点。
使用字段注入的类会变得越来越难以维护
当你用的字段注入模式,并且想在类里增加依赖时,你只需要加一个字段,然后加上@Autowired或者@Inject注解,然后就可以走了。听起来很棒,但几个月以后,你的类就会变成只有上帝才能理清楚的类了。 当然,这也很可能发生在另外两中方式上,但是另两种方式能迫使你更关注类中的依赖关系。
只要你用了字段注入,单测就没法做了
当我看了Josh Long关于Spring boot的演讲后,这句话就一直萦绕在我的脑海里, 从某种意义上来说,它也促使我写下这篇文章。你怎么测试字段注入的类?很有可能你正在回想那些不太直观的 Mockito 用法,就像这样。
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class MyBeanTest {
@Mock
private AnotherBean anotherBean;
@InjectMocks
private MyBean target;
//Tests...
}
这种利用反射的方式迫使开发者需要关注很多其他的地方,比如
- 如果MyBean有多个其他依赖怎么办?
- 我是否应该创建一个target实例,或者只是声明它?有什么不同?
- 当依赖用到泛型的时候你是否能保证类型安全?
- 如果你只需要部分依赖的真实实现怎么办?
用了字段注入的类都是非final的,容易产生循环依赖
如果是你想把@Autowired自动注入的字段声明为final类型的,编译器会直接报错,是不是很烦人。 而且这个字段只能被设置一次。除非你加了@Lazy注解,否则spring会在启动的时候去解析依赖图,你的bean可能因为循环依赖报出一个BeanCurrentlyInCreationException,例如:
public class A {
@Autowired
private B b;
}
public class B {
@Autowired
private C c;
}
public class C {
@Autowired
private A a;
}
现实中肯定不会出现这么简单的错误,但实际中可能会出现很多因为继承、跨类库,跨架构导致的依赖迷宫。这个问题可以提供把其中某个字段声明为非必须(可以通过@Autowired(required = false)允许为空),或者使用懒加载(使用@Lazy可以再解析完bean之后再设值)。遇到过这个Exception的人都知道,找到循环依赖中确切的一环是非常耗时耗力的工作。一旦你找到了,你如何确定牺牲那个依赖呢?你怎么恰当的把这些写到文档里呢?
spring中有很多种解决循环依赖的方法,而且现在有些方法开始变的很恶心了。
优点
- 最简洁
- 很多java开发者都喜欢这种方式
缺点
- 便利会弱化代码结构设计
- 很难测试
- 依赖不能是可变的(无法final)
- 容易出现循环依赖
- 需要使用到多个spring或者java注解
设值注入
模板和封装
三种方式里,设值注入是最模板化的,每个bean都必须有有个setter函数,每个setter函数必须加@Autowired或@Inject注解。这种方式你不用考虑你类依赖的数量问题,这算是另一种设计方式。 但你过多暴露类的内部,违反了开放封闭原则。
设值注入让单测变的简单
不需要反射的黑魔法,你只需要把你的依赖set进去。
import org.junit.Before;
import org.mockito.Mockito;
public class MyBeanTest {
private MyBean target = new MyBean();
private AnotherBean anotherBean = Mockito.mock(AnotherBean.class);
@Before
public void setUp() {
myBean.setAnotherBean(anotherBean);
}
//Tests...
}
设值注入对循环依赖免疫
使用设值注入,spring不会对你的bean做有向无环图依赖分析,这就意味着可以有循环依赖。允许循环依赖是把双刃剑,你不必处理那些因为循环依赖导致的恶心的问题,但你的代码以后也就很难分解开了。 试试上BeanCurrentlyInCreationException只是在启动时告诉你你的设计有问题。
优点
- 对循环依赖免疫
- 随着setter的添加,高度耦合的类很容易被识别出来。
缺点
- 违反开放封闭原则
- 会把循环依赖隐藏掉
- 三种方法里最模板化的方式
- 依赖不能是可变的(无法final)
终结方案:构造器注入
事实证明构造器注入是最佳的依赖注入解决方案。一些新的支持持续集成的平台,比如Angular,已经从其他平台吸取了教训,只支持构造器注入。
构造器注入能暴露出过度耦合的问题
无论什么时候你的类需要一个新的依赖,你都得加一个构造参数,这就会强迫你去审视你类的耦合度。我发现少于3个依赖是比较好的,如果多于5个依赖,就应该重构了。只在短短几行连续的代码上数有多少个依赖是很容易的。
额外的好处是,由于final字段可以在构造函数中初始化,所以我们的依赖关系可以是final的。恩,就应该是这样!
测试注入的构造函数类很简单
甚至比设值注入更简单。
import org.mockito.Mockito;
public class MyBeanTest {
private AnotherBean anotherBean = Mockito.mock(AnotherBean.class);
private MyBean target = new MyBean(anotherBean);
//Tests...
}
注入子类的构造函数必须具有非默认构造函数
使用构造函数注入的类的任何子类都必须具有调用父构造函数的构造函数。如果您继承了Spring组件,这就很麻烦了。我个人很少碰到这种情况。我尽量避免在父组件中注入依赖——我通常是通过组合而不是继承完成的。
优点
- 依赖可以是final的
- spring官方推荐的方式
- 三种方式里最容易测试的方式
- 高耦合类随着构造参数的增长很容易被识别出来
- 其他开发平台的开发者也很熟悉
- 不需要依赖@Autowired注解
缺点
- 构造函数需要下沉到子类
- 容易产生循环依赖
结论
有时候其他模式也有意义,但“为了与代码库的其余部分保持一致”和“使用字段注入模式更简单”并不是有效的借口。
例如,使用设值注入模式从xml setter注入方式迁移,或者需要修复BeanCurrentlyInCreationException问题时的中间状态,但并不意味着你最终就应该是这样。
甚至字段注入模式也足够了,例如,设计解决方案或回答StackOverflow上的问题时,除非他们的问题是关于Java中的依赖注入。在这种情况下,您应该用字段注入方便说明问题。