依赖注入

讲真的,今年就业形势相当不好,对IT从业者的专业能力要求将变得更高。近来一边忙学业,一边学习Java基础和框架源码,为不久之后的招聘做准备。

打算从Java7、Java8d的版本新特性着手,再深入JVM、并发编程等。Java7中发布了JSR-330标准的DI特性。IoC是控制反转,DI是依赖注入。依赖注入(控制反转的一种形式)是Java开发主流中一个重要的范式。

一、理解IoC和DI

IoC(控制反转)

非IoC范式编程,“功能中心”控制程序逻辑的流程,调用各个可重用对象中的方法执行特定的功能。

IoC程式编程,调用者的代码来处理程序的执行顺序,而程序逻辑则被封装在接受调用的子流程中。

IoC也被称为好莱坞原则,其思想可以归结为会有另一端代码拥有最初的控制线程(容器/工厂),并且有它来调用你的代码(注入/实例化对象),而不是由你的代码调用它。

好莱坞原则 – “不要给我们打电话,我们会打给你”
好莱坞经纪人总是给人打电话,而不是让别人打给他们!

IoC—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。如何理解好Ioc呢?理解好Ioc的关键是要明确“谁控制谁,控制什么,为何是反转(有反转就应该有正转了),哪些方面反转了”,那我们来深入分析一下:

谁控制谁,控制什么:传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。


为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

IoC应用

降低代码间的耦合度,让代码更易于测试、更易读、内聚性更强

IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。
IoC很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由IoC容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。

IoC实现方式

包括工厂模式,服务器定位模式,依赖注入(DI)。

DI(依赖注入)

DI—Dependency Injection,即“依赖注入”:是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。


DI是IoC的一种特定形态,是指寻找依赖项的过程(实例化)不在当前执行代码的直接控制之下。通常使用自带IoC容器的DI框架来实现依赖注入机制,如Guice,Spring。IoC可以看作运行时环境。


依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:

谁依赖于谁:当然是应用程序依赖于IoC容器;
为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;
谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;


●注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

IoC和DI由什么关系呢?其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。

二、DI实现实例

找出所有对Java开发人员比较友善的好莱坞经纪人

有个AgentFinder接口,及其两个实现类:

1
2
3
4
5
6
7
8
9
public abstract class AgentFinder {
public abstract List<String> getAllAgents();
}
public class DevAgentFinder extends AgentFinder {
public List<String> getAllAgents() {...}
}
public class BankAgentFinder extends AgentFinder {
public List<String> getAllAgents() {...}
}

在AgentFinderService中使用AgentFinder查找对Java开发人员友好的经纪人,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AgentFinderService {
public List<String> getGoodAgents() {
AgentFinder finder = new DevAgentFinder();
List<String> allAgents = finder.getAllAgents();
return filterAgents(allAgents);
}
private List<String> filterAgents(List<String> agents) {
List<String> fitAgents = new ArrayList<>();
for (String agent : agents) {
if (agent.contains("Java")) {
fitAgents.add(agent);
}
}
return fitAgents;
}
}

以上代码,AgentFinderService和DevAgentFinder紧密黏合,使用工厂模式和服务器定位模式可降低耦合,它们都是IoC的一种。

使用工厂/服务器定位模式

1
2
3
4
5
6
7
8
9
public class AgentFinderService {
public List<String> getGoodAgents(String agentFinderType) {
AgentFinderFactory factory = AgentFinderFactory.getInstance();
AgentFinder finder = factory.getAgentFinder(agentFinderType);
List<String> allAgents = finder.getAllAgents();
return filterAgents(allAgents);
}
private List<String> filterAgents(List<String> agents) {...}
}

AgentFinderFactory根据注入的agentFinderType实例化令人满意的AgentFinder。仍存在问题:

  • 代码注入agentFinderType作为引用凭据,而没有注入真正的对象。
  • getGoodAgents仍存在其他依赖项,达不到只关注自身职能的状态。

使用DI

1
2
3
4
5
6
7
public class AgentFinderService {
public List<String> getGoodAgents(AgentFinder finder) {
List<String> allAgents = finder.getAllAgents();
return filterAgents(allAgents);
}
private List<String> filterAgents(List<String> agents) {...}
}

如上AgentFinder被直接注入到getGoodAgents方法中,只专注于纯业务逻辑。存在问题,如何配置AgentFinder具体实现?原本AgentFinderFactory要做的事情只是换个地方完成。

使用JSR-330 DI

使用框架执行DI操作,DI框架用标准的JSR-330@Inject注解将依赖项注入到getGoodAgents方法中:

1
2
3
4
5
6
7
public class AgentFinderService {
@Inject public List<String> getGoodAgents(AgentFinder finder) {
List<String> allAgents = finder.getAllAgents();
return filterAgents(allAgents);
}
private List<String> filterAgents(List<String> agents) {...}
}

