IoC 的概念和作用
# 20.IoC 的概念和作用
在了解了程序中的耦合后,我们就可以看看 Spring 是如何解决这个问题的,也就是 IoC。
# 获取依赖的方式
之前我们在获取对象时,都是采用 new 的方式。是主动去寻找依赖的:
改为用工厂模式后,我们获取对象时,是跟工厂要资源,由工厂为我们查找或者创建对象,不再是主动的去寻找依赖,而是被动的提供
这种被动接收的方式获取对象的思想就是控制反转,它是 Spring 框架的核心之一。为什么叫控制反转呢?首先之前我们是使用 new 的方式来创建对象的,而改为使用工厂之后,就相当于将获取对象的主动权,交给了工厂,至于工厂返回的对象是否能用,我们是控制不了的,而是在配置文件配置的。
换句话说,控制权发生了转移,因此叫控制反转 Inverse Of Control,简称 IoC。IoC 能降低计算机程序的耦合。
如果说全部由我们自己实现 IoC,就太耗费精力了,我们可以使用将控制权交给 Spring。
特别说明:Spring5 版本是用 Java8 编写的,所以要求 Java8 及以上版本。
# 准备环境
之前我们提到了一个压缩包:LearnSpring/lib/spring-framework-5.0.2.RELEASE-dist.zip,这个 zip 包含了 Spring Framework 的所有 jar 包、文档和约束文件,解压后有 3 个文件夹:
- docs 文件夹:文档
- libs 文件夹:jar 文件
- schema 文件夹:约束文件
在 libs 目录下,每个依赖都有自己的文档和源码,例如:
- spring-aop-5.0.2.RELEASE.jar
- spring-aop-5.0.2.RELEASE-javadoc.jar
- spring-aop-5.0.2.RELEASE-sources.jar
*.javadoc.jar 结尾的就是该依赖的文档,而 *.sources.jar 结尾的就是源码
我们打开 spring-framework-5.0.2.RELEASE\docs\spring-framework-reference\index.html,可以看到 Spring Framework 的文档:
希望读者们可以将这个网页添加到收藏夹之类的地方,方便后续的阅读
# 调整项目
我们不再自己实现 IoC 了,因此 BeanFactory
可以删掉;同理,beans.properties 也删掉
调整 service 层:
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao = new AccountDaoImpl();
@Override
public void saveAccount() {
accountDao.saveAccount();
}
}
2
3
4
5
6
7
8
调整界面层 Client
:
public class Client {
public static void main(String[] args) {
IAccountService as = new AccountServiceImpl();
as.saveAccount();
}
}
2
3
4
5
6
# 添加依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
2
3
4
5
我们可以看到多了 6 个依赖:
第一个是 AOP,我们学到 AOP 的时候才用到;其他的例如 beans、context 都是要用到的
而最后一个 jcl,其实就是日志依赖 Jakarta Commons Logging,我们之前其实讲过:常见的日志框架-简单介绍 (opens new window)
我们可以在 Maven 视图中,分析这个依赖的关系:
可以看到如下结构:
这个图是不是在我们介绍 Spring 时,用到的图很类似?
而 Core Container 就是我们说的容器
# 新建 bean.xml
我们新建一个配置文件,bean.xml,其实也可以叫其他名字,只要不包含中文;
然后我们导入约束,可以去 docs\spring-framework-reference\index.html 这里找:我们点击 core
然后我们搜索 xmlns:可以看到有相关的约束,这里我们用第一个就行
<?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">
</beans>
2
3
4
5
6
7
然后我们就可以配置全限定类名了,这里我们用 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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 把对象交给Spring来管理 -->
<bean id="accountService" class="com.peterjxl.service.impl.AccountServiceImpl"/>
<bean id="accountDao" class="com.peterjxl.dao.impl.AccountDaoImpl"/>
</beans>
2
3
4
5
6
7
8
9
10
11
bean 标签的内容,和我们之前 beans.properties 文件里的内容是一样的,id 和全限定类名。
# 获取容器并创建对象
接下来我们就是获取到容器,然后根据 id 获取里面的对象了。
在 Spring 中,我们并不会直接使用 BeanFactory,而是 ApplicationContext,这是一个接口,实现类有 ClassPathXmlApplicationContext
,FileSystemXmlApplicationContext
和 AnnotationConfigApplicationContext
,前面两个是基于配置文件的,后面是基于注解的,这里我们用第一个
然后我们就通过容器来获取对象,改写 Client 的代码:
public static void main(String[] args) {
// 1. 获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2. 根据id获取Bean对象
IAccountService as = (IAccountService)ac.getBean("accountService");
IAccountDao adao = ac.getBean("accountDao", IAccountDao.class);
System.out.println(as);
System.out.println(adao);
}
2
3
4
5
6
7
8
9
10
首先我们创建了一个 ApplicationContext 对象,其参数就是配置文件的位置;
然后我们通过 getBean 方法获取了 IAccountService 对象,由于 getBean 返回的是 Object 对象,需要强转;
而如果我们获取对象时,传递了字节码对象,那么就不用强转也可以(第 6 行获取 IAccountDao 的时候)
运行结果:可以看到正常创建了实现类
com.peterjxl.service.impl.AccountServiceImpl@46d56d67
com.peterjxl.dao.impl.AccountDaoImpl@d8355a8
2
# ApplicationContext 的三个常用实现类
说明:
ClassPathXmlApplicationContext
:它可以加载类路径下的配置文件,要求配置文件必须在类路径下。不在的话,加载不了。(最常用)FileSystemXmlApplicationContext
:它可以加载磁盘任意路径下的配置文件(必须有访问权限)AnnotationConfigApplicationContext
:它是用于读取注解创建容器的,我们后续会学习
演示 FileSystemXmlApplicationContext
:我们可以将配置文件放到桌面上,此时文件的路径为:C:\Users\peterjxl\Desktop\bean.xml
public static void main(String[] args) {
// 1. 获取核心容器对象
ApplicationContext ac = new FileSystemXmlApplicationContext("C:\\Users\\peterjxl\\Desktop\\bean.xml");
// 2. 根据id获取Bean对象
IAccountService as = (IAccountService)ac.getBean("accountService");
IAccountDao adao = ac.getBean("accountDao", IAccountDao.class);
System.out.println(as);
System.out.println(adao);
}
2
3
4
5
6
7
8
9
10
可以看到第二个实现类依赖具体的文件路径,可移植性不强,因此不推荐使用第二个。
# ApplicationContext 和 BeanFactory 的区别
之前我们使用 ApplicationContext 接口,获取了容器;其实也可以用 BeanFactory 接口
ApplicationContext:它在构建核心容器时,创建对象采取的策略是采用立即加载的方式。也就是说,只要一读取完配置文件马上就创建配置文件中的配置对象。我们可以在实现类的构造函数中,加个打印语句,即可验证。如果要用单例对象,采用此接口。
BeanFactory:它在构建核心容器时,创建对象采取的策略是采用延迟加载的方式。也就是说,什么时候根据 id 获取对象了,什么时候才真正的创建对象。使用多例对象模式时,使用这个接口。
演示 ApplicationContext 立即加载,我们可以在构造函数中加个打印语句:
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao = new AccountDaoImpl();
public AccountServiceImpl() {
System.out.println("对象创建了");
}
@Override
public void saveAccount() {
accountDao.saveAccount();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
运行结果:
对象创建了
com.peterjxl.service.impl.AccountServiceImpl@46d56d67
com.peterjxl.dao.impl.AccountDaoImpl@d8355a8
2
3
演示使用 BeanFactory:
Resource resource = new ClassPathResource("bean.xml");
BeanFactory factory = new XmlBeanFactory(resource);
2
运行后,可以看到没有打印构造函数里的方法
实际开发中,更多使用的是 ApplicationContext,因为 BeanFactory 是顶层接口,功能没那么完善;并且使用 ApplicationContext 还可以配置是使用单例模式还是多例模式,功能更完善
# 三种创建 Bean 对象的方式
之前我们都是创建我们自己项目中的 bean;实际开发中,经常会用到第三方依赖,要如何存放呢?此时就涉及到其他创建 Bean 对象的方式
# 第一种方式
使用默认构造函数创建对象。在 Spring 的配置文件中使用 bean 标签,配以 id 和 class 属性之后,且没有其他属性和标签时,采用的就是默认构造函数创建 bean 对象,此时如果类中没有默认构造函数,则对象无法创建。
<bean id="accountService" class="com.peterjxl.service.impl.AccountServiceImpl"/>
我们可以试着给默认函数加个参数:
public AccountServiceImpl(String name) {
System.out.println("对象创建了");
}
2
3
此时运行会报错:
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.peterjxl.service.impl.AccountServiceImpl]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.peterjxl.service.impl.AccountServiceImpl.<init>()
可以看到其提示没有找到默认构造函数:No default constructor found
# 第二种方式
而实际开发中,我们经常会遇到没有默认构造函数的类,例如一些第三方的工具类;如果我们修改其源码,加上构造函数,也太麻烦了!此时我们可以调用其提供的构造方法,并使用 set 属性作为构造方法的函数(下一篇会讲怎么 set)。
除此之外,有时候我们并不是调用构造方法来获取对象,而是可能调用一个工厂类的方法,由该工厂类给我们返回对象。
我们可以新建一个类来模拟下:
package com.peterjxl.factory;
import com.peterjxl.service.IAccountService;
/**
* 模拟一个工厂类(该类可能是存在于jar包中的,我们无法通过修改源码的方式来提供默认构造函数)
*/
public class InstanceFactory {
public IAccountService getAccountService() {
return new AccountServiceImpl("test");
}
}
2
3
4
5
6
7
8
9
10
11
配置 bean.xml,指定工厂类和构造方法:
<bean id="instanceFactory" class="com.peterjxl.factory.InstanceFactory"/>
<bean id="accountService" factory-bean="instanceFactory" factory-method="getAccountService"/>
2
我们试着运行,可以看到是能正常获取对象的
# 第三种方式
有时候我们是调用工厂类中的静态方法来获取 bean。我们可以建立一个工厂类来模拟:
public class StaticFactory {
public static IAccountService getAccountService() {
return new AccountServiceImpl("test");
}
}
2
3
4
5
配置 bean.xml:
<bean id="accountService" class="com.peterjxl.factory.StaticFactory" factory-method="getAccountService"/>
我们试着运行,也是可以看到能正常获取对象的
# bean 的 scope 属性
我们之前使用 ApplicationContext,默认都是单例模式的:
IAccountService as = (IAccountService)ac.getBean("accountService");
IAccountService as2 = (IAccountService)ac.getBean("accountService");
System.out.println("as == as2 ? :" + (as == as2));
2
3
运行结果:as == as2 ? :true
我们可以通过配置 bean 标签的 scope 属性,来进行调整。取值:
- singleton:单例的(默认值)
- prototype:多例的
- request:作用于 Web 应用的请求范围
- session:作用于 Web 应用的会话范围
- global-session:作用于集群环境的会话范围(全局会话范围),当不是集群环境时,它就是 session,也就是为 prototype,即多例模式
常用的就是单例和多例模式。我们可以测试下,调整为多例模式:
<bean id="accountService"
class="com.peterjxl.factory.StaticFactory"
factory-method="getAccountService"
scope="prototype"
/>
2
3
4
5
运行结果:as == as2 ? :false
# bean 的生命周期
# 单例模式
对于单例对象而言,生命周期是这样的:
- 出生:当容器创建时对象出生
- 活着:只要容器在,对象一直活着
- 死亡:容器销毁,对象消亡
- 总结:单例对象的生命周期和容器相同
我们可以自定义初始化和销毁方法,当对象被初始化时,我们可以自定义对象的初始化方法;当对象被销毁时,我们可以自定义对象的销毁方法。
我们可以演示下,在 AccountServiceImpl 里添加 init 和 destroy 方法:
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao = new AccountDaoImpl();
public AccountServiceImpl(String name) {
System.out.println("对象创建了");
}
@Override
public void saveAccount() {
accountDao.saveAccount();
}
public void init() {
System.out.println("对象初始化了");
}
public void destroy() {
System.out.println("对象销毁了");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
然后配置 bean:
<bean id="accountService"
class="com.peterjxl.factory.StaticFactory"
factory-method="getAccountService"
init-method="init"
destroy-method="destroy"
/>
2
3
4
5
6
此时我们运行 Client,可以看到输出了“对象初始化了”,但没有输出“对象销毁了”;这是因为我们没有调用容器的销毁方法,就释放内存了;所以我们得手动关闭容器:注意要强制转换
((ClassPathXmlApplicationContext) ac).close();
然后再次运行,可以看到确实有销毁对象:
对象创建了
对象初始化了
as == as2 ? :true
com.peterjxl.service.impl.AccountServiceImpl@604ed9f0
com.peterjxl.dao.impl.AccountDaoImpl@6a4f787b
对象销毁了
2
3
4
5
6
# 多例模式
对于多例对象而言,生命周期是这样的:
- 出生:当我们使用对象时 Spring 框架为我们创建
- 活着:对象只要是在使用过程中就一直活着
- 死亡:当对象长时间不用,且没有别的对象引用时,由 Java 的垃圾回收器回收
# 源码
本项目已将源码上传到 GitHub (opens new window) 和 Gitee (opens new window) 上。并且创建了分支 demo2,读者可以通过切换分支来查看本文的示例代码。