手写简易版 IoC 容器:从 XML 配置到 BeanFactory 反射注入
TL;DR
- 场景:在转账案例基础上,业务代码仍需手动 new 对象并维护 Service → Dao → ConnectionUtils 的依赖链,对象稍多就开始混乱。
- 结论 :用一份
beans.xml+ 一个 70 行的BeanFactory(dom4j 读 XML + 反射 + setter 注入),就能把对象的创建和依赖组装从业务代码里抽出去,为后续事务管理和 AOP 铺路。 - 产出 :可运行的简易版 IoC 容器原型,包含
beans.xml、反射式BeanFactory、WzkServlet接入示例与转账验证结果。

版本矩阵
| 功能 | 状态 | 说明 |
|---|---|---|
| XML 解析(dom4j SAXReader) | ✅ 已验证 | dom4j 2.1.3,广泛用于 Java XML 处理,2024 年仍是主流选择 |
反射创建 Bean(Class.forName + newInstance) |
✅ 已验证 | JDK 原生反射,无第三方依赖 |
Setter 依赖注入(<property name ref>) |
✅ 已验证 | 按 setter 名匹配,容器反射调用 |
MySQL 驱动类 com.mysql.cj.jdbc.Driver |
✅ 已验证 | mysql-connector-java 8.0+ 推荐使用,旧类 com.mysql.jdbc.Driver 已 deprecated |
| Druid 数据库连接池 | ✅ 已验证 | 阿里巴巴开源,常用版本 1.2.x,输出 {dataSource-1} inited 即视为初始化成功 |
| Spring BeanFactory 设计思想 | ✅ 已验证 | BeanFactory 是 Spring 基础 IoC 容器,ApplicationContext 在其之上扩展 |
| Bean 不存在 / class 写错 / setter 缺失时的友好提示 | ⚠️ 待验证 | 当前仅 try/catch 打印堆栈,未做用户级错误处理 |
| 单例 / 多例(scope)区分 | ⚠️ 待验证 | 当前为静态 Map 单例模式,未实现 prototype scope |
| 循环依赖处理 | ⚠️ 待验证 | 当前两步式初始化可避免构造注入循环,setter 循环依赖未处理 |
上节进度
上节我们完成了基础转账案例的代码编写,但对象之间的创建和依赖关系仍然需要手动维护。
本节继续在这个案例基础上实现一个简易版 IoC 容器,把对象的创建和依赖组装交给容器处理,为后面继续实现事务管理和 AOP 做铺垫。
IoC 实现
当前代码中,如果需要使用某个对象,通常要手动 new 出来,并且还要手动给它设置依赖对象。
例如:
- Service 依赖 Dao
- Dao 依赖 ConnectionUtils
- ConnectionUtils 负责维护数据库连接
对象少的时候还能接受,但项目稍微复杂一点,手动创建和组装对象就会变得非常混乱。
所以我们需要一个简单的 Bean 管理容器。当程序启动时,容器根据配置文件创建对象,并维护对象之间的依赖关系。后续业务代码只需要从容器中获取对象即可。
本节使用 XML 文件描述需要创建哪些 Bean,以及 Bean 之间的依赖关系。
IoC(Inversion of Control,控制反转)简介
IoC,全称 Inversion of Control,中文通常叫控制反转。
它的核心思想是:对象的创建权和依赖管理权不再由业务代码自己控制,而是交给容器统一管理。
没有 IoC 时,代码通常是这样:
java
WzkConnectionUtils connectionUtils = new WzkConnectionUtils();
JdbcWzkAccountDaoImpl accountDao = new JdbcWzkAccountDaoImpl();
accountDao.setWzkConnectionUtils(connectionUtils);
WzkTransferServiceImpl transferService = new WzkTransferServiceImpl();
transferService.setWzkAccountDao(accountDao);
引入 IoC 后,对象由容器提前创建并组装好,业务代码只需要获取:
java
WzkTransferService transferService = (WzkTransferService) BeanFactory.getBean("wzkTransferService");
这样就把对象创建和依赖组装的逻辑从业务代码中抽离出来了。
IoC 的特点
解耦
IoC 将对象创建和依赖管理交给容器处理,业务类不需要关心依赖对象如何创建,从而降低模块之间的耦合。
动态依赖管理
容器可以根据配置文件或注解完成依赖注入,避免在业务代码中写死具体实现类。
灵活性
如果后续需要替换某个实现类,只需要调整配置,不需要大范围修改业务代码。
IoC 的应用场景
IoC 在很多框架中都有应用:
- Spring Framework:Spring 使用 IoC 容器管理 Bean 的生命周期和依赖关系。
- Guice、Dagger:常见的轻量级依赖注入框架。
- Angular:前端框架 Angular 中也有依赖注入机制,用于管理组件和服务之间的依赖。
在 Java 后端开发中,最典型的 IoC 实现就是 Spring 容器。
IoC 的优势
- 增强模块化:减少类与类之间的直接依赖。
- 提高可测试性:测试时可以替换依赖对象。
- 增强灵活性:通过配置切换实现类。
- 便于维护:对象创建和依赖管理集中到容器中处理。
IoC 的限制
- 初学者需要理解 IoC、DI、Bean 容器等概念。
- 容器初始化和反射调用会带来一定运行时开销。
- 如果项目中依赖关系过多,配置也可能变得复杂。
不过在实际项目中,IoC 带来的解耦收益通常远大于这点复杂度。
Resources
Resources 目录下新增 beans.xml 文件,用来描述需要交给容器管理的 Bean。
beans.xml
我们先编写一个 XML 文件保存 Bean 信息:
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!-- BeanFactory 类会处理这里的内容 -->
<beans>
<!-- WzkConnectionUtils 交给容器管理 -->
<bean id="wzkConnectionUtils" class="wzk.utils.WzkConnectionUtils"></bean>
<!-- JdbcWzkAccountDaoImpl 依赖 WzkConnectionUtils -->
<bean id="wzkAccountDao" class="wzk.dao.impl.JdbcWzkAccountDaoImpl">
<!-- name 表示需要调用的 setter 后缀,ref 表示引用容器中的哪个 Bean -->
<property name="WzkConnectionUtils" ref="wzkConnectionUtils"/>
</bean>
<!-- WzkTransferServiceImpl 依赖 WzkAccountDao -->
<bean id="wzkTransferService" class="wzk.service.impl.WzkTransferServiceImpl">
<!-- name 表示需要调用的 setter 后缀,ref 表示引用容器中的哪个 Bean -->
<property name="WzkAccountDao" ref="wzkAccountDao"></property>
</bean>
</beans>
这份配置主要做了三件事:
- 创建
wzkConnectionUtils - 创建
wzkAccountDao,并注入wzkConnectionUtils - 创建
wzkTransferService,并注入wzkAccountDao
对应的内容截图如下所示:

图片说明:IntelliJ IDEA 中
beans.xml的编辑界面,展示wzkConnectionUtils、wzkAccountDao、wzkTransferService三个 Bean 定义及其 Setter 注入关系,带中文注释,右下角 CSDN @武子康 水印。
注意:需要将 beans.xml 放到 resources 目录下,否则类加载器无法读取到该配置文件。

图片说明:IntelliJ IDEA 左侧项目结构,展示
src/main/resources/beans.xml与wzk/utils、webapp/WEB-INF/web.xml等同级目录的关系,说明配置文件必须放在 resources 目录才能被 ClassLoader 加载。
Proxy
BeanFactory
下面开始实现一个简易版 BeanFactory。
核心流程分为两步:
- 读取 XML 中的
bean标签,通过反射创建对象,并放入容器。 - 读取 XML 中的
property标签,通过 setter 方法完成依赖注入。
java
public class BeanFactory {
private BeanFactory() {}
/**
* 全局容器,所有创建好的对象都存储在这里
*/
private static Map<String, Object> map = new HashMap<>();
static {
// 读取 resources 目录下的 beans.xml
// 这里的 XML 解析方式,可以参考之前手写 MyBatis 的部分
InputStream resourceAsSteam = BeanFactory.class.getClassLoader().getResourceAsStream("beans.xml");
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(resourceAsSteam);
Element rootElement = document.getRootElement();
// 第一步:创建所有 Bean 对象,并放入容器
List<Element> beanList = rootElement.selectNodes("//bean");
for (Element element : beanList) {
String id = element.attributeValue("id");
String clazz = element.attributeValue("class");
// 通过反射创建对象
Class<?> aClass = Class.forName(clazz);
Object object = aClass.newInstance();
// 保存到容器中
map.put(id, object);
}
// 第二步:处理 Bean 之间的依赖关系
List<Element> propertyList = rootElement.selectNodes("//property");
for (Element element : propertyList) {
String name = element.attributeValue("name");
String ref = element.attributeValue("ref");
// 获取 property 所属的 bean 节点
Element parentElement = element.getParent();
String parentId = parentElement.attributeValue("id");
// 根据父级 bean 的 id,从容器中取出当前对象
Object parentObject = map.get(parentId);
// 找到对应的 setter 方法,完成依赖注入
Method[] methods = parentObject.getClass().getMethods();
for (Method method : methods) {
if (method.getName().equalsIgnoreCase("set" + name)) {
method.invoke(parentObject, map.get(ref));
}
}
// 更新容器中的对象
map.put(parentId, parentObject);
}
System.out.println("BeanFactory 初始化完毕 map:" + map);
} catch (Exception e) {
System.out.println("BeanFactory 初始化失败");
e.printStackTrace();
}
}
public static Object getBean(String id) {
return map.get(id);
}
}
这段代码只是一个简化版 IoC 容器,还没有处理很多边界情况,例如:
- Bean 不存在时如何提示
- class 路径写错时如何处理
- setter 方法不存在时如何处理
- 单例和多例如何区分
- 循环依赖如何解决
本节先关注主流程:读取配置、创建对象、注入依赖、从容器获取 Bean。
对应的截图如下所示:

