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

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.创建工程

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

SpringIOC的项目结构

首先在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配置文件。如下图:

创建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容器的底层接口。

ApplicationContext接口的继承关系

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

ApplicationContext接口的实现类

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

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();

}

运行代码,结果如下:

运行结果

posted @ 2020-07-14 15:51  李一二  阅读(171)  评论(0编辑  收藏