如上,AgentFinder的某个具体实现类的实例由支持JSR-330@inject注解的DI框架在运行时注入。

JSR 企业应用标准:
JSR-330: Dependency Injection for Java 1.0
JSR-330统一DI体系,对大多数Java DI框架的核心功能做了很好的汇总

从以上改造来温故依赖注入对我们的帮助:

  • 松耦合
  • 可测性
  • 更强的内聚性
  • 可重用组件
  • 更轻盈的代码

三、Java中标准化DI

DI新标准中,javax.inject包只是提供一个接口和几个注解类型,这些都会被遵循JSR-330标准的各种DI框架实现。

理解DI工作原来

优秀的Java开发人员不能只满足于使用类库和框架,要明白内部基本工作原理。在DI领域,会面临各种问题,如依赖项配置错误、依赖项诡异地超出作用域、依赖项在不该共享时被共享、分布调试离奇宕机等。

理解javax.inject包:

javax.inject包
这个包指明了获取对象的一种方式,与传统的构造方法、工厂模式、服务器定位模式(如JNDI)等相比,这种方式的可重用性、可测试性、可维护性都有极大提升。这种方式成为依赖注入。

javax.inject包中包括一个Provider接口和5个注解类型(@inject、@Qualifier、@Named、@Scope、@Singleton)。

@Inject 注解

@Inject注解可以出现在三种类成员之前,表示该成员需要依赖注入。按运行时处理顺序:

  1. 构造器
  2. 方法
  3. 属性

构造器上使用@Inject

在构造器上使用@Inject时,其参数在运行时由配置好的IoC容器提供。比如在下面的代码中,运行时调用AgentFinderService的构造器时,IoC容器会注入其参数AgentFinder。

1
2
3
4
5
6
public class AgentFinderService {
private final AgentFinder finder;
@Inject public AgentFinderService(AgentFinder finder) {
this.finder = finder;
}
}

注意

因为JRE无法决定构造器注入的优先级,所以规范中规定类中只能有一个构造器带@Inject注解

方法上使用@Inject

运行时可注入的参数可以是多个也可以是0个,使用参数注入的方法不能声明为抽象方法,也不能声明其自身的类型参数。下面这段代码在set方法前使用@Inject,这是注入可选属性的常用技术。

1
2
3
@Inject public void setContent(Content contnet) {
this.content = content;
}

向方法中注入参数技术对于服务类方法来说非常有用,其所需的资源可以作为参数注入,比如向查询数据库的服务方法中注入数据访问对象(DAO)。

向构造器注入的通常是类中必需的依赖项,而对于非必需的依赖项,通常是在set方法上注入。比如已经给出了默认的属性就是非必需的依赖项。

属性上使用@Inject

简单直接,但最好不要用。因为这样可能会使单元测试更加困难。

1
2
3
public class AgentFinderService {
@Inject private final AgentFinder finder;
}

@Qualifier 注解

JSR-330规范使用@Qualifier限定(标识)要注入的对象,比如IoC容器有两个类型相同的对象,当把他们注入到你的代码中时,要把他们区分开来。
image.png
创建一个@Qualifier实现必须遵循如下规则:

  • 必须标记为@Qualifier和@Retention(RUNTIME),以确保该限定注解在运行时一直有效。
  • 通常还要加上@Documented注解,这样该实现就能加到API的公共JavaDoc中了。
  • 可以有属性。
  • @Target注解可以限定其使用范围。

示例如下:

1
2
3
4
5
6
7
@Documented
@Retention
@Qualifier
public @interface MusicGenre {
Genre genre() default Genre.TRANCE;
public enum GENRE { CLASSICAL, METAL, ROCK, TRANCE }
}

@Named 注解

@amed@Named是一个特别的@Qualifier注解,借助@Named可以用名字注明要注入的对象。将@Named和@Inject一起使用,符合指定名称并且类型正确的对象会被注入。

1
2
3
4
public class AgentFinderService {
@Inject @Named("devFinder") private final AgentFinder devFinder;
@Inject @Named("bankFinder") private final AgentFinder bankFinder;
}

@Scoped 注解

@Scoped注解用于自定义注解器(IoC容器)对注入对象的重用方式。JSR-330默认了如下几种默认行为:

  • 如果未声明任何@Scope注解接口的实现,注入器应创建注入对象并且仅使用该对象一次。
  • 如果声明了@Scoped注解接口,注入对象的声明周期由所声明的@Scoped注解实现决定。
  • 如果注入对象在@Scoped实现中要由多个线程使用,则需保证注入对象的线程安全性。
  • 如果某个类上声明了多个@Scoped注解,或声明了不受支持的@Scoped注解,IoC容器应该抛出异常。

公认的通用@Scoped实现只有@Singleton一个,JSR-330只确定了这么一个标准的生命周期注解。

