IoC(Inverse of Control 控制反转)是Spring容器的内核,AOP、声明事物等功能在此基础上开花结果。下面通过实例来理解什么是IoC。
一、什么是IoC
下面以电影《心花怒放》的拍摄举个栗子,涉及到的对象有:导演,主角耿浩(GengHao ),剧本(Xhnf),饰演主角耿浩的演员(黄渤)(HuangBo),电影里面有一幕就是耿浩对郝义说:“Cong gin ya hou,eou tong nei en duan ya gue。(从今以后,我同你恩断义绝!)”。经典!动作(SplitUp)。
a、代码 SI-1
public class Xhnf{
public void SplitUp(){
//演员直接侵入剧本,演员黄渤直接与剧本关联起来了,剧本与演员直接耦合了
HuangBo hb = new HuangBo();
hb.said("Cong gin ya hou,eou tong nei en duan ya gue");
}
}
这样设计代码肯定是不行的,不应该直接把耿浩要执行的动作(said)绑定到黄渤,剧本应该围绕角色(耿浩)来设计,我们应该为耿浩这个角色定义一个接口,具体由谁来饰演这个角色我们不知道,就不一定是黄渤了,谁饰演耿浩谁就来执行这个said方法。
b、代码 SI-2
public class Xhnf{
public void SplitUp(){
//引入耿浩角色接口
GengHao gh= new HuangBo();
//通过接口展开剧情
gh.said("Cong gin ya hou,eou tong nei en duan ya gue");
}
}
此时,剧本、耿浩、黄渤三者的类图关系如图所示:
可以看出,剧本此时同时依赖GengHao 接口和HuangBo类,并没有达到所期望的剧本仅依赖于角色的目的。
通过导演,可以解决这个耦合问题,导演将HuangBo安排在GengHao 的角色上,导演负责剧本、角色、饰演者三者的协调控制。如图所示:
通过引入导演,使得剧本和具体饰演者解耦,对应到软件中。导演就像一台装配器,安排演员表演具体的角色。
- IoC(Inverse of Control)的字面意思是指控制反转。对应上面的例子,”控制”是指选择GengHao角色扮演者的控制权;”反转”是指这种控制权从Xhnf剧本中移除,转交到导演手中。对于软件来说,即某一接口的具体实现类的选择控制权从调用类中移除,转交给第三方决定,即Spring容器借由Bean配置来进行控制。Martin Flowler提出了DI(Dependency Injection,依赖注入)的概念来解释IoC,即让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。
二、IoC的类型
IoC主要分为:构造函数注入、属性注入、接口注入。Spring支持构造函数注入和属性注入。
1、构造函数注入
在构造函数注入中,通过调用类的构造函数,将接口实现类通过构造函数变量传入。
a、代码SI-3
public class Xhnf{
private GengHao gh;
//①注入耿浩的具体饰演者
public Xhnf(GengHao gh){
this.gh = gh;
}
public void SplitUp(){
gh.said("Cong gin ya hou,eou tong nei en duan ya gue");
}
}
Xhnf的构造函数不关心具体由谁来饰演GengHao这个角色,只要在①处传入饰演者即可,角色的具体饰演者由导演来安排,如代码SI-4所示
b、代码SI-4
public class Director{
public void direct(){
//①指定角色的饰演者黄渤
GengHao gh = new HuangBo();
//②注入具体饰演者到剧本中
Xhnf xhnf = new Xhnf(gh );
//③调用方法执行说台词的操作
xhnf.SplitUp();
}
}
2、属性注入
会发现一个问题,那就是虽然GengHao是本片的主角,但是并非每个场景都需要GengHao出现,如果采用构造函数注入,难么只要剧本(Xhnf)一开始,就需要创建GengHao这个角色,在这种情况下通过构造函数注入并不妥当。属性注入可以有选择的通过Setter方法完成调用类所需要依赖的注入,更加灵活方便。
a、代码SI-5
public class Xhnf{
private GengHao gh;
//①属性注入耿浩的具体饰演者
public void setGengHao(GengHao gh){
this.gh = gh;
}
public void SplitUp(){
gh.said("Cong gin ya hou,eou tong nei en duan ya gue");
}
}
b、代码SI-6
public class Director{
public void direct(){
Xhnf xhnf = new Xhnf();
GengHao gh = new HuangBo();
//①调用属性Setter方法注入
xhnf.setGengHao(gh);
xhnf.SplitUp();
}
}
和通过构造函数注入GengHao饰演者不同,在实例化Xhnf剧本时,并未指定任何饰演者,而是在实例化Xhnf后,在需要GengHao出场时,才调用其setGengHao方法注入饰演者。
3、接口注入
将调用类所有依赖注入的方法抽取到一个接口中,调用类通过实现该接口提供相应的注入方法。.
a、代码SI-7
public interface ActorArrangable{
void injectGengHao(GengHao gh);
}
public class Xhnf implements ActorArrangable{
private GengHao gh;
//①实现接口方法
public void injectGengHao(GengHao gh){
this.gh = gh;
}
public void SplitUp(){
gh.said("Cong gin ya hou,eou tong nei en duan ya gue");
}
}
Director通过ActorArrangable的injectGengHao()方法完成饰演者的注入工作。
b、代码SI-8
public class Director{
public void direct(){
Xhnf xhnf = new Xhnf();
GengHao gh = new HuangBo();
xhnf.injectGengHao(gh);
xhnf.SplitUp();
}
}
接口注入还要额外声明一个接口,而且它的效果和属性注入并无区别,还是更倾向于使用属性注入的方式。
三、通过容器完成依赖关系的注入
虽然剧本(Xhnf )和演员(HuangBo)实现了解耦,Xhnf 无需关注角色实现类的实例化工作,但这些工作在代码中依然存在,只是转移到了Director类中而已。怎么实现剧本,导演,演员,角色,全部解耦呢?
需要一个第三方容器,它来完成类的初始化与装配工作,开发者就无需关注这些底层实现类的实例化、依赖关系装配等工作,专注于业务逻辑的开发。
Spring就是一个这样的容器,通过配置文件或注解描述类和类之间的依赖关系,自动完成类的初始化和依赖注入工作。
在Spring配置文件中,只需要简单的配置就可以完成依赖注入,下面截取一个片段:
<bean id="gengHao" class="HuangBo"/>
<bean id="Xhnf" class="com.cn.Xhnf" p:gengHao-ref="gengHao"/><!--通过gengHao-ref建立依赖关系-->
在容器启动时,Spring根据配置文件的描述信息,自动实例化Bean并完成依赖关系的装配,从容器中即可返回准备就绪的Bean实例,后续可直接使用。底层的实现原理要涉及到Java语言本身的类反射功能,下篇就介绍Java反射。