Spring 第二天:深入 IoC 容器与 DI 依赖注入全解析
前言
在上一篇文章中,我们初识了 Spring Framework,理解了 IoC 与 DI 的核心思想,并手写了第一个入门案例。今天我们继续深入,把 IoC 容器中 Bean 的配置、实例化、生命周期 彻底搞懂,并全面掌握 DI 依赖注入 的各种姿势——setter 注入、构造器注入、自动装配、集合注入。
这是 Spring 地基中的钢筋水泥,掌握好这些,后面的 Spring Boot 才能用得游刃有余。
💬 课前唠一唠:上节课的 XML 配置方式你觉得繁琐吗?是不是很想问“有没有不用写 XML 的方法?”今天我们先扎实学完 XML 配置,理解了底层逻辑,后面学注解就会豁然开朗。
一、IoC 相关内容
1.1 bean 基础配置
1.1.1 id 与 class
在 XML 配置文件中,我们用 <bean> 标签把一个类交给 Spring 容器管理:
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl"/>
| 属性 | 说明 |
|---|---|
id | Bean 的唯一标识,同一个容器中 不能重复 |
class | Bean 的全限定类名,必须写实现类,不能写接口(接口无法实例化) |
1.1.2 name 别名配置
如果因为命名习惯不同,想给 Bean 起多个名字,可以用 name 属性:
<bean id="bookService" name="service service4 bookEbi" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
name可以指定多个别名,用逗号、分号、空格分隔皆可。- 获取 Bean 时,用
id或name中的任意一个值都可以:BookService bookService = (BookService) ctx.getBean("service4");
1.1.3 scope 作用范围(重点)
scope 属性控制 Bean 是单例还是非单例:
| 值 | 说明 |
|---|---|
singleton | 单例(默认) |
prototype | 非单例 |
验证单例:
BookDao bookDao1 = (BookDao) ctx.getBean("bookDao");
BookDao bookDao2 = (BookDao) ctx.getBean("bookDao");
System.out.println(bookDao1 == bookDao2); // true,地址值相同
配置非单例:
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl" scope="prototype"/>
🧠 思考:为什么默认是单例?
- 单例意味着容器中只有一个对象,避免了频繁创建与销毁,性能高。
- 但如果 Bean 有状态(有成员变量存储数据),多线程共享可能导致线程安全问题。无状态的 Bean(如 Service、Dao)适合单例,封装实例的域对象则不适合。
✍️ 小结一下:
<bean id="" class="" scope="" name=""/>,这几个属性要烂熟于心。
1.2 bean 实例化
对象到底是怎么被 Spring 创建出来的?底层用的是构造方法。Spring 提供了三种实例化方式:
1.2.1 构造方法实例化(最常用)
默认情况下,Spring 调用类的无参构造方法来创建对象:
public class BookDaoImpl implements BookDao {
public BookDaoImpl() {
System.out.println("book dao constructor is running ...");
}
public void save() {
System.out.println("book dao save ...");
}
}
配置文件:
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
测试代码:
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
验证结论:
- 如果把无参构造方法设为
private,Spring 依然能调用——它底层用的是反射。 - 如果只提供有参构造方法(不提供无参),运行会报错:
NoSuchMethodException: BookDaoImpl.<init>(),说明 Spring 默认找的是无参构造。
🔍 看异常有技巧:从下往上看,最底层的
Caused by通常是根本原因。
1.2.2 静态工厂实例化(了解)
适用于早期遗留系统,了解即可。
// 静态工厂类
public class OrderDaoFactory {
public static OrderDao getOrderDao() {
return new OrderDaoImpl();
}
}
<bean id="orderDao" class="com.itheima.factory.OrderDaoFactory" factory-method="getOrderDao"/>
1.2.3 实例工厂与 FactoryBean(实用)
实例工厂需要先创建工厂对象,再调用方法,配置较繁琐。Spring 提供了 FactoryBean 来简化:
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
// 创建对象
public UserDao getObject() throws Exception {
return new UserDaoImpl();
}
// 返回对象类型
public Class<?> getObjectType() {
return UserDao.class;
}
// 是否单例(默认 true,一般不需要重写)
public boolean isSingleton() {
return true;
}
}
<bean id="userDao" class="com.itheima.factory.UserDaoFactoryBean"/>
FactoryBean 三个方法:
| 方法 | 作用 |
|---|---|
getObject() | 返回创建的对象 |
getObjectType() | 返回对象的 Class |
isSingleton() | 是否单例,默认 true |
这种方式在 Spring 整合其他框架(如 MyBatis)时非常常用,需要理解掌握。
1.3 bean 的生命周期
生命周期:Bean 对象从创建到销毁的整个过程。
我们可以在 Bean 的初始化阶段和销毁前插入自定义逻辑。
1.3.1 方式一:配置文件指定 init-method / destroy-method
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
// 初始化方法(方法名任意)
public void init() {
System.out.println("init...");
}
// 销毁方法(方法名任意)
public void destroy() {
System.out.println("destroy...");
}
}
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"
init-method="init" destroy-method="destroy"/>
注意:直接运行 main 函数,destroy 方法不会执行,因为 JVM 退出前容器来不及销毁 Bean。
解决办法:
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
bookDao.save();
// 方式1:手动关闭容器
ctx.close();
// 方式2:注册钩子,JVM 退出前自动关闭容器
ctx.registerShutdownHook();
| 关闭方式 | 说明 |
|---|---|
ctx.close() | 调用时立即关闭 |
ctx.registerShutdownHook() | JVM 退出前回调关闭(推荐) |
1.3.2 方式二:实现 InitializingBean / DisposableBean 接口
public class BookServiceImpl implements BookService, InitializingBean, DisposableBean {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
System.out.println("set ....");
this.bookDao = bookDao;
}
// 属性设置后执行(初始化方法)
public void afterPropertiesSet() throws Exception {
System.out.println("service init");
}
// 销毁前执行
public void destroy() throws Exception {
System.out.println("service destroy");
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
🔍 执行顺序验证:
setBookDao在afterPropertiesSet之前执行,说明初始化方法在属性注入之后。
1.3.3 完整生命周期流程
- 创建对象(内存分配)
- 执行构造方法
- 执行属性注入(setter 方法)
- 执行 bean 初始化方法(
afterPropertiesSet或init-method) - 使用 bean(执行业务操作)
- 关闭/销毁容器
- 执行 bean 销毁方法(
destroy或destroy-method)
二、DI 相关内容
DI 注入方式主要分两种:setter 注入 和 构造器注入。注入的数据类型分:引用类型 和 简单类型。
2.1 setter 注入
2.1.1 注入引用类型(复习)
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
name:属性名,Spring 会找setBookDao()方法ref:引用容器中另一个 Bean 的id
2.1.2 注入简单类型
public class BookDaoImpl implements BookDao {
private String databaseName;
private int connectionNum;
// 省略 setter 方法...
}
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<property name="databaseName" value="mysql"/>
<property name="connectionNum" value="10"/>
</bean>
| 类型 | 注入属性 |
|---|---|
| 引用类型 | ref |
| 简单类型 | value |
⚠️
value="10"中 Spring 会自动转换类型,但不能写成value="abc"赋给int,会报错。
2.2 构造器注入
2.2.1 注入引用类型
public class BookServiceImpl implements BookService {
private BookDao bookDao;
// 构造方法
public BookServiceImpl(BookDao bookDao) {
this.bookDao = bookDao;
}
}
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<constructor-arg name="bookDao" ref="bookDao"/>
</bean>
2.2.2 注入简单类型
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<constructor-arg name="databaseName" value="mysql"/>
<constructor-arg name="connectionNum" value="666"/>
</bean>
2.2.3 构造器注入参数名耦合问题及解决
当构造方法参数名发生变化时,XML 中 name 也要改,存在耦合。两种备选方案:
方案一:按类型注入(type)
<constructor-arg type="int" value="10"/>
<constructor-arg type="java.lang.String" value="mysql"/>
- 缺点:如果有多个同类型参数,无法区分。
方案二:按索引注入(index)
<constructor-arg index="0" value="mysql"/>
<constructor-arg index="1" value="100"/>
- 缺点:参数顺序变了,又得改。
结论:一般情况下参数名很少变,正式开发用 name 即可。以上两种作为了解。
2.2.4 setter 注入 vs 构造器注入,如何选?
| 对比维度 | setter 注入 | 构造器注入 |
|---|---|---|
| 灵活性 | 高,可选依赖灵活注入 | 低,必须在构造时提供 |
| 强制性 | 可能因忘记注入导致 NullPointer | 强制依赖,保证对象完整 |
| Spring 推荐 | 自研模块推荐 setter | 第三方框架多用构造器 |
原则:强制依赖用构造器,可选依赖用 setter。两者可同时使用。
2.3 自动装配
问:配置写得太累,有没有更简单的方式?
答:有,自动装配!
什么是自动装配?
IoC 容器根据 Bean 所依赖的资源,在容器中自动查找并注入,无需手动写 <property> 或 <constructor-arg>。
2.3.1 按类型注入(byType,常用)
<bean class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl" autowire="byType"/>
注意事项:
- 属性必须有 setter 方法
- 被注入的对象必须被 Spring 容器管理
- 若容器中有多个同类型 Bean,会报
NoUniqueBeanDefinitionException
2.3.2 按名称注入(byName)
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl" autowire="byName"/>
按名称规则: Spring 会找与属性名同名的 id。
- 属性名是
bookDao→ 找id="bookDao"的 Bean - 原理:Spring 通过
setBookDao方法名推断出属性名为bookDao
⚠️ 若找不到同名 Bean,注入
null,不会报错。
2.3.3 自动装配小结
| 装配方式 | 使用频率 | 说明 |
|---|---|---|
| byType | 常用 | 容器中同类型 Bean 必须唯一 |
| byName | 少用 | 变量名与配置耦合,不推荐 |
- 自动装配仅适用于引用类型,不能注入简单类型
- 自动装配优先级低于 setter/构造器注入,同时出现时自动装配失效
🎯 动手挑战:把你上一篇文章中手动写的
<property>标签全部删掉,换成autowire="byType",重新运行一下原来的代码。成功了吗?如果出错,排查一下是不是同一个类型的 Bean 注册了多次?欢迎在评论区晒出你的改动。
2.4 集合注入
集合中既可以装简单数据类型,也可以装引用数据类型。Spring 支持注入以下集合类型:
环境准备
public class BookDaoImpl implements BookDao {
private int[] array;
private List<String> list;
private Set<String> set;
private Map<String, String> map;
private Properties properties;
// 省略 setter 和打印方法...
}
2.4.1 数组注入
<property name="array">
<array>
<value>100</value>
<value>200</value>
<value>300</value>
</array>
</property>
2.4.2 List 注入
<property name="list">
<list>
<value>itcast</value>
<value>itheima</value>
</list>
</property>
2.4.3 Set 注入
<property name="set">
<set>
<value>itcast</value>
<value>itheima</value>
</set>
</property>
2.4.4 Map 注入
<property name="map">
<map>
<entry key="country" value="china"/>
<entry key="province" value="henan"/>
</map>
</property>
2.4.5 Properties 注入
<property name="properties">
<props>
<prop key="country">china</prop>
<prop key="province">henan</prop>
</props>
</property>
📝 补充:
- 构造器注入中的
<constructor-arg>标签内也能嵌套<array>、<list>等<list>和<array>标签底层都是数组,可以混用- 如果集合中存的是引用类型,把
<value>换成<ref bean="..." />即可
三、总结
今天的内容量比较大,我们来一张图回顾核心知识点:
IoC 容器
├── Bean 基础配置
│ ├── id:唯一标识
│ ├── class:全限定类名
│ ├── name:别名(多个)
│ └── scope:singleton(默认) / prototype
├── Bean 实例化
│ ├── 构造方法(最常用,底层反射)
│ ├── 静态工厂(了解)
│ └── 实例工厂 → FactoryBean(实用,整合框架时用)
├── Bean 生命周期
│ ├── init-method / destroy-method(XML 配置)
│ └── InitializingBean / DisposableBean 接口
│ 流程:构造 → 属性注入 → 初始化 → 使用 → 销毁
│
DI 依赖注入
├── setter 注入
│ ├── 引用类型:<property name="" ref=""/>
│ └── 简单类型:<property name="" value=""/>
├── 构造器注入
│ ├── <constructor-arg name="" ref/value=""/>
│ └── 备选方案:type / index(了解)
├── 自动装配
│ ├── byType(常用,同类型唯一)
│ └── byName(少用,找同名 id)
└── 集合注入
├── <array>、<list>、<set>
├── <map>(<entry key="" value=""/>)
└── <props>(<prop key="">value</prop>)
🎤 结课小调查:今天的内容中,你觉得最不好理解的是哪一块?
A. Bean 生命周期流程
B. FactoryBean 实例化
C. 构造器注入的参数耦合问题
D. 集合注入的各种标签评论区打出你的选项,我看看大家普遍卡在哪里,后续可以出一篇专题解答!
本文为 Spring Framework 第二天授课内容整理。万丈高楼平地起,XML 配置虽然繁琐,但理解了它,后面的注解开发将无比通透。如果觉得有帮助,欢迎点赞、收藏、关注,我们下节课见!
更多推荐

所有评论(0)