《SpringBoot 3:入门与应用实战》第 2 章 IOC 思想与实现 阅读笔记 1
控制反转 (Inverse of Control,IOC) 这个概念对于刚开始接触 Spring Framework 的读者不是那么容易理解。为了能让读者更容易理解 IOC 的思想,并且能尽可能一次性理解透彻,本章将会从一个场景演绎开始推导 IOC 思想的实现过程。
2.1 IOC 是怎么来的
2.1.1 原生 Servlet 时代的三层架构
1.构建基于 Maven 的原生 Servlet 工程
采用 Maven 来完成工程构建,首先使用 IDEA 创建一个新的 Maven 工程。



随后在 pom.xml 文件中引入 jakarta.servlet-api 的坐标(注意此处引入的版本为 6.0,对应的 artifactId 已不再是 javax 开头,而是 jakarta 开头);
另外为了保证工程的编译使用 Java 17,还需要引入 Maven 的编译插件 maven-compiler-plugin,并声明 source、target、encoding 属性;
最后,将工程的打包方式改为 war 包,因为我们搭建的是一个 Web 工程。

2.工程部署 Servlet 容器
工程创建完毕后,不要着急编写代码,而要先把工程部署到 Servlet 容器中。选择 Tomcat 10 作为 Servlet 容器来运行工程。
安装 Tomcat 10 可参考如下步骤:
(1) 从 Apache Tomcat 的官方网站下载最新版的 Tomcat 10,并解压到没有中文名的目录下;

(2) 来到 IDEA 中,在IDEA中添加Tomcat。

将 Tomcat 配置到 IDEA 后,设置编译打包输出配置。

最后,完成工程的 Servlet 容器部署。

3.编写 Servlet 测试用例
新建一个 Servlet。

java
package com.yangjunbo.servlet;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(urlPatterns = "/demo1")
public class DemoServlet1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println("DemoServlet1 run ......");
}
}
启动 Tomcat,IDEA 会自动编译工程并部署到 Tomcat 中。可以正常打印,证明工程搭建并配置成功。

4.编写 Service 层与 Dao 层
在工程中分别新建 servlet、service、dao 三个包,并分别创建 DemoService 和 DemoDao 接口以及它们的实现类。

(1) Dao 与 DaoImpl
先定义一个 DemoDao 接口,并声明一个 findAll 方法;之后编写它对应的实现类 DemoDaoImpl。

java
package com.yangjunbo.dao;
import java.util.List;
/**
* ClassName: DeamDao
* Package: com.yangjunbo.dao
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 11:53
* @Version 1.0
*/
public interface DemoDao {
List<String> findAll();
}

java
package com.yangjunbo.dao.impl;
import com.yangjunbo.dao.DemoDao;
import java.util.Arrays;
import java.util.List;
/**
* ClassName: DemoDaoImpl
* Package: com.yangjunbo.dao.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 11:57
* @Version 1.0
*/
public class DemoDaoImpl implements DemoDao {
@Override
public List<String> findAll() {
return Arrays.asList("aaa", "bbb", "ccc");
}
}
(2) Service 与 ServiceImpl
再来编写 Service 层的代码,编写一个 DemoService 接口,并声明 findAll 方法;随后编写对应的实现类 DemoServiceImpl,并在内部依赖DemoDao 接口,调用 DemoDao 的 findAll 方法查询数据。

java
package com.yangjunbo.service;
import java.util.List;
/**
* ClassName: DemoService
* Package: com.yangjunbo.service
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 13:20
* @Version 1.0
*/
public interface DemoService {
List<String> findAll();
}

java
package com.yangjunbo.service.impl;
import com.yangjunbo.dao.DemoDao;
import com.yangjunbo.dao.impl.DemoDaoImpl;
import com.yangjunbo.service.DemoService;
import java.util.List;
/**
* ClassName: DemoServiceImpl
* Package: com.yangjunbo.service.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 13:21
* @Version 1.0
*/
public class DemoServiceImpl implements DemoService {
private DemoDao demoDao = new DemoDaoImpl();
@Override
public List<String> findAll() {
return demoDao.findAll();
}
}
5.修改 DemoServlet1
模拟整体的三层架构,DemoServlet1 依赖 DemoService,并在 doGet 方法触发时,执行 DemoService 的 findAll 方法并输出。

