依赖注入

讲真的,今年就业形势相当不好,对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接口,及其两个实现类:

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开发人员友好的经纪人,

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的一种。

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

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

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方法中:

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。

public class AgentFinderService {
	private final AgentFinder finder;
	@Inject public AgentFinderService(AgentFinder finder) {
		this.finder = finder;
	}
}

注意

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

方法上使用@Inject

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

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

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

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

属性上使用@Inject

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

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

@Qualifier 注解

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

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

示例如下:

@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一起使用,符合指定名称并且类型正确的对象会被注入。

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作为注入对象的默认声明周期,无需显式发明。

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

接口Provider

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

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

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

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作为注入对象。

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对象关系图
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绑定:

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注解方法的返回类型,决定 注入哪个对象。

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类:

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章

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值