Spring的工作原理(二)IOC-DI

2023-11-17

 

目录

                   

Spring的工作原理(二)IOC-DI

 

一、什么是IOC,什么是DI?

1.1 IOC(Inversion of Control)-控制反转。

1.2 DI(Dependency Injection)-依赖注入。

二、IOC控制反转基础知识

2.1 IOC对bean的获取

2.2 IOC容器对Bean的管理


                 

                         Spring的工作原理(二)IOC-DI

由上一篇文章其实借着打印HelloWorld的这个例子已经简单理解了什么是控制反转。这里再一次详细讲解一下。

 

一、什么是IOC,什么是DI?

先把定义记住,后面才能有序的进行,否则就容易看不懂。

1.1 IOC(Inversion of Control)-控制反转。

控制反转与其定义为Spring中的一种技术,不如说是一种设计思想。在之前我们使用new的方式来进行创建对象,现在创建对象的权利交给了Spring来统一管理:即对对象的定义、作用域、生命周期、后置处理器、定义集成。

1.2 DI(Dependency Injection)-依赖注入。

        懂了控制反转之后,依赖注入就容易理解了,依赖注入的原理就是控制反转,因为管理权在Spring,如果对象与对象之间存在一定的联系,那么由Spring容器(或者叫IOC容器)动态的将某个依赖关系注入到组件当中,这个过程就是依赖注入。组件之间的依赖关系由容器运行期决定。

举个例子

在Spring框架下,当Bean实例 A运行过程中需要引用另外一个Bean实例B时,Spring框架会创建Bean的实例B,并将实例B通过实例A的构造函数、set方法、自动装配和注解方式注入到实例A,这种注入实例Bean到另外一个实例Bean的过程称为依赖注入。

二、IOC控制反转基础知识

        本节就是讲解IOC怎么进行控制反转的,并且通过哪一个类、或者哪一个接口实现的。它是怎么进行管理Bean对象的?管理了对象的哪一些地方?怎么进行创建?销毁时间是什么?下面就详细介绍一下:

2.1 IOC对bean的获取

首先,被Spring管理的类使用哪一个接口创建?那就需要了解一下Spring中的Spring BeanFactory容器和Sring ApplicationContext容器。

2.1.1 Spring BeanFactory容器

这个容器接口在org.springframework.beans.factory.BeanFactor中被定义。BeanFactory中有大量的接口被实现,最常用的就是XmlBeanFactory类。它可以从一个XML文件中读取元数据,获取Bean。

例如我们声明了一个Spring的xml配置文件为Beans.xml,实体类为HelloWorld.java

   <bean id="helloWorld" class="com.tutorialspoint.HelloWorld">
       <property name="message" value="Hello World!"/>
   </bean>

我们使用XmlBeanFactory进行读取Beans.xml,并创建HelloWorld的方式为:

XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("Beans.xml"));
HelloWorld obj = (HelloWorld) factory.getBean("helloWorld"); //HelloWorld被Bean设置的Id为"helloWorld"

分析一下:

第一步就是利用Spring框架提供的XmlBeanFactory()API生成工厂Bean并利用 ClassPathResource() API 去加载在路径 CLASSPATH 下可用的 bean 配置文件。在此时XmlBeanFactory()初始化所有的Bean对象。

第二步就是利用第一步生成Bean工厂对象,利用getBean()方法获取所需要的Bean。该方法是通过配置文件中的Bean Id来返回一个真正的对象。

2.1.2 Spring ApplicationContext 容器

说到ApplicationContext容器,其实它是BeanFactory 的子接口。称为Spring上下文。

接口:org.springframework.context.ApplicationContext interface中被定义。

最常被使用的 ApplicationContext 接口实现:

  • FileSystemXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你需要提供给构造器 XML 文件的完整路径。

  • ClassPathXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你不需要提供 XML 文件的完整路径,只需正确配置 CLASSPATH 环境变量即可,因为,容器会从 CLASSPATH 中搜索 bean 配置文件。

  • WebXmlApplicationContext:该容器会在一个 web 应用程序的范围内加载在 XML 文件中已被定义的 bean。

例如我们声明了一个Spring的xml配置文件为Beans.xml,实体类为HelloWorld.java

   <bean id="helloWorld" class="com.tutorialspoint.HelloWorld">
       <property name="message" value="Hello World!"/>
   </bean>

