Spring升级案例之IOC介绍和依赖注入

一、IOC的概念和作用
1.什么是IOC

控制反转(Inversion of Control, IoC)是一种设计思想,在Java中就是将设计好的对象交给容器控制,而不是传统的在对象内部直接控制。传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;可以理解为IoC 容器控制了对象和外部资源获取(不只是对象包括比如文件等)。

2.反转和正转

有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

3.IoC的作用

IoC 不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

此外,IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。

二、基于XML的IOC
1.创建工程

本项目建立在入门案例中传统三层架构的基础上,项目结构如下:

首先在pom.xml文件中添加如下内容:

<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
</dependencies>
2.创建xml文件

在resource目录下新建beans.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>

这里有一个小细节,在创建xml文件的时候,选择new->XML Configuration File->Spring Config,就会自动创建带有约束的Spring的xml配置文件。如下图:

3.使用Spring来创建bean对象

在bean标签内部添加如下内容:IOC容器本质上是一个map,id就是key,class对应的就是bean对象的全限定类名,Spring可以依据全限定类名来创建bean对象来作为map的value属性。

<!-- 把对象的创建交给Spring来管理 -->
<bean id="accountService" class="service.impl.AccountServiceImpl"></bean>
<bean id="accountDao" class="dao.impl.AccountDaoImpl"></bean>
4.使用IOC容器创建的bean对象

在src/main/java目录下创建ui.Client类:

public class Client {
/**
* 获取Spring的IoC核心容器,并根据id获取对象
* @param args
*/
public static void main(String[] args) {
//1.获取IoC核心容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
//2.根据id获取bean对象
//第一种方法:只传入id获取到对象之后强转为需要的类型
IAccountService accountService = (IAccountService) applicationContext.getBean("accountService");
System.out.println(accountService);
//第二种方法:传入id和所需要类型的字节码,这样getBean返回的对象就已经是所需要的对象
IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);
System.out.println(accountDao);
}
}

关于ApplicationContext,这里需要说明一下,首先通过选中这个接口然后右键Diagrams->Show Diagrams,可以看到接口的继承关系:其中BeanFactory接口就是IoC容器的底层接口。

在diagram中选中ApplicationContext接口,然后右键Show Implementations,可以看到该接口的实现类:

关于这些实现类需要说明如下几点:

ApplicationContext的实现类:
1.ClassPathXmlApplicationContext:加载类路径下的配置文件,要求配置文件必须在类路径下
2.FileSystemApplicationContext:加载磁盘任意路径下的配置文件,要求配置文件必须有访问权限,这种方法不常用
3.AnnotationApplicationContext:用于读取注解创建容器
5.IoC核心容器的两个接口:ApplicationContext和BeanFactory
  • ApplicationContext:创建核心容器时采用立即加载的方式创建对象,读取配置文件之后,立刻创建Bean对象(单例模式)。
  • BeanFactory:创建核心容器时采用延迟加载的方式创建对象,当根据id获取对象时,才会创建Bean对象(多例模式)

为了更加清楚地看到这两个接口之间的区别,我们在AccountDaoImpl和AccountServiceImpl类的无参构造方法中添加如下内容:

//AccountDaoImpl
public AccountDaoImpl() { System.out.println("dao创建了"); }
//AccountServiceImpl
public AccountServiceImpl() { System.out.println("service创建了"); }

对ui.Client类中的main方法添加如下代码:

Resource resource = new ClassPathResource("beans.xml");
BeanFactory factory = new DefaultListableBeanFactory();
BeanDefinitionReader bdr = new XmlBeanDefinitionReader((BeanDefinitionRegistry) factory);
bdr.loadBeanDefinitions(resource);
System.out.println(factory.getBean("accountDao"));

采用断点调试,我们可以发现:

  1. 对于ApplicationContext来说,执行ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");之后,立刻就会输出“service创建了”和“dao创建了”。
  2. 而对于BeanFactory来说,只有当执行到System.out.println(factory.getBean("accountDao"));之后,才会输出“dao创建了”。
  3. 这也就说明ApplicationContext是立即加载,BeanFactory是延迟加载。通常而言,ApplicationContext接口更加常用。此外,我们也可以自己指定单例模式还是多例模式。
