分析 Spring 的依赖注入模式

简介: 分析 Spring 的依赖注入模式

一、依赖注入

依赖注入 (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”.


42.png

42.png

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


二、Field Injection

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


@Component
public 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 的建立与依赖的注入是同时发生的


@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;
       // ...
   }
   // ...
}

优点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,让测试程式码看起来更干净了,我们也能轻松的用 来实体化待测对象、注入假依赖,整体而言看起来更 清楚、好理解,就算是不熟 Java 或 Mockito 的开发人员应该也能看得懂七八成,对于新人也比较好上手,而且也比较不会有误用 @Annotation 所产生额外成本 ,优秀的单元测试就应该如此。new


优点4. Immutable Object

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


缺点:循环依赖

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


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

43.png

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


四、总结

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


相关文章
|
29天前
|
Java 调度 开发者
spring的@Scheduled()有几种定时模式?
【10月更文挑战第12天】spring的@Scheduled()有几种定时模式?
71 1
|
1月前
|
Java BI API
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
这篇文章介绍了如何在Spring Boot项目中整合iTextPDF库来导出PDF文件,包括写入大文本和HTML代码,并分析了几种常用的Java PDF导出工具。
403 0
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
|
1月前
|
XML Java 应用服务中间件
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
167 2
|
2月前
|
设计模式 Java Spring
spring源码设计模式分析(五)-策略模式
spring源码设计模式分析(五)-策略模式
|
2月前
|
负载均衡 Java 网络架构
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
实现微服务网关:Zuul与Spring Cloud Gateway的比较分析
100 5
|
2月前
|
消息中间件 设计模式 缓存
spring源码设计模式分析(四)-观察者模式
spring源码设计模式分析(四)-观察者模式
|
2月前
|
设计模式 Java Spring
spring源码设计模式分析(六)-模板方法模式
spring源码设计模式分析(六)-模板方法模式
|
2月前
|
XML 存储 Java
Spring-源码深入分析(二)
Spring-源码深入分析(二)
|
2月前
|
XML 设计模式 Java
Spring-源码深入分析(一)
Spring-源码深入分析(一)
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。