我们使用FileSystemXmlApplicationContext进行读取Beans.xml,并创建HelloWorld的方式为:

ApplicationContext context = new FileSystemXmlApplicationContext("C:/Users/ZARA/workspace/HelloSpring/src/Beans.xml");
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");

其他两个同理。不过是路径不同。

分析一下:

第一步生成工厂对象。加载完指定路径下 bean 配置文件后,利用框架提供的 FileSystemXmlApplicationContext API 去生成工厂 bean。FileSystemXmlApplicationContext 负责生成和初始化所有的对象,比如,所有在 XML bean 配置文件中的 bean。

第二步用第一步生成的上下文中的 getBean() 方法得到所需要的 bean。 这个方法通过配置文件中的 bean ID 来返回一个真正的对象。一旦得到这个对象,就可以利用这个对象来调用任何方法。

2.2 IOC容器对Bean的管理

由2.1 我们可以知道在Bean中定义了一个HelloWorld对象。就来详细研究一下,Bean的定义、作用域、生命周期。

2.2.1 SpringBean定义

属性 描述
class 这个属性是强制性的,并且指定用来创建 bean 的 bean 类。
name 这个属性指定唯一的 bean 标识符。在基于 XML 的配置元数据中,你可以使用 ID 和/或 name 属性来指定 bean 标识符。
scope 这个属性指定由特定的 bean 定义创建的对象的作用域。
constructor-arg 基于构造函数的依赖注入。
properties 基于设置函数的依赖注入。
autowiring mode 自动装配实现
lazy-initialization mode 延迟初始化的 bean 告诉 IoC 容器在它第一次被请求时,而不是在启动时去创建一个 bean 实例。
initialization 方法 在 bean 的所有必需的属性被容器设置之后,调用回调方法。
destruction 方法 当包含该 bean 的容器被销毁时,使用回调方法。

Spring和Bean之间的关系图:

2.2.2 配置数据元供给Spring

(1)基于 XML 的配置文件

(2)基于注解的配置

(3)基于 Java 的配置

让我们一一来看一下:

(1)基于 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-3.0.xsd">

   <!-- 基础的Bean对象加载方式 -->
   <bean id="..." class="...">
       <!-- collaborators and configuration for this bean go here -->
   </bean>

   <!-- Bean的懒加载方法 -->
   <bean id="..." class="..." lazy-init="true">
       <!-- collaborators and configuration for this bean go here -->
   </bean>

   <!-- Bean初始化方法 -->
   <bean id="..." class="..." init-method="...">
       <!-- collaborators and configuration for this bean go here -->
   </bean>
   <!-- Bean的销毁方法 -->
   <bean id="..." class="..." destroy-method="...">
       <!-- collaborators and configuration for this bean go here -->
   </bean>

   <!-- more bean definitions go here -->

</beans>

对于Spring Bean的定义,属于默认空间命名,没有空间名。

xmlns="http://www.springframework.org/schema/beans"

对于xsi来说,它是一个标准命名空间。这个命名空间为每个文档中指定相应的Schema文件,这是标准组织定义的标准命名空间。

 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

(2)基于注解的配置

自从Spring2.5之后,Spring就可以用配置注解的方式进行依赖注入。该种方式无需在xml中配置Bean引用。

注解配置默认在Spring中是关闭的,需要在配置文件中进行打开。(引入context空间)

<?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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:annotation-config/>

</beans>

下面就可以使用注解,解释几个常用的重要注解:

  • @Required注解应用于bean属性的setter方法,用来检查是否已经设置了所有必需的属性。
  • @Autowired它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。
  • @Qualifier,通过指定确切的将被引用的bean,@Autowired@Qualifier注解可以用来删除混乱
  • SR-250 Annotations,Spring支持JSR-250的基础的注解,其中包括了@Resource@PostContruct@PreDestory注解

简介项目:

pom.xml文件中引入了

spring-core spring-beans spring-context junit

package com.mcb.spring.vo;
public class HelloWorld {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void sayHello(){
        System.out.println(name+"说: Hello World");
    }
}
<?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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config />
    <bean id="hello" class="com.mcb.spring.vo.HelloWorld">
        <property name="name" value="Sum"/>
    </bean>