三、Bean对象的管理细节
1.三种创建bean对象的方式
  • 第一种方式:使用默认构造方法创建

    在Spring配置文件中使用bean标签,如果只有id和class属性,就会使用默认构造方法(无参构造方法)创建对象。如果没有默认构造方法,则对象无法创建。例如,之前我们所使用的便是这第一种方式。

    <bean id="accountService" class="service.impl.AccountServiceImpl"></bean>
  • 第二种方式:使用其他类(比如工厂类)中的方法创建对象,并存入Spring容器,该类可能是jar包中的类,无法通过修改源码来提供默认构造方法。

    为了演示,我们在src/main/java目录新建factory包,在factory包下新建类InstanceFactory:

    public class InstanceFactory {
    //非静态方法
    public IAccountService getAccountService() {
    return new AccountServiceImpl();
    }
    }

    instanceFactory对应的就是factory包下的InstanceFactory类的对象,accountService对应的是InstanceFactory类下的getAccountService方法返回的对象。factory-bean属性用于指定创建本次对象的factory,factory-method属性用于指定创建本次对象的factory中的方法。

    <bean id="instanceFactory" class="factory.InstanceFactory"></bean>
    <bean id="accountService" factory-bean="instanceFactory" factory-method= "getAccountService"></bean>
  • 第三种方式:使用其他类(比如工厂类)中的静态方法创建对象,并存入Spring容器,该类可能是jar包中的类,无法通过修改源码来提供默认构造方法。

    为了演示,我们在src/main/java目录新建factory包,在factory包下新建类StaticFactory:

    public class StaticFactory {
    //静态方法
    public static IAccountService getAccountService() {
    return new AccountServiceImpl();
    }
    }

    由于是静态方法,所以无需指定factory-bean属性。class属性指定创建bean对象的工厂类,factory-method方法指定创建bean对象的工厂类中的静态方法。

    <bean id="accountService" class="factory.StaticFactory" factory-method="getAccountService"></bean>
2.bean对象的作用范围

bean标签的scope属性(用于指定bean对象的作用范围),有如下取值:常用的就是单例和多例

  • singleton:单例(默认值)
  • prototype:多例
  • request:作用域Web的请求范围
  • session:作用于Web的会话范围
  • global-session:作用于集群的会话范围(全局会话范围),当不是集群环境时,它就是session

这里我们演示单例和多例:

<bean id="accountService" class="service.impl.AccountServiceImpl" scope="singleton"></bean>
<bean id="accountDao" class="dao.impl.AccountDaoImpl" scope="prototype"></bean>

此时即便Client类中的main方法使用ApplicationContext接口:

public static void main(String[] args) {
//1.获取IoC核心容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
//2.根据id获取bean对象
//第一种方法:只传入id获取到对象之后强转为需要的类型
IAccountService accountService = (IAccountService) applicationContext.getBean("accountService");
System.out.println(accountService);
//第二种方法:传入id和所需要类型的字节码,这样getBean返回的对象就已经是所需要的对象
IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);
IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao");
System.out.println(accountDao == accountDao1);
}

使用断点调试,我们可以发现:

  1. 在执行到ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");时,就会输出“service创建了”,不会输出“dao创建了”。

  2. 只有当执行到IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);和IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao");时,才会输出“dao创建了”。

  3. 并且accountDao == accountDao1的结果是false。

3.bean对象的生命周期
  • 单例对象:生命周期和容器相同,容器创建对象就创建,容器销毁对象就销毁
  • 多例对象:当需要使用对象时(根据id获取对象时),对象被创建;当没有引用指向对象且对象长时间不用时,由Java的垃圾回收机制回收

为了演示,这里需要介绍bean标签的两个属性:init-method属性指定初始化方法,destroy-method属性指定销毁方法

<bean id="accountService" class="service.impl.AccountServiceImpl" scope="singleton"
init-method="init" destroy-method="destroy"></bean>
<bean id="accountDao" class="dao.impl.AccountDaoImpl" scope="prototype" init-method="init"
destroy-method="destroy"></bean>

同时,还有在AccountDaoImpl类和AccountService类中添加如下代码:

//AccountDaoImpl:
public void init() { System.out.println("dao初始化了"); }
public void destroy() { System.out.println("dao销毁了"); } //AccountServiceImpl:
public void init() { System.out.println("service初始化了"); }
public void destroy() { System.out.println("service销毁了"); }

