Spring的IOC理解 #
什么是IOC #
在这里我们不谈Spring的基础知识,我们知道谈到Spring就会谈到IOC,这个IOC是什么呢,中文名叫控制反转,这个东西是伴随着一些编程思想出现,其实同Java的本身也有关
就好比我熟悉的Python就是一个鸭子语言,你可以随便把一个值丢掉函数里面去,只要他满足一些特性就能正常运行,但是Java是一种强类型语言,你函数给什么参数,必须传什么参数
这里就不讨论两张语言的设计优劣呢,Java这种特性也做了一些妥协,我们肯定得为语言的扩展性做点事,谁也不知道未来会发生什么,Java里面使用多态来实现这种扩展,只要他是函数参数的家族成员,他就能上去运行
这个多态是实现IOC的基础,但是造成他出现的原因是因为设计模式里面的单一职责原则,这个要求我们类功能要单一,我们这里给一个例子来说明这个问题
class Car {
void run() {
System.out.println("Car running...");
}
}
首先我们有一个Car
的类,一开始我们只让他有run
这个属性,很好,接下来我们想知道是谁驾驶这辆车,于是我们便给这个类加一个字段driver
public class Car {
String driver;
public Car(String driver) {
this.driver = driver;
}
void run() {
System.out.println("Driver :" + driver);
System.out.println("Car running...");
}
}
很好我们知道驾驶这辆车的人,接着我们又想知道这个驾驶人的驾龄,如果我们继续给Car
加入字段,这样我们就违背了单一职责原则
,Car
类不但承担了车的功能还承担了人的功能
于是我们就把驾驶人隔离出来
class Driver{
String name;
String age;
public Driver(String name, String age) {
this.name = name;
this.age = age;
}
}
class Car {
Driver driver;
public Car(Driver driver) {
this.driver = driver;
}
void run() {
System.out.println("Driver age:" + driver.age + "name: " + driver.name);
System.out.println("Car running...");
}
}
我们重新将类分成两个类来实现了这个问题,但是这个时候又来了一个问题,我们有一个飞行员的也想驾驶这辆车,但是这辆车只能司机来驾驶,但是飞行员和司机开车的动作步骤是一样的,为了复用run
这个函数,你开始揪起了你的头发.
你想呀想突然想到,Java的多态,假如我们声明一个IDriver
的接口,让飞行员和司机都继承这个类这样我们只要给车一个IDriver
对象就能复用run
函数
//IDriver.java
public interface IDriver{
String getName();
void setName(String name);
int getAge();
void setAge(int age);
}
// Driver.java
public class Driver implements IDriver{
String name;
int age;
@Override
public String getName() {
return this.name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public int getAge() {
return this.age;
}
@Override
public void setAge(int age) {
this.age = age;
}
}
// Aviator.java
public class Aviator implements IDriver{
String name;
int age;
@Override
public String getName() {
return null;
}
@Override
public void setName(String name) {
}
@Override
public int getAge() {
return 0;
}
@Override
public void setAge(int age) {
}
}
//Car.java
public class Car {
private IDriver driver;
public void setDriver(IDriver driver) {
this.driver = driver;
}
void run() {
System.out.println("Driver age: " + driver.getAge() + " name: " + driver.getName());
System.out.println("Car running...");
}
}
我们重构代码把Driver
抽象为接口,然后让司机和飞行员都继承它,这样不管我们再添加什么其他的人就能适配这辆车.这个就是依赖倒置(DI)的思想
网上大部分教程就停留到这里了,这里我们继续探索下去,看看Spring是如何让这个DI更加简单的
首先我们反思一下,我们使用接口参数让我们的代码符合了设计模式,但是也带来了一些繁琐,我们来用代码"开"这辆车
IDriver driver = new Driver();
driver.setName("allen");
driver.setAge(18);
Car car = new Car();
car.setDriver(driver);
car.run();
PS:当然可以把赋值放到构造器中减少代码,但是由于Bean
依赖方法接口来赋值,所以为了后面讲解Bean
这里就不采用构造器来减少代码
代码有2行变成了6行,而且我们发现这个代码现在带来两个问题:
- 每次运行都得创建一个实现
IDriver
的对象 - 每次我们想换人开车的时候都得修改源代码
而且这些工作都很繁琐,作为一个偷懒的程序员,我可不想给每个用户都重新写一套代码,我们的想法很简单,我们希望这个Car
能够开箱即用,其实前面我们已经实现了控制反转了,现在就是要解决控制反转带来的“负面影响”
而且我们发现了一个问题,假如我们把上面函数放到一个代码里面,每次我们“开车”都得创建一个司机,然而我们还是相信“老司机”的手艺,所以我们也希望是否能够”记住“司机,只让一个老司机开车
接下来就是隆重介绍Spring
的Bean
的用法了,前面我们知道我们需要某种机制来去除”IOC“的弊端,我们把每个Car
当做一个对象,其实我们需要一个配置文件来记录IDriver
这些依赖对象,对象的其实在Java里面表现就是一棵树,所以通俗来讲我们需一个”树结构“数据来存贮依赖关系
我们程序在运行的时候解析这个树结构,然后依次给对象注入你想给他实例话的对象(比如你把”IDriver“设置为飞行员),这样的话,我们把依赖关系成功放到了配置文件中
这样带来两个好处:
- 想给不同用户使用软件时候,源代码不需要改变,只要给他们不同的配置文件就行
- 我们可以保存依赖实现”老司机“的复用
所以现在我们理理思路,我们需要的有两个东西
- 配置文件
- 一个加载配置文件并保存依赖的对象
在Spring
的Bean
中这两个分别对应xml文件
和实现ResourceLoader
接口对象(有多种实现)
为了更好的理解Bean
,接下来我们就从代码出发来测试这个Bean
最简单的实现 #
首先我们新建一个Spring
项目,无论你是用IntelliJ
还是Eslipse
都没关系,只能你能引用到Spring
库就行,我们复用前面的代码,看看使用Spring
Bean
来如何解决掉IOC的”副作用“
我们把前面的类分别放到同一路径不同的文件夹中,接下来我们先创建一个xml
文件,什么名字不重要,我们这里先命名为driver.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="car" class="Car">
<property name="driver">
<bean class="Driver">
<property name="name" value="Allen"></property>
<property name="age" value="18"></property>
</bean>
</property>
</bean>
</beans>
写入这些东西,接下来我们看看是否能够通过这个xml
文件来直接得到一个配置好司机Allen
的车
随便新建一个类在上面的路径中,我们这里就新建一个Main
吧
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("driver.xml");
Car car = context.getBean("car", Car.class);
car.run();
}
}
输出为
Driver age:18 name: Allen
Car running...
我们成功通过一个xml
配置文件和一个ApplicationContext
对象实现了一个即开即走的车,而且假如我们想换个司机,我们可以修改配置文件把class
换成“飞行员”,而且我们可以发现我们得到的司机都是一样的,验证方法很简单,我就不写代码了,假如我们想换司机怎么办,简单在bean
里面加上scope=“prototype”
就行(默认值为singleton
)
接下来我们又有一个疑问,假如我们有一辆特别宝马车,我们希望只有某一种加上员能能开(假设只有飞行员),也就是是说,我们其实即不想放弃IOC,但是又不想将这个配置写到Bean
里面去,有办法能够解决吗?
当然有,Spring
2.5就支持注解来写Bean
配置,对于一些固定的类,我们可以把依赖关系用代码写到类中,这样一方面能够保证IOC
,一方面又能实现Bean xml
文件瘦身
由于Spring
默认不会去扫描注解,所以有三种方式,第一种是在xml
里面用加上一个
<context:component-scan base-package="...."></context:component-scan>
第二种是使用AnnotationConfigApplicationContext
来对象来进行扫描,第三种就是SpringApplication
来运行Spring
程序自动扫描
这三种方式假如你最后要做一个web
程序的话,第三种是非常方便的,这里我们就不谈怎么使用注解来代替xml
文件了,本质上是一样的,其实在我没有理解Bean
的强大之前,我比较推崇使用注解来写Bean
,但是随着对Bean
的探索,我发现xml
文件才是最佳选择,他将程序依赖与代码分离开来,假如我们还想用程序依赖写在代码里面,那就违背了Bean
的设计初衷
如果你想了解怎么使用注解可以阅读这篇博客
总结 #
至此,我们从问题的出现到问题的解决探索了IOC背后的故事,但是你可能会有一个疑问,为什么Spring
里面会有IOC
问题。
其实这个也跟Web
的发展有关,我们知道从Web的发展,一开始是没有前端的,只有后端,慢慢的后端分离出来前端,Web端页面也被分离出视图层和数据层,随着逐渐分离,也就出现我们前面举到的例子,类越来越多,比如视图层依赖数据层,数据层依赖控制层…..
这种层层依赖的问题延生出来的IOC的提出,也就慢慢的促进了Bean
这个库的开发,也正是因为Bean
我们才能享受静态强类型语言的低耦合的酸爽。