java
package com.yangjunbo.servlet;
import com.yangjunbo.service.DemoService;
import com.yangjunbo.service.impl.DemoServiceImpl;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(urlPatterns = "/demo1")
public class DemoServlet1 extends HttpServlet {
DemoService demoService = new DemoServiceImpl();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println(demoService.findAll().toString());
}
}
6.重新运行应用并测试效果
编码完毕后,我们将工程重新部署到 Tomcat 并运行,访问 /demo1 路径,浏览器中会输出 'aaa', 'bbb', 'ccc',说明代码编写正确且运行正常。


2.1.2 需求变更
假设上述的项目由读者来负责,项目已经有一定规模,在数据库选型时使用了 MySQL。在项目临近交付时,甲方要求将数据库由 MySQL 切换为 Oracle。你迫于甲方的要求无奈只能将数据库实现改为 Oracle,但是由于 Dao 层代码已经使用 MySQL 的语法实现,读者都知道,不同的数据库在 SQL 语法层面有一些细微的差别(例如分页),因此修改数据库实现时不能只修改数据库驱动和连接池的配置,而要针对不同的 SQL 语法特性进行重新适配,又由于该项目已经有一定规模,改动量非常大。
1.修改DaoImpl

2.数据库再次变动
经过一星期的紧张修改,你终于完成了全部 Dao 层的 SQL 语句修改,马上就到项目交付的时候,甲方又因为一些"不可预见"的特殊原因,决定将数据库改回 MySQL。甲方提需求固然轻松,你作为开发者却非常痛苦,本来好不容易改完的代码又要再改一遍。那么是否有解决方案能避免大量改动呢?
3.引入静态工厂
如果在项目开发时就将适配不同数据库的 Dao 层实现全部制作完毕,在 Service 层引用时借助静态工厂创建特定的实现类。当需求发生变更时,只需要改变一处代码,而不用修改所有的 Dao 层代码。
(1) 构造静态工厂
下面改造代码,我们来声明一个静态工厂类,并给这个类起名:BeanFactory。
此处由 BeanFactory 负责实例化 DemoDao 的实现类,而为了区分基于 MySQL 和 Oracle 两个不同的数据库,DemoDaoImpl 类也要相应地复制出两份,分别命名为 DemoMySQLDao 和 DemoOracleDao。

java
package com.yangjunbo.dao.impl;
import com.yangjunbo.dao.DemoDao;
import java.util.Arrays;
import java.util.List;
/**
* ClassName: DemoDaoImpl
* Package: com.yangjunbo.dao.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 11:57
* @Version 1.0
*/
public class DemoMysqlDaoImpl implements DemoDao {
@Override
public List<String> findAll() {
return Arrays.asList("Mysql", "Mysql", "Mysql");
}
}

java
package com.yangjunbo.dao.impl;
import com.yangjunbo.dao.DemoDao;
import java.util.Arrays;
import java.util.List;
/**
* ClassName: DemoDaoImpl
* Package: com.yangjunbo.dao.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 11:57
* @Version 1.0
*/
public class DemoOracleDaoImpl implements DemoDao {
@Override
public List<String> findAll() {
return Arrays.asList("Oracle", "Oracle", "Oracle");
}
}

java
package com.yangjunbo.factory;
import com.yangjunbo.dao.DemoDao;
import com.yangjunbo.dao.impl.DemoOracleDaoImpl;
public class BeanFactory {
public static DemoDao getDemoDao() {
// return new DemoMysqlDaoImpl();
return new DemoOracleDaoImpl();
}
}
(2) 改造 ServiceImpl
DemoServiceImpl 中引用的 DemoDao 实现类不再使用 new 关键字创建,而是由 BeanFactory 的静态方法 getDemoDao 返回。
如此改造后,即便 Service 层的实现类再多,Dao 层的实现类再多,当发生需求更改时,只需要改动 BeanFactory 中静态方法的返回值。