</beans>
import com.mcb.spring.vo.HelloWorld;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestSpring {
    @Test
    public void sayHello(){
        ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
        HelloWorld hello = (HelloWorld)context.getBean("hello");
        hello.sayHello();
    }
}

A. @Required

将该注解加到setter 方法中:

    @Required
    public void setName(String name) {
        this.name = name;
    }

此时如果我们将Beans.xml中name的<property>属性注释掉,我们会发现class报错。并提示"必须的参数缺失"。

如果我们将注解@Required去掉,并且同样注释参数,运行程序。运行成功,但是参数为属性name的值为 null。

B.@Autowired

1,setter方法中的@Autowired

我们稍微改造一下上面的工程,添加Person类

Person类:

package com.mcb.spring.vo;

public class Person {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

定义Beans.xml。将Person放在Spring进行管理。

    <context:annotation-config />
    <bean id="hello" class="com.mcb.spring.vo.HelloWorld">
        <!--<property name="name" value="Sum"/>-->
    </bean>

    <bean id="person" class="com.mcb.spring.vo.Person">
        <property name="name" value="Tom"/>
    </bean>

将Person对象通过setter方法进行注入。

package com.mcb.spring.vo;
import org.springframework.beans.factory.annotation.Autowired;
public class HelloWorld {
    private String name;
    public String getName() {
        return name;
    }
    @Autowired    // 将Person类注入进来
    public void setName(Person person) {
        this.name = person.getName();
    }
    public void sayHello(){
        System.out.println(name+"说: Hello World");
    }
}

运行程序,让我们看一下结果:

2,同理在属性中和构造方法中@Autowired  ,原理一致。都可以将Person对象注入到HelloWorld中,进行name输出。

    @Autowired    // 将Person类注入进来
    private Person person;
    @Autowired
    public HelloWorld(Person person) {
        this.name = person.getName();
    }

C.@Qualifier

结合使用@Qualifier@Autowired注解通过指定哪一个真正的bean将会被装配来消除混乱。

还是以上图工程为例:配置同一个Preson,两个不同对象Bean实例。

<?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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">
    <context:annotation-config />
    <bean id="hello" class="com.mcb.spring.vo.HelloWorld">
        <!--<property name="name" value="Sum"/>-->
    </bean>
    <bean id="person1" class="com.mcb.spring.vo.Person">
        <property name="name" value="Tom"/>
    </bean>
    <bean id="person2" class="com.mcb.spring.vo.Person">
        <property name="name" value="Jim"/>
    </bean>
</beans>

在HelloWorld中进行注入

package com.mcb.spring.vo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
public class HelloWorld {
    @Autowired
    @Qualifier("person2")
    private Person person;
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void sayHello(){
        System.out.println(person.getName()+"说: Hello World");
    }
}

运行测试类,进行打印:

D. SR-250 Annotations

在此我们了解一下一个注解@Resource。

上图使用了 @Autowired 和Qualifier来进行了注入。

@Autowired
@Qualifier("person2")
private Person person;

我们还可以使用@Resource进行注入。

@Resource(name = "person2")
private Person person;

可以达到同样的效果。

他们的区别如下:

  • @Autowired注解为Spring提供的注解,只按照byType方式注入,默认情况下,它要求依赖对象必须存在,如果允许为null,可以设置它的required属性为false,如果我们想按照byName方式来装配,可以结合@Qualifier注解一起使用;
  • @Resource为J2EE提供的注解,它有两个重要的属性:name和type。而默认情况下,@Resource注解按照byName的方式来装配。@Resource的装配顺序是这样的: 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。

(3)基于 Java 的配置

Spring中为了减少XML配置,可以声明一个配置类类对bean进行配置,主要用到两个注解@Configuration和@bean

第一步:在Spring中开启包扫描。

<beans>
     <context:component-scan base-package="com.mcb.spring.vo"/>
</beans>

第二步:建立SpringConfig类。

package com.mcb.spring.config;

import com.mcb.spring.vo.HelloWorld;
import com.mcb.spring.vo.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
    @Bean
    public HelloWorld helloWorld(Person person){
        HelloWorld helloWorld = new HelloWorld();
        helloWorld.setName(person.getName());
        return helloWorld;
    }
    @Bean
    public Person person() {
        Person person = new Person();
        person.setName("Jim");
        return person;
    }
}

第三步:测试类使用AnnotationConfigApplicationContext获取ApplicationContext类。