图片说明:IntelliJ IDEA 中
BeanFactory类的完整源码,展示 dom4j 解析 XML、反射创建对象、Set 注入依赖的两步式初始化流程。
Controller
WzkServlet
接下来修改 WzkServlet。
原来我们需要在 Servlet 中手动创建 Dao、Service,并手动维护它们之间的依赖关系。现在有了 BeanFactory,就可以直接从容器中获取 wzkTransferService。
java
@WebServlet(name="wzkServlet", urlPatterns = "/wzkServlet")
public class WzkServlet extends HttpServlet {
// ========================== 2 ==========================
// 由 BeanFactory 创建并管理
private WzkTransferService wzkTransferService = (WzkTransferService) BeanFactory.getBean("wzkTransferService");
// =======================================================
@Override
protected void doGet(javax.servlet.http.HttpServletRequest req, javax.servlet.http.HttpServletResponse resp) throws javax.servlet.ServletException, IOException {
System.out.println("=== WzkServlet doGet ===");
// ======================= 1 =============================
// 没有 IoC 容器之前,需要手动创建对象并维护依赖关系
//
// JdbcWzkAccountDaoImpl jdbcWzkAccountDaoImpl = new JdbcWzkAccountDaoImpl();
// jdbcWzkAccountDaoImpl.setWzkConnectionUtils(new WzkConnectionUtils());
//
// WzkTransferServiceImpl wzkTransferService = new WzkTransferServiceImpl();
// wzkTransferService.setWzkAccountDao(jdbcWzkAccountDaoImpl);
// ======================================================
// 执行业务逻辑
try {
wzkTransferService.transfer("1", "2", 100);
} catch (Exception e) {
e.printStackTrace();
System.out.println("=== transfer error ====");
}
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().print("=== WzkServlet doGet ===");
}
}
此时 Servlet 不再负责对象创建和依赖组装,只负责调用业务方法。
这就是 IoC 带来的直接变化:业务代码更干净,对象关系交给容器处理。
WzkServlet(2)
还有一种写法:在 Servlet 初始化时再从容器中获取 Bean。
java
// ========================== 3 ==========================
// 另一种写法:在 Servlet 初始化阶段获取 Bean
private WzkTransferService wzkTransferService;
@Override
public void init() throws ServletException {
super.init();
this.wzkTransferService = (WzkTransferService) BeanFactory.getBean("wzkTransferService");
}
// ======================================================
这种写法和前面的思路一致,都是从 BeanFactory 中获取已经创建好的对象。区别只是获取时机不同。
测试运行
运行代码后,可以看到控制台输出如下:
shell
BeanFactory 初始化完毕 map:{wzkTransferService=wzk.service.impl.WzkTransferServiceImpl@5dfdd67c, wzkAccountDao=wzk.dao.impl.JdbcWzkAccountDaoImpl@380d9fc0, wzkConnectionUtils=wzk.utils.WzkConnectionUtils@2d150354}
=== WzkServlet doGet ===
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
11月 18, 2024 6:24:24 下午 com.alibaba.druid.pool.DruidDataSource info
信息: {dataSource-1} inited
transfer fromResult: 1 toResult: 1
从日志可以看到:
BeanFactory已经完成初始化。wzkTransferService、wzkAccountDao、wzkConnectionUtils都已经放入容器。- 转账方法正常执行。
- 数据库更新结果为
fromResult: 1 toResult: 1。
其中 MySQL 驱动的提示是因为旧驱动类 com.mysql.jdbc.Driver 已经过时,新版本推荐使用 com.mysql.cj.jdbc.Driver。这不影响本节 IoC 的主流程,但后续可以顺手调整。
对应的控制台结果如下所示:

图片说明:IntelliJ IDEA Run 控制台输出,展示
BeanFactory 初始化完毕日志、=== WzkServlet doGet ===进入标记、MySQL 驱动弃用警告、Druid{dataSource-1} inited连接池初始化日志,以及核心业务结果transfer fromResult: 1 toResult: 1,底部面包屑定位到wzk-spring-ioc-aop项目的JdbcWzkAccountDaoImpl#updateW...。
查看结果
数据库中的账户金额已经发生变化,说明转账逻辑成功执行。

图片说明:数据库管理工具(Navicat/DBeaver)中的 account 表,包含 card / name / money 三列;第一行
1 / wzk / 300,第二行2 / icu / 700,展示 wzk 向 icu 转账 200 后的账户金额结果(总金额 1000 守恒)。
当然,也可以通过断点调试进一步确认对象之间的依赖是否已经注入成功。
重点可以观察:
BeanFactory是否读取到了beans.xmlmap中是否保存了三个 BeanwzkAccountDao中是否注入了wzkConnectionUtilswzkTransferService中是否注入了wzkAccountDao
目前问题
到这里,我们已经完成了一个简易版 IoC 容器。
它解决了对象创建和依赖管理的问题,让 Servlet 不再需要手动组装 Service、Dao 和工具类。
但是当前转账业务还有一个明显问题:事务还没有统一管理。
转账操作至少包含两次数据库更新:
- 转出账户扣钱
- 转入账户加钱
如果第一步成功,第二步失败,就会出现数据不一致。
所以接下来需要继续实现一个事务管理器。事务管理器会使用 ThreadLocal 保存当前线程的数据库连接,保证同一次请求中的多个数据库操作使用同一个连接,从而实现统一提交和统一回滚。
错误速查卡
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
BeanFactory 初始化失败 + ClassNotFoundException |
beans.xml 中 <bean class="..."> 写错了全限定类名,或类没编译到 classpath |
看堆栈中的类名,核对 IDE 编译产物(target/classes) |
修正 class 属性,或执行 mvn clean compile 重新编译 |
NullPointerException 出现在 method.invoke(parentObject, map.get(ref)) |
ref 指向的 Bean id 与 //bean 中 id 不一致;或 XML 中 <property> 拼写错 |
打印 map.keySet() 确认容器里的 id,核对 <property ref> 值 |
保持 id 引用一致,或调整命名 |
| setter 没被调用,依赖对象为 null | name 与 setter 后缀不匹配(忽略大小写时大小写错、或属性名拼错) |
在 methods 循环里打印候选方法名 |
修正 name 值为 setter 后缀(如 WzkAccountDao 对应 setWzkAccountDao) |
FileNotFoundException: beans.xml(或 getResourceAsStream 返回 null) |
beans.xml 没放到 src/main/resources 下,Maven 不会把其他目录打到 classpath |
看 target/classes 里是否存在 beans.xml |
把文件移到 src/main/resources 后重新构建 |
控制台报警告 Illegal reflective access to method ... SAXContentHandler |
JDK 9+ 对 dom4j 这类库的非法反射访问告警 | 看启动日志首行 WARNING: An illegal reflective access operation has occurred |
不影响运行,可忽略;长期方案:升级到 dom4j 2.1.3+ 或换 JDK 8 |
启动告警 Loading class 'com.mysql.jdbc.Driver'. This is deprecated |
mysql-connector-java 8.0+ 已弃用旧驱动类 | 看 Druid 之前的 driver 加载日志 | 把驱动类改为 com.mysql.cj.jdbc.Driver,并检查连接 URL 的时区参数 |
转账只成功一半(fromResult: 1, toResult: 0)或中途异常 |
转账没有事务,两次 UPDATE 互相独立 | 关闭自动提交后看是否两边一起回滚 | 下节引入 TransactionManager + ThreadLocal 统一管理连接和事务边界 |
| 循环依赖导致启动栈溢出或 NPE | 两个 Bean 互相 setter 注入(本实现两步式初始化可避开构造循环,但 setter 循环仍会出问题) | 画 Bean 依赖图,找闭环 | 通过 @Lazy、三级缓存或重构依赖方向解决(Spring 的标准方案) |
作者:武子康的个人博客