java
package com.yangjunbo.service.impl;
import com.yangjunbo.dao.DemoDao;
import com.yangjunbo.dao.impl.DemoDaoImpl;
import com.yangjunbo.factory.BeanFactory;
import com.yangjunbo.service.DemoService;
import java.util.List;
/**
* ClassName: DemoServiceImpl
* Package: com.yangjunbo.service.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 13:21
* @Version 1.0
*/
public class DemoServiceImpl implements DemoService {
//private DemoDao demoDao = new DemoDaoImpl();
DemoDao demoDao = BeanFactory.getDemoDao();
@Override
public List<String> findAll() {
return demoDao.findAll();
}
}
2.1.3 紧耦合问题
1.类之间的依赖关系---紧耦合
BeanFactory 强依赖于 DemoMysqlDaoImpl 和 DemoOracleDaoImpl,如果这两个类不存在,工程编译出错,无法启动。
2.解决紧耦合
因为工程中没有 DemoOracleDaoImpl.java 文件,所以工程无法编译,工作无法正常进行。但是项目开发进度不能因为丢失一个类而被阻塞,有没有一种办法能解决这个无法编译的问题?
反射可以通过声明一个类的全限定名,获取它的字节码描述,如此一来也能构造对象!
如此改造后,BeanFactory 便可以正常编译,尽管在 DemoService 的初始化时还是会出现问题,但是工程可以正常启动。

java
package com.yangjunbo.factory;
import com.yangjunbo.dao.DemoDao;
import com.yangjunbo.dao.impl.DemoOracleDaoImpl;
public class BeanFactory {
// public static DemoDao getDemoDao() {
// // return new DemoMysqlDaoImpl();
// return new DemoOracleDaoImpl();
// }
public static DemoDao getDemoDao() {
try {
return (DemoDao) Class.forName("com.yangjunbo.dao.impl.DemoOracleDaoImpl")
.getDeclaredConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
}
}
}
3.弱依赖
使用反射机制之后,程序出现错误的现象不再是在编译时出现,而是在工程启动后,由于 BeanFactory 要构造 DemoDaoOracleImpl 时确实还没有该类,因此抛出 ClassNotFoundException 异常。这样 BeanFactory 对 DemoDaoOracleImpl 的依赖程度就降低了,这种情况可以被称作"弱依赖"(松耦合)。
2.1.4 硬编码问题
在切换 MySQL 和 Oracle 数据库时还是会出现一个问题:由于类的全限定名被写死在 BeanFactory 的源码中,导致每次切换数据库后工程依然需要重新编译才可以正常运行,这种方式不是很理想,应该有更好的处理方案。
1.引入外部化配置文件
每次 BeanFactory 被初始化时,让它读取指定的配置文件,就不会出现硬编码的现象。
(1) 加入 factory.properties 文件
为了方便接下来取出这些类的全限定名,示例代码中给每一个类名都起一个"别名",这样就可以根据别名来找到对应的全限定类名。

xml
demoDao=com.yangjunbo.dao.impl.DemoOracleDaoImpl
(2) 改造 BeanFactory
既然配置文件是 properties 类型,在 JDK 中刚好也有一个 API 名为 Properties,它可以解析 properties 文件。于是可以在 BeanFactory 中加入一个静态变量,并借助静态代码块,在工程启动的时候就初始化 Properties。配置文件读取到之后,下面的 getDemoDao 方法也要一并修改。

java
package com.yangjunbo.factory;
import com.yangjunbo.dao.DemoDao;
import com.yangjunbo.dao.impl.DemoOracleDaoImpl;
import java.io.IOException;
import java.util.Properties;
public class BeanFactory {
// public static DemoDao getDemoDao() {
// // return new DemoMysqlDaoImpl();
// return new DemoOracleDaoImpl();
// }
// public static DemoDao getDemoDao() {
// try {
// return (DemoDao) Class.forName("com.yangjunbo.dao.impl.DemoOracleDaoImpl")
// .getDeclaredConstructor().newInstance();
// } catch (Exception e) {
// e.printStackTrace();
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
private static Properties properties;
// 使用静态代码块初始化properties,加载factory.properties文件
static {
properties = new Properties();
try {
// 必须使用类加载器读取resource文件夹下的配置文件
properties.load(BeanFactory.class.getClassLoader()
.getResourceAsStream("factory.properties"));
} catch (IOException e) {
// BeanFactory类的静态初始化失败,后续代码也没有必要继续执行,抛出异常
throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
}
}
public static DemoDao getDemoDao() {
try {
Class<?> beanClazz = Class.forName(properties.getProperty("demoDao"));
return (DemoDao) beanClazz.getDeclaredConstructor().newInstance();
}catch (Exception e){
throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
}
}
}
制作一个通用的方法,这个方法传入要获得对象的"别名",由 BeanFactory 从配置文件中查找对应的全限定类名,反射构造对象返回即可。