为了手动关闭容器需要在Client类中的main方法中最后加入:

//容器需要手动关闭,因为applicationContext是接口类型,所以没有close方法,需要强制转换为实现类对象
((ClassPathXmlApplicationContext) applicationContext).close();

这个时候,我们再去使用断点调试,可以发现:

  1. 当执行到ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");时,就会输出“service创建了”和“service初始化了”。
  2. 只有当执行到IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);和IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao");时,才会输出“dao创建了”和“dao初始化了”。
  3. 执行到((ClassPathXmlApplicationContext) applicationContext).close();时,会输出“service销毁了”,不会输出“dao销毁了”。这是因为创建AccountDaoImpl类的对象时,使用的是多例模式。多例模式下的对象回收由JVM决定,关闭Ioc容器并不能使得JVM回收对象。
四、IOC的依赖注入
1.之前代码中的问题

在之前的代码中,我们一直没有使用AccountServiceImpl对象中的saveAccount方法,这是因为我们还没有实例化该类中的accountDao对象。我们先看看AccountServiceImpl的源代码:

public class AccountServiceImpl implements IAccountService {
//持久层接口对象的引用,为了降低耦合,这里不应该是new AccountDaoImpl
private IAccountDao accountDao; public AccountServiceImpl() { System.out.println("service创建了"); }
/** 模拟保存账户操作 */
public void saveAccounts() {
System.out.println("执行保存账户操作");
//调用持久层接口函数
accountDao.saveAccounts();
}
}

在之前的三层架构中,对于accoutDao对象,我们是private IAccountDao accountDao = new AccountDaoImpl(); 实际上,为了降低耦合,我们不应该在此处对accountDao对象进行实例化操作,应该直接是private IAccountDao accountDao; 。为了将该对象实例化,我们就需要用到依赖注入。

2.依赖注入介绍

依赖注入(Dependency Injection, DI):它是spring框架核心IoC的具体实现(IoC是一种思想,而DI是一种设计模式)。 在编写程序时,通过控制反转,把对象的创建交给了 spring,但是代码中不可能出现没有依赖的情况。IoC 解耦只是降低他们的依赖关系,但不会消除。例如:我们的业务层仍会调用持久层的方法,这种业务层和持久层的依赖关系,在使用 spring 之后,就让 spring 来维护了。简单的说,就是让框架把持久层对象传入业务层,而不用我们自己去获取。

3.依赖注入的数据类型和方式

在依赖注入中,能够注入的数据类型有三类:

  • 基本类型和String类型
  • 其他Bean类型:在注解或配置文件中配置过的Bean,也就是Spring容器中的Bean
  • 复杂类型(集合类型):例如List、Array、Map等

为了演示依赖注入,我们在src/main/java目录下,新建一个包entity,在该包下新建实体类People:

代码中的字段如下,注意构造方法一定要加上无参构造方法。

public class People {
//如果是经常变化的数据,并不适用于依赖注入
private String name;
private Integer age;
//Date类型不是基本类型,属于Bean类型
private Date birthDay;
//以下都是集合类型
private String[] myString;
private List<String> myList;
private Set<String> mySet;
private Map<String, String> myMap;
private Properties myProps; //为了节省空间,这里省略了所有的set方法和toString方法,在实际代码中要补上 public People() { } //提供默认构造方法 public People(String name, Integer age, Date birthDay) {
this.name = name;
this.age = age;
this.birthDay = birthDay;
}
}

