Spring揭秘-IOC基本概念

让别人为你服务

IoC是随着近年来轻量级容器(Lightweight Container)的兴起而逐渐被很多人提起的一个名词,它的全称为Inversion of Control,中文通常翻译为“控制反转”,它还有一个别名叫做依赖注入DI(Dependency Injection)。

好莱坞原则“Don’t call us, we will call you.”恰如其分地表达了“反转”的意味,是用来形容IoC最多的一句话。那么,为什么需要IoC?IoC的具体意义是什么?它到底有什么独到之处?

注:关于IoC控制反转和DI依赖注入,此处等同看待,也有不同观点,认为依赖注入只是控制反转的一种实现方式。

为了更好地阐述IoC模式的概念,我们引入以下简单场景。

在某项目中,经常需要近乎实时地为客户提供外汇新闻。通常情况下,都是先从不同的新闻社订阅新闻来源,然后通过批处理程序定时地到指定的新闻服务器抓取最新的外汇新闻,接着将这些新闻存入本地数据库,最后在FX系统的前台界面显示。

假设我们有一个FXNewsProvider类来做以上工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FXNewsProvider
{
private IFXNewsListener newsListener;
private IFXNewsPersister newPersistener;
public void getAndPersistNews()
{
String[] newsIds = newsListener.getAvailableNewsIds();
if(ArrayUtils.isEmpty(newsIds))
{
return;
}
for(String newsId : newsIds)
{
FXNewsBean newsBean = newsListener.getNewsByPK(newsId);
newPersistener.persistNews(newsBean);
newsListener.postProcessIfNecessary(newsId);
}
}
}
JAVA

其中,FXNewsProvider需要依赖IFXNewsListener来帮助抓取新闻内容,并依赖IFXNewsPersister存储抓取的新闻。

假设默认使用道琼斯(Dow Jones)新闻社的新闻,那么我们相应地提供了DowJonesNewsListener和DowJonesNewsPersister两个实现。通常情况下,需要在构造函数中构造IFXNewsProvider 依赖的这两个类(以下将这种被其他类依赖的类或对象,简称为“依赖类”、“依赖对象”):

1
2
3
4
public FXNewsProvider() {
newsListener = new DowJonesNewsListener();
newPersistener = new DowJonesNewsPersister();
}
JAVA

这就是我们通常的方式,如果我们依赖于某个类或服务,最简单而有效的方式就是直接在类的构造函数中new相应的依赖类。这就好比要装修新房,需要用家具,这个时候,根据通常解决对象依赖关系的做法,我们就会直接打造出需要的家具来。
不过,通常都是分工明确的,所以,大多数情况下,我们可以去家具广场将家具买回来,然后根据需要装修布置即可。

不管是直接打造家具(通过new构造对象),还是去家具广场买家具(或许是通过Service-Locator解决直接的依赖耦合),有一个共同点需要我们关注,那就是我们都是自己主动地去获取依赖的对象!

可是我们自己每次用到什么依赖对象都要主动地去获取,这是否真的必要?我们最终所要做的,其实就是直接调用依赖对象所提供的某项服务而已。只要用到这个依赖对象的时候,它能够准备就绪,我们完全可以不管这个对象是自己找来的还是别人送过来的。对于FXNewsProvider来说,那就是在getAndPersistNews()方法调用newsListener的相应方法时,newsListener能够准备就绪就可以了。

实际上,IoC就是为了帮助我们提供了更加轻松简洁的方式。它的反转,就反转在从原来的事必躬亲,转变为现在的享受服务,IoC的理念就是让别人为你服务!

依赖注入方式

IoC三种依赖注入的方式,即构造方法注入(constructor injection)、setter方法注入(setter injection)以及接口注入(interface injection)。

构造方法注入

顾名思义,构造方法注入,就是被注入对象可以通过在其构造方法中声明依赖对象的参数列表,让外部(通常是IoC容器)知道它需要哪些依赖对象。

