IoC (Inverse of Control,控制反转)是 Spring 容器的底层核心功能。
1 概念
在电影《陆犯焉识》中,有这样一个场景:获得自由回归社会后的陆焉识,常常想起劳改时候的生活,对现在的社会许多现象感到愤怒与不解,小女儿于是嘲讽他 “ 那你为什么要回来? ” 陆焉识(扮演者是陈道明)却毫不在意地回答: “ 我是为了你妈妈! ” 这一句自信的回答,让小女儿羞愧地无地自容又羡慕地无以复加,她小半生业绩卓著,却从未遇到一个男人,这样坚定地为她!
这里我们通过 Java 语言来编写剧本,借此来理解 IoC 的概念。
public class Script {
/**
* 回答
*/
public void dialog() {
ChenDaoMing cdm = new ChenDaoMing();//扮演者侵入剧本
cdm.answer(" 我是为了你妈妈!");
}
}
我们会发现以上剧本,把作为具体饰演者的陈道明直接侵入到剧本中,使剧本和演员直接耦合在了一起:
一个明智的编剧在剧情创作时应该围绕故事的角色进行,而不应考虑角色的具体饰演者,这样才可能在剧本投拍时自由地选择适合的演员,而非绑定在某一个人身上。所以,我们要为主人公陆焉识定义一个接口:
//引入 ”陆焉识“ 接口
LuYanShi luYanShi=new ChenDaoMing();
luYanShi.answer(" 我是为了你妈妈!");
剧本的情节通过角色展开,在拍摄时由演员饰演:
我们看到 Script 同时依赖于 LuYanShi 接口和 ChenDaoMing 类,并没有达到我们所期望的剧本仅依赖于角色的目的 。
可以在影片投拍时,由导演将 ChenDaoMing 类安排在 LuYanShi 的角色上,通过导演之手将剧本、角色、饰演者组合起来。
导演就像是一台装配器,将具体角色的演员赋给了剧本中的某个角色。
现在我们可以讲讲 IoC 咯,IoC(Inverse of Control)的字面意思是控制反转,它包含:其一是“控制”,其二是“反转”。
对应到前面的例子,“控制”是指剧本角色的选择权,“反转”是指这种选择权从《陆犯焉识》的剧本中移除,转移到导演手中。对于软件而言,即是某一接口具体实现类的选择控制权从调用类中移除,转交给第三方来决定,即由 Spring 容器的 Bean 配置来决定。
因为 IoC 表达的不够直接,因此业界曾进行过广泛的讨论,最终由软件界的泰斗级人物 Martin Fowler 提出了 DI (依赖注入: Dependency Injection )的概念,即让调用类对某一接口的实现类的依赖关系由第三方(容器或协作类)注入,从而移除了类对某一接口实现类的依赖 。“ 依赖注入 ” 的概念显然比 “ 控制反转 ” 易于理解 。
2 类型
IoC I划分为三种注入类型,分别是构造函数注入、属性注入和接口注入。
2.1 构造函数注入
通过调用类的构造函数,将接口实现类通过构造函数的参数传入:
private LuYanShi luYanShi;
/**
* 注入陆焉识的演员
* @param luYanShi
*/
public Script(LuYanShi luYanShi) {
this.luYanShi = luYanShi;
}
/**
* 对话
*/
public void dialog3() {
luYanShi.answer(" 我是为了你妈妈!");
}
在导演类中,通过构造函数注入扮演陆焉识的演员:
public class Director {
public void direct(){
LuYanShi luYanShi=new ChenDaoMing();//指定演员
Script script=new Script(luYanShi);//注入
}
}
2.2 属性注入
有时,导演会发现,虽然陆焉识是影片的男主角,但并非每场戏都需要他的出场,所以通过构造函数方式注入并不恰当,这种情况下,可以使用属性注入方式 。 属性注入通过 setter 方法完成调用类所需依赖的注入,更加灵活方便:
private LuYanShi luYanShi;
/**
* 属性注入
* @param luYanShi
*/
public void setLuYanShi(LuYanShi luYanShi) {
this.luYanShi = luYanShi;
}
/**
* 对话
*/
public void dialog4() {
luYanShi.answer(" 我是为了你妈妈!");
}
导演类通过设置属性来注入具体的演员:
public void direct(){
LuYanShi luYanShi=new ChenDaoMing();//指定演员
Script script=new Script();
script.setLuYanShi(luYanShi);//属性注入
script.dialog4();
}
2.3 接口注入
将调用类中所有的注入方法抽象到一个接口中,调用类通过实现这一接口提供相应的注入方法。为了采取接口注入的方式,需要声明一个接口:
public interface ActorArrangable {
/**
* 接口注入
* @param luYanShi
*/
void setLuYanShi(LuYanShi luYanShi);
}
在剧本类中通过接口方法注入陆焉识的扮演者:
public class Script implements ActorArrangable{
private LuYanShi luYanShi;
/**
* 接口注入
* @param luYanShi
*/
public void setLuYanShi(LuYanShi luYanShi) {
this.luYanShi = luYanShi;
}
}
由于通过接口注入需要额外声明一个接口,增加了类的数目,而且它的效果和属性注入并无本质区别,所以不建议使用这种方式。
3 使用容器注入依赖关系
虽然剧本和陈道明实现了解耦,但这些工作在代码中依然存在,只是转移到导演手中而已,导致导演的权力非常大,不断出现潜规则。假设某一制片人想改变这一局面,在选好某一个剧本后,通过“海选”或者第三方机构来选择导演、演员,让他们各司其职,那么剧本、导演与演员就都实现了解耦咯。
所谓媒体“海选”和第三方机构在程序领域中,就是一个第三方容器,它帮助我们完成类的初始化和装配工作。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 实例化类-->
<bean id="luYanShi" class="net.deniro.springBoot.spring4.IoC.ChenDaoMing"/>
<!-- 建立依赖关系-->
<bean id="script" class="net.deniro.springBoot.spring4.IoC.Script"
p:luYanShi-ref="luYanShi"/>
</beans>