java
package com.yangjunbo.factory;
import com.yangjunbo.dao.DemoDao;
import com.yangjunbo.dao.impl.DemoOracleDaoImpl;
import java.io.IOException;
import java.util.Properties;
public class BeanFactory {
// public static DemoDao getDemoDao() {
// // return new DemoMysqlDaoImpl();
// return new DemoOracleDaoImpl();
// }
// public static DemoDao getDemoDao() {
// try {
// return (DemoDao) Class.forName("com.yangjunbo.dao.impl.DemoOracleDaoImpl")
// .getDeclaredConstructor().newInstance();
// } catch (Exception e) {
// e.printStackTrace();
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
private static Properties properties;
// 使用静态代码块初始化properties,加载factory.properties文件
static {
properties = new Properties();
try {
// 必须使用类加载器读取resource文件夹下的配置文件
properties.load(BeanFactory.class.getClassLoader()
.getResourceAsStream("factory.properties"));
} catch (IOException e) {
// BeanFactory类的静态初始化失败,后续代码也没有必要继续执行,抛出异常
throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
}
}
// public static DemoDao getDemoDao() {
// try {
// Class<?> beanClazz = Class.forName(properties.getProperty("demoDao"));
// return (DemoDao) beanClazz.getDeclaredConstructor().newInstance();
// }catch (Exception e){
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
public static Object getBean(String beanName) {
try {
// 从properties文件中读取指定name对应类的全限定名,并反射实例化
Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
return beanClazz.getDeclaredConstructor().newInstance();
}catch (Exception e){
throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
}
}
}
如此改造之后,DemoServiceImpl 中就不再调用 getDemoDao 方法,而是转用 getBean 方法,并指定需要获取的对象名称为 "demoDao",即可获得想要的DemoDao实现类对象。

java
package com.yangjunbo.service.impl;
import com.yangjunbo.dao.DemoDao;
import com.yangjunbo.dao.impl.DemoDaoImpl;
import com.yangjunbo.factory.BeanFactory;
import com.yangjunbo.service.DemoService;
import java.util.List;
/**
* ClassName: DemoServiceImpl
* Package: com.yangjunbo.service.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 13:21
* @Version 1.0
*/
public class DemoServiceImpl implements DemoService {
//private DemoDao demoDao = new DemoDaoImpl();
//DemoDao demoDao = BeanFactory.getDemoDao();
DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
@Override
public List<String> findAll() {
return demoDao.findAll();
}
}
2.外部化配置
对于这种可能会变化的配置、属性等,通常不会直接硬编码在源码中,而是抽取为一些配置文件的形式(properties、XML、JSON、YML 等),配合程序对配置文件的加载和解析,从而达到动态配置、降低配置耦合度的目的。这种抽取配置文件的思想,称为"外部化配置"。
2.1.5 多次实例化
BeanFactory 演变至此依然存在问题,我们可以在 ServiceImpl 的构造方法中连续多次获取 DemoDao 的实现类对象,并观察这些对象的内存地址。
重新运行程序,可以发现连续 5 次获取 DemoDao 的实现类对象时,每个对象打印的内存地址都不相同,这证明创建了 5 个不同的DemoOracleDaoImpl!这样的设计并不合理,在一个工程中,具有功能的对象在没有特殊需求下,最好只存在一个(单实例)。

java
package com.yangjunbo.service.impl;
import com.yangjunbo.dao.DemoDao;
import com.yangjunbo.dao.impl.DemoDaoImpl;
import com.yangjunbo.factory.BeanFactory;
import com.yangjunbo.service.DemoService;
import java.util.List;
/**
* ClassName: DemoServiceImpl
* Package: com.yangjunbo.service.impl
* Description:
*
* @Author 杨钧博
* @Create 2026/8/11 13:21
* @Version 1.0
*/
public class DemoServiceImpl implements DemoService {
//private DemoDao demoDao = new DemoDaoImpl();
//DemoDao demoDao = BeanFactory.getDemoDao();
DemoDao demoDao = (DemoDao) BeanFactory.getBean("demoDao");
@Override
public List<String> findAll() {
for (int i = 0; i < 5; i++) {
System.out.println(BeanFactory.getBean("demoDao"));
}
return demoDao.findAll();
}
}
改良方法:引入缓存。
对于这些没有必要创建多个对象的组件,可以在 BeanFactory 中加入一个缓存区,并在 getBean 方法中设置缓存检查逻辑,如果缓存中存在指定对象,则直接返回;如果没有对象,则会先创建对象,并将创建好的对象放入缓存中,再返回。为了控制并发问题,需要引入双检锁保证对象只有一个。
改造完成后,重启工程再次测试,发现多次打印都指向同一个对象,说明改造完成。

java
package com.yangjunbo.factory;
import com.yangjunbo.dao.DemoDao;
import com.yangjunbo.dao.impl.DemoOracleDaoImpl;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class BeanFactory {
// public static DemoDao getDemoDao() {
// // return new DemoMysqlDaoImpl();
// return new DemoOracleDaoImpl();
// }
// public static DemoDao getDemoDao() {
// try {
// return (DemoDao) Class.forName("com.yangjunbo.dao.impl.DemoOracleDaoImpl")
// .getDeclaredConstructor().newInstance();
// } catch (Exception e) {
// e.printStackTrace();
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
private static Properties properties;
// 使用静态代码块初始化properties,加载factory.properties文件
static {
properties = new Properties();
try {
// 必须使用类加载器读取resource文件夹下的配置文件
properties.load(BeanFactory.class.getClassLoader()
.getResourceAsStream("factory.properties"));
} catch (IOException e) {
// BeanFactory类的静态初始化失败,后续代码也没有必要继续执行,抛出异常
throw new ExceptionInInitializerError("BeanFactory initialize error, cause: " + e.getMessage());
}
}
// public static DemoDao getDemoDao() {
// try {
// Class<?> beanClazz = Class.forName(properties.getProperty("demoDao"));
// return (DemoDao) beanClazz.getDeclaredConstructor().newInstance();
// }catch (Exception e){
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
// public static Object getBean(String beanName) {
// try {
// // 从properties文件中读取指定name对应类的全限定名,并反射实例化
// Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
// return beanClazz.getDeclaredConstructor().newInstance();
// }catch (Exception e){
// throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
// }
// }
// 缓存区,保存已经创建好的对象
private static Map<String, Object> beanMap = new HashMap<>();
public static Object getBean(String beanName) {
// 双检锁保证beanMap中确实没有beanName对应的对象
if (!beanMap.containsKey(beanName)) {
synchronized (BeanFactory.class) {
if (!beanMap.containsKey(beanName)) {
// 过了双检锁,证明确实没有,可以执行反射创建
try {
Class<?> beanClazz = Class.forName(properties.getProperty(beanName));
Object bean = beanClazz.getDeclaredConstructor().newInstance();
// 反射创建后放入缓存再返回
beanMap.put(beanName, bean);
}catch (Exception e){
throw new RuntimeException("DemoDao instantiation error, cause: " + e.getMessage());
}
}
}
}
return beanMap.get(beanName);
}
}

2.1.6 IOC 思想的引入
java
private DemoDao dao = new DemoDaoImpl();
private DemoDao dao = (DemoDao) BeanFactory.getBean("demoDao");
本来开发者可以使用上面的方式,主动声明实现类,但如果选择下面的方式,那就不再是开发者主动声明,而是将获取对象的方式交给了BeanFactory。
这种将控制权交给别人的思想就是所谓的控制反转 (Inverse of Control,IOC) 。
而 BeanFactory 根据指定的 beanName 去获取和创建对象的过程,可以称作依赖查找 (Dependency Lookup,DL)。