浅析“依赖注入(DI)/控制反转(IOC)”的实现思路

时间:2022-05-21 03:31:17

开始学习Spring的时候,对依赖注入(DI)——也叫控制反转(IOC)—— 的理解不是很深刻。随着学习的深入,也逐渐有了自己的认识,在此记录,也希望能帮助其他入门同学更深入地理解Spring。本文不再介绍其背景与定义,比 如“究竟是什么控制被反转了?”、“注入了什么依赖?”等等问题,在网络上应该会搜出很多相关的内容。本文下面主要从入门者的角度来分析如下问题:“依赖 注入要解决什么问题?” 以及 “依赖注入可能是如何实现的?”

==============================

为了更好地解释这两个问题,本文列举了一个简单的代码小例子来配合说明,先将例子中设计的类列举如下:

比如我有苹果(Apple)、橘子(Orange)、香蕉(Banana)三种水果(Fruit),我想知道某种水果是什么颜色的。

对应水果类的实现代码非常简单:

Fruit是个接口:

package com.example.impl;

public interface Fruit {
//只有一个简单的方法:返回水果的颜色
public String getColor();
}

Apple、Banana、Orange三个类各自实现Fruit接口:

package com.example.action;
import com.example.impl.Fruit; public class Apple implements Fruit {
public String getColor() {
return "Red";
}
} --------------------------------------------
package com.example.action;
import com.example.impl.Fruit; public class Banana implements Fruit {
public String getColor() {
return "Yellow";
}
} --------------------------------------------
package com.example.action;
import com.example.impl.Fruit; public class Orange implements Fruit {
public String getColor() {
return "Orange";
}
}

==============================

OK,现在开始解释文章开头提出的两个问题:

(1)依赖注入要解决什么问题?

简单地说,依赖注入就是为了“对类之间的依赖进行解耦”。

比如上面的水果例子中,如果现在我想知道某种水果的颜色,我需要这样实现:

package com.example.test;
import com.example.impl.Fruit;
import com.example.action.*; public class Person {
public static void main(String[] args) {
//我想知道苹果的颜色.
Fruit fruit = new Apple();
System.out.println(fruit.getColor());
}
}

这里,我(Person)与水果类(这里具体为Apple)之间是有依赖的,即Person依赖Fruit,如果我现在想知道香蕉的颜色,那我只能修改Person类的代码如下:

package com.example.test;
import com.example.impl.Fruit;
import com.example.action.*; public class Person {
public static void main(String[] args) {
//我现在改成想知道香蕉的颜色了...
Fruit fruit = new Banana();
System.out.println(fruit.getColor());
}
}

这就是依赖产生的问题:我每次都必须修改Person的代码,才能实现“知道不同Fruit的颜色”的目的。

依赖注入要解决的就是上面这个问题,套在本例中就是:如果我想知道不同Fruit的颜色,我不需要修改Person的代码。也就是Person与Fruit解耦啦!!

那依赖注入具体是怎么实现的呢?且继续往下看。

(2)依赖注入可能是如何实现的?

之所以加“可能”二字,是因为我还没拜读过Spring的内部代码,所以暂时是靠自己的理解认为是这么实现的,但我估计应该八九不离十吧。

上面例子中说到,Person现在是对Fruit有依赖,Spring第一个想到的就是引入Java的反射机制,关于反射本文不再解释,直接拿过来用了,引入反射之后,Person的代码会变成这样:

package com.example.test;
import com.example.impl.Fruit;
import com.example.action.*; public class Person {
public static void main(String[] args) {
try {
//使用Java反射机制,使Person对Fruit具体类的依赖,转变为对类名的字符串的依赖...
Fruit fruit = (Fruit) Class.forName("com.example.action.Apple").newInstance(); //注意:forName()需要传入类的全路径名称
System.out.println(fruit.getColor());
}
catch (Exception e) {
System.out.println("class not found.");
}
}
}

代码看起来复杂了很多,但实际上只有标红的一行是最主要的变化,其余都是异常处理的代码。

可以看到,这个时候,Person对Fruit的依赖,已经转变成了对Fruit下面某个具体类的类名字符串的依赖了,即上面代码中,那段蓝色的字符串。如果此时我想知道橘子的颜色,我只需要把上面蓝色字符串的内容改成:com.example.action.Orange即可。

但是,现在Person还是没有完全摆脱对Fruit的依赖,只是依赖变成了一个字符串而已,那Spring第二个想到的就是通过文件读取这个类名字符串了,我们先在D盘建立一个test.txt文件,然后把“com.example.action.Apple”这段内容写到这个test.txt文件中,同时代码改为:

package com.example.test;
import com.example.impl.Fruit;
import com.example.action.*; public class Person {
public static void main(String[] args) {
try {
FileReader fr = new FileReader("D://test.txt");
BufferedReader br = new BufferedReader(fr);
String fruitName = br.readLine();
//使用Java反射机制,使Person对Fruit具体类的依赖,转变为对类名的字符串的依赖...
//加上读取文件的配置,类名字符串是从文件中读取的,不直接写在Person中
Fruit fruit = (Fruit) Class.forName(fruitName).newInstance(); //注意:forName()需要传入类的全路径名称
System.out.println(fruit.getColor()); br.close();
fr.close();
}
catch (Exception e) {
System.out.println("class not found.");
}
}
}

上面代码其实就是从D://test.txt中读取第一行内容,即类名字符串,然后利用java反射机制实例化这个类。

至此,可以看到Person类也不需要依赖Fruit的类名字符串了,转而变成了对D://test.txt这个文件的依赖,但是我们不需要修改这个文件的地址和文件名,只需要修改文件内容,就可以动态地知道不同水果的颜色了。比如我把test.txt的内容改成com.example.action.Banana,运行程序后,自然会打印出Yellow而不再是Red了。

而Spring实际上是把上文的txt文件替换成了xml文件,这样做的好处?xml文件中的标签更容易对类的各种属性进行表述,比如类名、类属性、类属性的值等等,不用像txt中那种没层次关系、需要自己瞎定义了。自然是更方便使用。

Spring实际使用时,大概就是把上面读取文件+反射的过程封装成ApplicationConext类,所以不需要在Person中写这么大段的代码了,而是直接用ApplicationContext和getBean()即可,或者直接用注解的方式更是方便,其实可以理解为Spring是把这些代码都封装到了框架里,不需要我们自己写了。

以上就是我在学习Spring依赖注入的过程中,一些自己的心得体会,希望大家多多交流、多提意见和建议!

相关文章