yuanye0 2020-05-14
Ioc不是一种实实在在的技术,只是一种设计思想。
面向对象编程中,对象之间不可避免且必要地存在着耦合,但过度耦合会导致代码难以维护。
一般来说,当一个对象需要获取另一个对象,他就需要在自身的代码中显式地实例化一个该对象,比如
Object object = new Object();
对象实例化的时机由调用者在其自身的代码中决定,调用者掌握着控制权。
Ioc就是把原本由调用者掌握的控制权交给Ioc容器,由Ioc容器根据配置文件(xml)来代为实例化对象,以达到松耦合的目的。
很通俗的例子,由自己(调用者)买食材做食物(亲自new对象)变成到食堂(Ioc容器)购买。
至于为什么能解耦,需要先了解Ioc容器。
Ioc容器:具有依赖注入功能的容器,IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。
应用程序无需直接在代码中new相关的对象,应用程序由IOC容器进行组装。
Spring IoC 容器的设计主要是基于以下两个接口:
- BeanFactory
- ApplicationContext
其中 ApplicationContext 是 BeanFactory 的子接口之一,换句话说:BeanFactory 是 Spring IoC 容器所定义的最底层接口,而 ApplicationContext 是其最高级接口之一,并对 BeanFactory 功能做了许多的扩展,所以在绝大部分的工作场景下,都会使用 ApplicationContext 作为 Spring IoC 容器。
延续上一个例子,定义一个Food接口和若干食物bean(Rice,Buger,Noodle...)
注:面向接口编程是实现Ioc的重要基础之一。
public interface Food { } public class Rice implements Food { String name; } ...
人每天都吃饭,抽象出一个Person类,包含food属性和eat方法
public class Person { Food food; public Person(){ //紧耦合 this.food=new Rice(); } public void eat(){ } }
此时,Person和Food之间的关系可以表示为
不难看出,此时Person和实现Food接口的Rice类是紧耦合的。
Person类应该描述一个能吃任何食物的人,但由于紧耦合,它只能描述一个只吃rice的人,想要这个人能吃buger和noodle,必须在其代码中做出修改,但仍然只能吃特定的食物。这显然有违我们创建这个类的初衷。
利用Ioc容器作为管理对象实例的工具,可以达到松耦合的目的,下图很直观地反映了这一点。
有了Ioc容器,不必再在Person类中显式地实例化一个具体的食物类
public class Person { Food food; public Person(Food food) { //松耦合 this.food = food; } public void eat() { } }
Person的构造方法仅传进来了一个Food类型的对象的引用,而Food只是一个接口,整段代码中完全没有出现任何一种具体实现了这个接口的类。
在这种情况下,Person怎么才能知道food的具体类型呢?
Spring IoC 的容器的初始化和依赖注入
虽然 Spring IoC 容器的生成十分的复杂,但是大体了解一下 Spring IoC 初始化的过程还是必要的。这对于理解 Spring 的一系列行为是很有帮助的。
注意:Bean 的定义和初始化在 Spring IoC 容器是两大步骤,它是先定义,然后初始化和依赖注入的。
- Bean 的定义分为 3 步:
1.Resource 定位
Spring IoC 容器先根据开发者的配置,进行资源的定位,在 Spring 的开发中,通过 XML 或者注解都是十分常见的方式,定位的内容是由开发者提供的。
2.BeanDefinition 的载入
这个时候只是将 Resource 定位到的信息,保存到 Bean 定义(BeanDefinition)中,此时并不会创建 Bean 的实例
3.BeanDefinition 的注册
这个过程就是将 BeanDefinition 的信息发布到 Spring IoC 容器中
注意:此时仍然没有对应的 Bean 的实例。
bean被定义了,但并没有被实例化。
对于单例模式的bean,真正的实例化将发生在Ioc容器被创建时,即
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Ioc容器被创建的同时,其中的单例对象将被实例化,分配资源。
对于多例模式的bean,其实例化则要等到使用Ioc容器获取bean时,即
Object object = (Object)context.getBean("<name>",<class>);
注:单例和多例模式由scope配置选项决定)
此外,还有一个配置选项lazy-init,即懒加载。其含义就是是否初始化 Spring Bean。在没有任何配置的情况下,它的默认值为 default,实际值为 false,也就是 Spring IoC 默认会自动初始化 Bean。如果将其设置为 true,那么只有当我们使用 Spring IoC 容器的 getBean 方法获取它时,它才会进行 Bean 的初始化,完成依赖注入。
依赖的形式有三种
1)构造方法注入
2)setter注入
3)接口注入(几乎不用)
回到之前的例子,Person类中没有实例化任何bean,而是等待Ioc容器为其注入。
首先为Rice类和Person类定义构造方法。
public interface Food { } public class Rice implements Food { String name; public Rice(String name) { this.name=name; } } ... public class Person { Food food; public Person(Food food){ //松耦合 this.food=food; } public void eat(){ } }
此时,需要为Rice注入name,为Person注入Rice。
在配置文件applicationContext.xml中
<!-- 定义rice --> <bean id="rice" class="com.spring.di.Rice"> <!-- 通过构造方法注入常量 --> <constructor-arg name="name" value="rice"/> </bean> <!-- 定义person --> <bean id="person" class="com.spring.di.Person"> <!-- 通过构造方法注入bean --> <constructor-arg name="food" ref="rice"/> </bean>
为两个类设置setter
public interface Food { } public class Rice implements Food { String name; public void setName(String name) { this.name = name; } } ... public class Person { Food food; public void setFood(Food food) { this.food = food; } }
xml
<!-- 定义rice --> <bean id="rice" class="com.spring.di.Rice"> <!-- 通过setter注入常量 --> <property name="name" value="rice"/> </bean> <!-- 定义person --> <bean id="person" class="com.spring.di.Person"> <!-- 通过setter注入bean --> 10 <property name="food" ref="rice"/> 11 </bean>
至此已经完成了bean的定义
使用Ioc容器
1 ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); 2 Person person = (Person)context.getBean("person",Person.class); 3 person.eat();
终于吃上饭了。
这时候如果想吃面,只需在xml配置文件中定义一个noodle的bean,并将其注入到Person类中。
这就实现了Person与Food之间的松耦合,从而不修改Java代码,Person就可以吃到任何种类的食物。
参考:《Spring实战 第4版》