import com.mcb.spring.config.SpringConfig;
import com.mcb.spring.vo.HelloWorld;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class TestSpring {
    @Test
    public void sayHello(){
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        HelloWorld hello = (HelloWorld)context.getBean(HelloWorld.class);
        hello.sayHello();
    }
}

运行正常。

在第一步中,也可以用@ComponentScan注解代替xml的方式进行。

@Configuration
@ComponentScan(basePackages = "com.mcb.spring.config")
public class SpringConfig {
    ......
}

温馨提示:什么时候下使用什么样的注解呢?

2.2.3 Spring Bean的作用域
当Spring定义一个Bean时,必须声明该作用域的选项。目前支持五个作用域,分别是:singleton、prototype、request、session、global session。

用例: bean属性:scope

<bean id="..." class="..." scope="singleton">
    <!-- collaborators and configuration for this bean go here -->
</bean>

2.2.4 Spring Bean 生命周期

Bean的生命周期可以表达为: Bean的定义--> Bean的初始化--> Bean的使用 --> Bean的销毁

A。Bean的初始化回调函数。(实现InitializingBean)

import org.springframework.beans.factory.InitializingBean;
    public void afterPropertiesSet() throws Exception {
        System.out.println("我是Init回调函数");
    }

B。Bean的销毁回调函数。(DisposableBean)

import org.springframework.beans.factory.DisposableBean;
public void destroy() throws Exception {
        System.out.println("我是destroy回调函数");
}

C。Bean的初始化方法、销毁方法自定义。

public void destoryMy() {
    System.out.println("我是销毁方法");
}
public void init() {
        System.out.println("我是初始化方法");
}
<bean id="helloWorld" class="com.mcb.spring.vo.HelloWorld" init-method="init" destroy-method="destoryMy">

</bean>

将A、B、C同时配置后,结果如下:

我是Init回调函数
我是初始化方法
Jim说: Hello World
我是destroy回调函数
我是销毁方法

温馨提示:InitializingBean和DisposableBean的回调方法尽量不要使用,因为XML配置在命名方法上提供了极大的灵活性。

调用destory的时候,需要关闭hook的registerShutdownHook();

ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
HelloWorld hello = (HelloWorld)context.getBean("helloWorld");
hello.sayHello();
((ClassPathXmlApplicationContext) context).registerShutdownHook();

默认初始化和销毁方法(Beans 标签下)

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring的工作原理(二)IOC-DI 的相关文章

