依赖注入
讲真的,今年就业形势相当不好,对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 |
|
在AgentFinderService中使用AgentFinder查找对Java开发人员友好的经纪人,
1 |
|
以上代码,AgentFinderService和DevAgentFinder紧密黏合,使用工厂模式和服务器定位模式可降低耦合,它们都是IoC的一种。
使用工厂/服务器定位模式
1 |
|
AgentFinderFactory根据注入的agentFinderType实例化令人满意的AgentFinder。仍存在问题:
- 代码注入agentFinderType作为引用凭据,而没有注入真正的对象。
- getGoodAgents仍存在其他依赖项,达不到只关注自身职能的状态。
使用DI
1 |
|
如上AgentFinder被直接注入到getGoodAgents方法中,只专注于纯业务逻辑。存在问题,如何配置AgentFinder具体实现?原本AgentFinderFactory要做的事情只是换个地方完成。
使用JSR-330 DI
使用框架执行DI操作,DI框架用标准的JSR-330@Inject注解将依赖项注入到getGoodAgents方法中:
1 |
|
如上,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
@Inject 注解
@Inject注解可以出现在三种类成员之前,表示该成员需要依赖注入。按运行时处理顺序:
- 构造器
- 方法
- 属性
构造器上使用@Inject
在构造器上使用@Inject时,其参数在运行时由配置好的IoC容器提供。比如在下面的代码中,运行时调用AgentFinderService的构造器时,IoC容器会注入其参数AgentFinder。
1 |
|
注意
因为JRE无法决定构造器注入的优先级,所以规范中规定类中只能有一个构造器带@Inject注解
方法上使用@Inject
运行时可注入的参数可以是多个也可以是0个,使用参数注入的方法不能声明为抽象方法,也不能声明其自身的类型参数。下面这段代码在set方法前使用@Inject,这是注入可选属性的常用技术。
1 |
|
向方法中注入参数技术对于服务类方法来说非常有用,其所需的资源可以作为参数注入,比如向查询数据库的服务方法中注入数据访问对象(DAO)。
向构造器注入的通常是类中必需的依赖项,而对于非必需的依赖项,通常是在set方法上注入。比如已经给出了默认的属性就是非必需的依赖项。
属性上使用@Inject
简单直接,但最好不要用。因为这样可能会使单元测试更加困难。
1 |
|
@Qualifier 注解
JSR-330规范使用@Qualifier限定(标识)要注入的对象,比如IoC容器有两个类型相同的对象,当把他们注入到你的代码中时,要把他们区分开来。
创建一个@Qualifier实现必须遵循如下规则:
- 必须标记为@Qualifier和@Retention(RUNTIME),以确保该限定注解在运行时一直有效。
- 通常还要加上@Documented注解,这样该实现就能加到API的公共JavaDoc中了。
- 可以有属性。
- @Target注解可以限定其使用范围。
示例如下:
1 |
|
@Named 注解
@amed@Named是一个特别的@Qualifier注解,借助@Named可以用名字注明要注入的对象。将@Named和@Inject一起使用,符合指定名称并且类型正确的对象会被注入。
1 |
|
@Scoped 注解
@Scoped注解用于自定义注解器(IoC容器)对注入对象的重用方式。JSR-330默认了如下几种默认行为:
- 如果未声明任何@Scope注解接口的实现,注入器应创建注入对象并且仅使用该对象一次。
- 如果声明了@Scoped注解接口,注入对象的声明周期由所声明的@Scoped注解实现决定。
- 如果注入对象在@Scoped实现中要由多个线程使用,则需保证注入对象的线程安全性。
- 如果某个类上声明了多个@Scoped注解,或声明了不受支持的@Scoped注解,IoC容器应该抛出异常。
公认的通用@Scoped实现只有@Singleton一个,JSR-330只确定了这么一个标准的生命周期注解。
@Singleton 注解
@Singleton注解接口在DI框架中应用广泛,需要注入一个不会改变的对象时,就要用@Singleton。大多数DI框架都将@Singleton作为注入对象的默认声明周期,无需显式发明。
1 |
|
接口Provider
当DI框架的标准注解不能满足你的需求,你想对DI框架注入代码中的对象拥有更多的控制权,可以要求DI框架将Provider
- 可以获取该对象的多个实例。
- 可以延迟加载对象。
- 可以打破循环依赖。
- 可以定义作用域,能在比整个被加载的应用小的作用域中查找对象。
该接口仅有一个T get()方法,这个方法会返回一个构造好的注入对象(T)。
1 |
|
四、DI参考实现:Guice3
Guice3是JSR-330规范的完整参考实现,可以配置、绑定、注入依赖项。
实现DI
创建绑定关系
先创建绑定关系AgentFinderModule,重写configure()声明绑定关系,当AgentFinderService要求@Inject一个AgentFinder时,就会绑定DevAgentFinder作为注入对象。
1 |
|
代码4-1
构建Guice对象关系图
1 |
|
Guice的各种绑定
Guice提供多种绑定方式:
- 链接绑定
- 绑定注解
- 实例绑定
- @Provides方法
- Provider绑定
- 无目标绑定
- 内置绑定
- 及时绑定
最常用的包括链接绑定、绑定注解、@Provides方法、Provider
链接绑定
代码4-1中AgentFinderModule即为链接绑定,是最简单的绑定方式,只是告诉注入器运行时应该注入实现类或扩展类(可以直接注入子类)。
绑定注解
将注入类的类型和额外的标识符组合起来,以标识恰当的注入对象。使用JSR-330标准注解@Named,注入特定名称的AgentFinder,在AgentFinderModule中配置@Named绑定:
1 |
|
@Provides和Provider:提供完全定制的对象
需要注入特别的AgentFinder,使用@Provides注解或在configure()方法中绑定,注入器会查看左右标记了@Provides注解方法的返回类型,决定 注入哪个对象。
1 |
|
@Provides方法会变得越来越大,为简化Module,需要把定制化代码拆分出去。使用toProvider方法绑定到Provider类:
1 |
|
参考
https://jinnianshilongnian.iteye.com/blog/1413846
《Java程序员修炼之道 Benjamin J.Evans》第3章