注入的方式有三种:

  • 使用构造方法注入

    这种方式使用的标签为constructor-arg,在bean标签的内部使用,该标签的属性有五种,其中的1-3种用于指定给构造方法中的哪个参数注入数据:

    1. type:用于要注入的数据的数据类型,该数据类型也是构造方法中某个或某些参数的类型
    2. index:用于给构造方法中指定索引位置的参数注入数据,索引从0开始
    3. name:用于给构造方法中指定名称的参数注入数据(最常用)
    4. value:要注入的数据的值(只能是基本类型或者String类型)
    5. ref:用于指定其他bean类型数据(只能是在Spring的IOC核心中出现过的bean对象)
    <bean id="people1" class="entity.People">
    <!-- 如果有多个String类型的参数,仅使用type标签无法实现注入 -->
    <constructor-arg type="java.lang.String" value="Jack"></constructor-arg>
    <constructor-arg index="1" value="18"></constructor-arg>
    <constructor-arg name="birthDay" ref="date"></constructor-arg>
    </bean>
    <!-- 配置一个日期对象 -->
    <bean id="date" class="java.util.Date"></bean>
  • 使用set方法注入

    这种方式使用的标签为property,在bean标签的内部使用,该标签的属性有三种:

    1. name:用于指定注入时所调用的set方法名称,即set之后的名称,并且要改成小写(例如"setUsername"对应的name就是"username"),换句话说就是属性名称
    2. value:要注入的数据的值(只能是基本类型或者String类型)
    3. ref:用于指定其他bean类型数据(只能是在Spring的IOC核心中出现过的bean对象)
    <bean id="people2" class="entity.People">
    <property name="name" value="Jack"></property>
    <property name="age" value="18"></property>
    <property name="birthDay" ref="date"></property>
    </bean>
  • 使用注解注入:本篇主要讲解使用xml配置文件的方式注入,因此这种方法暂不做介绍

4.关于集合类型的注入

这里我们使用set方法来向集合中注入数据,对于使用的标签,注意以下三点:

  1. 用于给List结构集合注入的标签有:array、list、set
  2. 用于给Map结构集合注入的标签有:map、props
  3. 结构相同,标签可以互换
<bean id="people3" class="entity.People">
<property name="myString">
<array>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</array>
</property>
<property name="myList">
<list>
<value>ListA</value>
<value>ListB</value>
<value>ListC</value>
</list>
</property>
<property name="mySet">
<set>
<value>SetA</value>
<value>SetB</value>
<value>SetC</value>
</set>
</property>
<property name="myMap">
<map>
<entry key="A" value="MapA"></entry>
<entry key="B" value="MapB"></entry>
<!-- 对于entry标签,可以使用value属性来指定值,也可以在标签内部使用value标签 -->
<entry key="C">
<value>MapC</value>
</entry>
</map>
</property>
<property name="myProps">
<props>
<!-- 对于prop标签,只有key属性,没有value属性,所以直接将该标签的值作为value -->
<prop key="A">PropA</prop>
<prop key="B">PropB</prop>
<prop key="C">PropC</prop>
</props>
</property>
</bean>
5.完善之前的代码

在本部分的开头,我们还有一个问题没有解决,那就是AccountServiceImpl类中的accountDao对象无法实例化。现在我们就可以通过配置的方式来对进行依赖注入:

<bean id="accountService" class="service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
<bean id="accountDao" class="dao.impl.AccountDaoImpl"></bean>

最后我们再进行统一的测试,修改Client类中的main方法:

public static void main(String[] args) {
//验证依赖注入
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
People people1 = applicationContext.getBean("people1", People.class);
System.out.println(people1);
People people2 = applicationContext.getBean("people2", People.class);
System.out.println(people2);
People people3 = applicationContext.getBean("people3", People.class);
System.out.println(people3); //向accountService中注入accountDao以调用saveAccounts方法
IAccountService accountService = (IAccountService) applicationContext.getBean("accountService");
System.out.println(accountService);
accountService.saveAccounts(); }

运行代码,结果如下:

Spring升级案例之IOC介绍和依赖注入的更多相关文章

  1. Spring控制反转(IOC)和依赖注入(DI),再记不住就去出家!

    每次看完spring的东西感觉都理解了,但是过了一段时间就忘,可能是不常用吧,也是没理解好,这次记下来. 拿ssh框架中的action,service,dao这三层举例: 控制反转:完成一个更新用户信 ...

  2. Spring IOC - 控制反转(依赖注入) - 入门案例 - 获取对象的方式 - 别名标签

    1. IOC - 控制反转(依赖注入) 所谓的IOC称之为控制反转,简单来说就是将对象的创建的权利及对象的生命周期的管理过程交 由Spring框架来处理,从此在开发过程中不再需要关注对象的创建和生命周 ...

  3. Spring的控制反转(IOC)和依赖注入(DI)具体解释

    Spring的控制反转(IOC)和依赖注入(DI)具体解释 首先介绍下(IOC)控制反转: 所谓控制反转就是应用本身不负责依赖对象的创建及维护,依赖对象的创建及维护是由外部容器负责的.这样控制器就有应 ...

  4. 深入分析MVC中通过IOC实现Controller依赖注入的原理

    这几天利用空闲时间,我将ASP.NET反编译后的源代码并结合园子里几位大侠的写的文章认真的看了一遍,收获颇丰,同时也摘要了一些学习内容,存入了该篇文章:<ASP.NET运行机制图解>,在对 ...

  5. 控制反转(IoC)与依赖注入(DI)

    前言 最近在学习Spring框架,它的核心就是IoC容器.要掌握Spring框架,就必须要理解控制反转的思想以及依赖注入的实现方式.下面,我们将围绕下面几个问题来探讨控制反转与依赖注入的关系以及在Sp ...

  6. IOC容器的依赖注入

    1.依赖注入发生的时间 当Spring IoC容器完成了Bean定义资源的定位.载入和解析注册以后,IoC容器中已经管理类Bean定义的相关数据,但是此时IoC容器还没有对所管理的Bean进行依赖注入 ...

  7. Spring详解(三)------DI依赖注入

    上一篇博客我们主要讲解了IOC控制反转,也就是说IOC 让程序员不在关注怎么去创建对象,而是关注与对象创建之后的操作,把对象的创建.初始化.销毁等工作交给spring容器来做.那么创建对象的时候,有可 ...

  8. springboot成神之——ioc容器(依赖注入)

    springboot成神之--ioc容器(依赖注入) spring的ioc功能 文件目录结构 lang Chinese English GreetingService MyRepository MyC ...

  9. 控制反转(IOC)和依赖注入(DI)

    控制反转(IOC)和依赖注入(DI)IoC(Inversion of Control,控制反转) 是Spring 中一个非常非常重要的概念,它不是什么技术,而是一种解耦的设计思想.它的主要目的是借助于 ...

随机推荐

  1. PHP 数组排序方法总结

    sort:本函数为 array 中的单元赋予新的键名.这将删除原有的键名而不仅是重新排序. rsort:本函数对数组进行逆向排序(最高到最低). 删除原有的键名而不仅是重新排序. asort:对数组进 ...

  2. ORACLE清除某一字段重复的数据(选取重复数据中另一个字段时期最大值)

    需求:资产维修表中同一资产可能维修完继续申请维修,这时候维修状态需要根据最近的维修时间去判断维修状态,所以同一资产ID下会出现重复的数据(维修审批通过,维修审批未通过),或者可能不出现(未申请维修), ...

  3. spark-scheduled调度算法

    1.3源码是如此,后面新版本源码会尽可能的根据用户shell配置的参数进行分配 1.spareadOutApps 尽可能分配到多的机器上面execute和CPU core 2.非spareadouta ...

  4. LeetCode33 Search in Rotated Sorted Array

    题目: Suppose a sorted array is rotated at some pivot unknown to you beforehand. (i.e., 0 1 2 4 5 6 7  ...

  5. datatables完整的增删改查

    1.需要指定datatables的ID <button class="btn btn-primary" id="newAttribute">新增证照 ...

  6. React中的路由系统

    React中的路由系统 提起路由,首先想到的就是 ASPNET MVC 里面的路由系统--通过事先定义一组路由规则,程序运行时就能自动根据我们输入的URL来返回相对应的页面.前端中的路由与之类似,前端 ...

  7. PHP序列化变量的4种方法

    序列化是将变量转换为可保存或传输的字符串的过程:反序列化就是在适当的时候把这个字符串再转化成原来的变量使用.这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性. 1.  serialize ...

  8. 接收键盘输入的字符串,用FileWirter类将字符串写入文件,用FileReader类读出文件内容显示在屏幕上

    public class SY63 { public static void main(String[] args) throws Exception { System.out.print(" ...

  9. shiro 自定义过滤器,拦截过期session的请求,并且以ajax形式返回

    自定义过滤器: public class CustomFormAuthenticationFilter extends FormAuthenticationFilter { @Override pro ...

  10. 从 s = &quot;我爱北京天安门&quot; 中悟道了-----------迭代器操作print(c.__next__())的最!大!好!处!-----------------------------------------------------可以一个一个输出

    s = "我爱北京天安⻔"c = s.__iter__() # 获取迭代器# print(c) # 打印迭代器的地址# print(c.__next__()) # 打印迭代器中的下 ...