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"));
采用断点调试,我们可以发现:
- 对于ApplicationContext来说,执行ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");之后,立刻就会输出“service创建了”和“dao创建了”。
- 而对于BeanFactory来说,只有当执行到System.out.println(factory.getBean("accountDao"));之后,才会输出“dao创建了”。
- 这也就说明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);
}
使用断点调试,我们可以发现:
-
在执行到ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");时,就会输出“service创建了”,不会输出“dao创建了”。
-
只有当执行到IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);和IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao");时,才会输出“dao创建了”。
-
并且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();
这个时候,我们再去使用断点调试,可以发现:
- 当执行到ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");时,就会输出“service创建了”和“service初始化了”。
- 只有当执行到IAccountDao accountDao = applicationContext.getBean("accountDao", IAccountDao.class);和IAccountDao accountDao1 = (IAccountDao) applicationContext.getBean("accountDao");时,才会输出“dao创建了”和“dao初始化了”。
- 执行到((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种用于指定给构造方法中的哪个参数注入数据:
- type:用于要注入的数据的数据类型,该数据类型也是构造方法中某个或某些参数的类型
- index:用于给构造方法中指定索引位置的参数注入数据,索引从0开始
- name:用于给构造方法中指定名称的参数注入数据(最常用)
- value:要注入的数据的值(只能是基本类型或者String类型)
- 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标签的内部使用,该标签的属性有三种:
- name:用于指定注入时所调用的set方法名称,即set之后的名称,并且要改成小写(例如"setUsername"对应的name就是"username"),换句话说就是属性名称
- value:要注入的数据的值(只能是基本类型或者String类型)
- 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方法来向集合中注入数据,对于使用的标签,注意以下三点:
- 用于给List结构集合注入的标签有:array、list、set
- 用于给Map结构集合注入的标签有:map、props
- 结构相同,标签可以互换
<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();
}
运行代码,结果如下: