控制反转(Ioc)

yuanye0 2020-05-14

控制反转(Ioc)

Ioc不是一种实实在在的技术,只是一种设计思想。

面向对象编程中,对象之间不可避免且必要地存在着耦合,但过度耦合会导致代码难以维护。

一般来说,当一个对象需要获取另一个对象,他就需要在自身的代码中显式地实例化一个该对象,比如

 Object object = new Object();  

对象实例化的时机由调用者在其自身的代码中决定,调用者掌握着控制权。

Ioc就是把原本由调用者掌握的控制权交给Ioc容器,由Ioc容器根据配置文件(xml)来代为实例化对象,以达到松耦合的目的。

很通俗的例子,由自己调用者买食材做食物亲自new对象)变成到食堂Ioc容器)购买。

至于为什么能解耦,需要先了解Ioc容器。

Ioc容器

Ioc容器:具有依赖注入功能的容器,IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖

应用程序无需直接在代码中new相关的对象,应用程序由IOC容器进行组装

Spring IoC 容器的设计主要是基于以下两个接口:

  • BeanFactory
  • ApplicationContext

其中 ApplicationContext 是 BeanFactory 的子接口之一,换句话说:BeanFactory 是 Spring IoC 容器所定义的最底层接口,而 ApplicationContext 是其最高级接口之一,并对 BeanFactory 功能做了许多的扩展,所以在绝大部分的工作场景下,都会使用 ApplicationContext 作为 Spring IoC 容器。

控制反转(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之间的关系可以表示为

控制反转(Ioc)

不难看出,此时Person和实现Food接口的Rice类是紧耦合的。

Person类应该描述一个能吃任何食物的人,但由于紧耦合,它只能描述一个只吃rice的人,想要这个人能吃buger和noodle,必须在其代码中做出修改,但仍然只能吃特定的食物。这显然有违我们创建这个类的初衷。

利用Ioc容器作为管理对象实例的工具,可以达到松耦合的目的,下图很直观地反映了这一点。

控制反转(Ioc)

有了Ioc容器,不必再在Person类中显式地实例化一个具体的食物类

public class Person {
    Food food;

    public Person(Food food) {
        //松耦合
        this.food = food;
    }

    public void eat() {

    }
}

Person的构造方法仅传进来了一个Food类型的对象的引用,而Food只是一个接口,整段代码中完全没有出现任何一种具体实现了这个接口的类。

在这种情况下,Person怎么才能知道food的具体类型呢?

依赖注入(DI)

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注入

为两个类设置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版》

       Spring AOP 简介

相关推荐