@Singleton 注解

@Singleton注解接口在DI框架中应用广泛,需要注入一个不会改变的对象时,就要用@Singleton。大多数DI框架都将@Singleton作为注入对象的默认声明周期,无需显式发明。

1
2
3
public class AgentFinderService {
@Inject @Singleton private AgentFinder devFinder;
}

接口Provider

当DI框架的标准注解不能满足你的需求,你想对DI框架注入代码中的对象拥有更多的控制权,可以要求DI框架将Provider接口实现注入对象。

  • 可以获取该对象的多个实例。
  • 可以延迟加载对象。
  • 可以打破循环依赖。
  • 可以定义作用域,能在比整个被加载的应用小的作用域中查找对象。

该接口仅有一个T get()方法,这个方法会返回一个构造好的注入对象(T)。

1
2
3
4
5
6
7
8
public class AgentFinderService {
@Inject public AgentFinderService(AgentFinderProvider provider) {
AgentFinder finder = provider.get();
if (condition) { // 延迟加载
AgentFinder finder2 = provider.get(); // 多个实例对象
}
}
}

四、DI参考实现:Guice3

Guice3是JSR-330规范的完整参考实现,可以配置、绑定、注入依赖项。

实现DI

创建绑定关系

先创建绑定关系AgentFinderModule,重写configure()声明绑定关系,当AgentFinderService要求@Inject一个AgentFinder时,就会绑定DevAgentFinder作为注入对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AgentFinderModule extends AbstractModule {
@Override
protected void configure() {
bind(AgentFinder.class).to(DevAgentFinder.class);
}
}
public class AgentFinderService {
private final AgentFinder finder;
@Inject public AgentFinderService(AgentFinder finder) {
this.finder = finder;
}
public List<String> getGoodAgents() {
List<String> allAgents = finder.getAllAgents();
return filterAgents(allAgents);
}
...
}

代码4-1

构建Guice对象关系图

1
2
3
4
5
6
7
8
9
public class AgentApplication {

public static void main(String[] args) {
Injector injector = Guice.createInjector(new AgentFinderModule());
AgentFinderService hollywoodService = injector.getInstance(AgentFinderService.class);
List<String> agents = hollywoodService.getGoodAgents();
System.out.println(agents);
}
}

代码4-2

Guice的各种绑定

Guice提供多种绑定方式:

  • 链接绑定
  • 绑定注解
  • 实例绑定
  • @Provides方法
  • Provider绑定
  • 无目标绑定
  • 内置绑定
  • 及时绑定

最常用的包括链接绑定、绑定注解、@Provides方法、Provider绑定。

链接绑定

代码4-1中AgentFinderModule即为链接绑定,是最简单的绑定方式,只是告诉注入器运行时应该注入实现类或扩展类(可以直接注入子类)。

绑定注解

将注入类的类型和额外的标识符组合起来,以标识恰当的注入对象。使用JSR-330标准注解@Named,注入特定名称的AgentFinder,在AgentFinderModule中配置@Named绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AgentFinderModule extends AbstractModule {
@Override
protected void configure() {
bind(AgentFinder.class)
.annotatedWith(Names.named("primary"))
.to(DevAgentFinder.class);
}
}
public class AgentFinderService {
private final AgentFinder finder;
@Inject
public AgentFinderService(@Named("primary") AgentFinder finder) {
this.finder = finder;
}
}

@Provides和Provider:提供完全定制的对象

需要注入特别的AgentFinder,使用@Provides注解或在configure()方法中绑定,注入器会查看左右标记了@Provides注解方法的返回类型,决定 注入哪个对象。

1
2
3
4
5
6
7
8
9
10
public class AgentFinderModule extends AbstractModule {
@Override
protected void configure() {...}
@Provides
AgentFinder provideAgentFinder() { // 返回注入器需要的类型
DevAgentFinder finder = new DevAgentFinder(); // 创建实例并定制
finder.setName("JavaFind");
return finder;
}
}

@Provides方法会变得越来越大,为简化Module,需要把定制化代码拆分出去。使用toProvider方法绑定到Provider类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AgentFinderProvider implements Provider<AgentFinder> {
@Override
public AgentFinder get() {
DevAgentFinder finder = new DevAgentFinder(); // 创建实例并定制
finder.setName("JavaFind");
return finder;
}
}
public class AgentFinderModule extends AbstractModule {
@Override
protected void configure() {
bind(AgentFinder.class)
.toProvider(AgentFinderProvider.class);
}
}

参考

https://jinnianshilongnian.iteye.com/blog/1413846
《Java程序员修炼之道 Benjamin J.Evans》第3章


依赖注入
https://blackist.org/2019-06-13-java-java7-ioc/
作者
董猿外
发布于
2019年6月13日
许可协议