1
2
3
4
5
public FXNewsProvider(IFXNewsListener newsListner,IFXNewsPersister newsPersister) 10
{
this.newsListener = newsListner;
this.newPersistener = newsPersister;
}
JAVA

IoC Service Provider会检查被注入对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。同一个对象是不可能被构造两次的,因此,被注入对象的构造乃至其整个生命周期,应该是由IoC Service Provider来管理的。构造方法注入方式比较直观,对象被构造完成后,即进入就绪状态,可以马上使用。

setter方式注入

对于JavaBean对象来说,通常会通过setXXX()和getXXX()方法来访问对应属性。 这些setXXX()方法统称为setter方法,getXXX()当然就称为getter方法。通过setter方法,可以更改相应的对象属性,通过getter方法,可以获得相应属性的状态。所以,当前对象只要为其依赖对象所对应的属性添加setter方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中。

外界就可以通过调用setNewsListener和setNewPersistener方法为FXNewsProvider对象注入所依赖的对象了。

setter方法注入虽不像构造方法注入那样,让对象构造完成后即可使用,但相对来说更宽松一些,可以在对象构造完成后再注入。

接口注入

相对于前两种注入方式来说,接口注入没有那么简单明了。被注入对象如果想要IoC ServiceProvider为其注入依赖对象,就必须实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。
IoC Service Provider最终通过这些接口来了解应该为被注入对象注入什么依赖对象。下图演示了如何使用接口注入为FXNewsProvider注入依赖对象。

FXNewsProvider为了让IoC Service Provider为其注入所依赖的IFXNewsListener,首先需要实现IFXNewsListenerCallable接口,这个接口会声明一个injectNewsListner方法(方法名随意),该方法的参数,就是所依赖对象的类型。这样, InjectionServiceContainer对象,即对应的IoC Service Provider就可以通过这个接口方法将依赖对象注入到被注入对象FXNewsProvider当中。

注:在这种情况下,实现的接口和接口中声明的方法名称都不重要。重要的是接口中声明方法的参数类型,必须是“被注入对象”所依赖对象的类型。

接口注入方式最早并且使用最多的是在一个叫做Avalon的项目中,相对于前两种依赖注入方式,接口注入比较死板和烦琐。如果需要注入依赖对象,被注入对象就必须声明和实现另外的接口。这就好像你同样在酒吧点啤酒,为了让服务生理解你的意思,你就必须戴上一顶啤酒杯式的帽子,看起来有点多此一举。

三种注入方式比较

  • 接口注入。从注入方式的使用上来说,接口注入是现在不甚提倡的一种方式,基本处于“退役状态”。因为它强制被注入对象实现不必要的接口,带有侵入性。而构造方法注入和setter方法注入则不需要如此。

  • 构造方法注入。 这种注入方式的优点就是,对象在构造完成之后,即已进入就绪状态,可以马上使用。缺点就是,当依赖对象比较多的时候,构造方法的参数列表会比较长。而通过反射构造对象的时候,对相同类型的参数的处理会比较困难,维护和使用上也比较麻烦。而且在Java中,构造方法无法被继承,无法设置默认值。对于非必须的依赖处理,可能需要引入多个构造方法,而参数数量的变动可能造成维护上的不便。

  • setter方法注入。因为方法可以命名,所以setter方法注入在描述性上要比构造方法注入好一些。另外, setter方法可以被继承,允许设置默认值,而且有良好的IDE支持。缺点当然就是对象无法在构造完成后马上进入就绪状态。

  • 综上所述,构造方法注入和setter方法注入因为其侵入性较弱,且易于理解和使用,所以是现在使用最多的注入方式;而接口注入因为侵入性较强,近年来已经不流行了。


Spring揭秘-IOC基本概念
https://leehoward.cn/2019/10/22/Spring揭秘-IOC基本概念/
作者
lihao
发布于
2019年10月22日
许可协议