随机推荐

  • 关于Python的定义

    Python是一种高级编程语言 它被广泛应用于人工智能 大数据分析 网络编程 游戏开发等领域 Python的语法简单易学 代码可读性较高 使用简便 成为初学者入门的优秀选择 Python具有丰富的第三方库 可以轻松地实现各种功能 其中最为出
  • 最新让机器“看见”—计算机视觉原理及实战-从OpenCV基础到深度学习实战

    课程目标让机器 看见 计算机视觉原理及实战 从OpenCV基础到深度学习实战课程简介课程由浅入深 图文并茂 在讲述概念的同时注重和实际系统结合 为快速上手并深入研究无人驾驶 智能机器人 人机交互 医疗等行业应用奠定坚实基础 下载地址 百度网
  • 可见光与红外双模态图像融合行人检测

    摘要 由于传统融合检测方法未能较好地解决双模态融合中冗余信息带来的误检 漏检问题 为了更有效地利用双模态信息 提出一种光照感知和卷积块注意模块相结合的双模态特征融合行人检测网络 IWFC Net 首先根据可见光图像提取光照感知值 将其作为融
  • STM32------ADC基本原理

    目录 一 ADC 1 ADC简介 2 stm32f10x ADC特点 3 stm32f10x 大容量芯片带3个ADC控制器 4 ADC通道和引脚对应关系 5 ADC引脚 6 ADC框图 7 STM32F1的ADC的各个通道可以单次 连续 扫
  • Stable Diffusion Prompt用法

    Stable Diffusion可以根据你输入的提示词 prompt 来绘制出想象中的画面 1 正向提示词 Prompt 提高图像质量的prompt prompt 用途 HDR UHD 64K HDR UHD 4K 8K和64K 这样的质量
  • TinyMCE的上传文件的功能

    记录一下TinyMCE的上传文件的功能 用Base64上传图片 if meta filetype image var input document createElement input input setAttribute type fi
  • windows下安装使用git-lfs克隆大文件

    下载安装git lfs工具 首先去git lfs这里 下载相应平台的工具 我下载的windows版本 非安装版本 直接配置到系统环境变量里 执行以下命令验证是否成功 git lfs install 克隆数据集 这样自动会下载里边的大文件 g
  • 在vivado中使用tcl脚本(UG894)

    本文源自UG894 主要介绍如何在vivado中使用tcl脚本 1 vivado中如何获取tcl help vivado中任何自带的命令都可以通过 help 获取帮助信息 也可以直接输入 help 取得vivado命令合集 并通过 help
  • News Distribution(Codeforces 1167C) (并查集简单应用)

    并查集查询时间复杂度是O 1 合并时间复杂度才是O n 题意 n 人数 m 组数 m行 先输入k 表示这组有k个人 下面是k个人的编号 同组可以传递信息 问当第i个人是信息源时 有几个人知道信息 AC代码 include
  • 排序算法6-归并排序

    1 什么是归并排序 归并排序是建立在归并操作上的一种有效的排序算法 该算法是采用分治法 Divide and Conquer 的一个非常典型的应用 将已有序的子 序列合并 得到完全有序的序列 即先使每个子序列有序 再使子序列段间有序 若将两
  • 电路端接电阻与信号完整性

    信号沿着传输线传播时 每时每刻阻抗都可能发生变化 例如 PCB走线的宽度或者厚度发生变化 PCB过孔 PCB转角 PCB上的电阻 电容 电感 接插件和器件引脚都会产生阻抗变化 若走线的瞬时阻抗 只和传输线的横截面积和材质特性有关 发生变化
  • keil使用arm_math文件报错

    引入该文件后编译报错 application arm math h 306 error 35 error directive Define according the used Cortex cor 添加如下全局宏定义 USE HAL DR
  • 如何将任意数据保存到以太坊区块链?

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 虽然有北大博士讲 95 的区块链项目都没有前途 但我们知道区块链还是有它的优势 比如数据的不可篡改性对于版权保护有相当大的意义 而地址的匿名性则有其他潜在的用途 那么 如何
  • TensorFlow、PyTorch 和 Keras

    1 可用的 RNN 类型 在寻找 NLP 问题的深度学习解决方案时 循环神经网络 RNN 是开发人员最流行的首选架构 因此 从这个角度来比较框架是有意义的 所有正在考虑的框架都具有允许我们创建简单 RNN及其更进化的变体的模块 门控循环单元
  • V4l2框架基础知识(二)

    V4L2对uvc USB video class 免驱usb设备的编程框架 主要用于采集USB摄像头 采集方式 打开视频设备 设置视频设备的属性 缩放 裁剪等 在linux编程中直接使用ioctl 函数对设备I O通道进行管理 V4L2操作
  • 一文看懂PPP协议——PPP协议简介

    今天给大家介绍一下PPP协议的相关内容 文章浅显易懂 特别适合0基础同学 本文主将主要从PPP概述 PPP链路建立过程和PPP认证三个角度对PPP协议进行讲解 一 PPP概述 PPP point to point协议 即点对点协议 是数据链
  • unity获取用户鼠标与键盘操作

    Unity鼠标键盘输入 代码写在Update 函数中 屏幕坐标的原点在屏幕的左下角 1 通过Input API 获得鼠标的操作信息 1 获得鼠标的坐标位置 Input mousePosition 2 检测鼠标按下 Input GetMous
  • oracle如何去列的重复,oracle重复列只显示一次的实现

    CREATE TABLE test ob id VARCHAR 32 ob name VARCHAR 32 INSERT INTO test VALUES A001 A001 a INSERT INTO test VALUES A001 A
  • YOLOE,2022年新版YOLO解读

    前言 这是2022年第一个关于YOLO的改版 该版本由百度提出 称之为YOLOE 是目前各项指标sota的工业目检测器 性能sota且部署相对友好 该检测器的设计机制包括 Anchor free无锚盒机制 可扩展的backbone和neck
  • Spring的工作原理(二)IOC-DI

    目录 Spring的工作原理 二 IOC DI 一 什么是IOC 什么是DI 1 1 IOC Inversion of Control 控制反转 1 2 DI Dependency Injection 依赖注入 二 IOC控制反转基础知识