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

    运行代码,结果如下:

    运行结果

  • 相关阅读:
    [RxJS] sample
    [Whole Web] SQL INJECTION
    [RxJS] Using iif
    [React] Animate SVG Paths with Framer Motion
    bash 教程5 shell 流程控制 条件判断 重定向 read [MD]
    bash 教程4 shell 脚本 调试 环境 [MD]
    『前端算法』数组合并两个有序数组
    块设备读写测试
    IO栈整体认知
    MySQL [Warning] Can’t create test file xxx lowertest(转)
  • 原文地址:https://www.cnblogs.com/liyier/p/13299505.html
Copyright © 2020-2023  润新知