Java-Spring入门指南(三)深入剖析IoC容器与Bean核心机制
Java-Spring入门指南(三)深入剖析IoC容器与Bean核心机制
前言
- 在上一篇博客中,我们成功用 IDEA 搭建了第一个 Spring 系统:从配置 Maven 依赖、编写
beans.xml,到通过容器获取User对象——但操作完成后,你大概率会有这些疑问:- “Spring 帮我创建对象的背后,
IoC到底是个什么东西?它解决了什么本质问题?” - “
BeanFactory和ApplicationContext都是容器,到底有啥区别?为啥推荐用后者?” - “
scope="prototype"加不加,对象居然不一样?init-method和destroy-method又是怎么生效的?”
- “Spring 帮我创建对象的背后,
- 所以这一篇,我们不再只做“操作步骤”,而是从“问题本质”出发,把 IoC 思想、容器特性、Bean 核心机制这些“底层逻辑”讲透,让你不仅会用 Spring,更懂 Spring。
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的Java-Spring入门指南知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_13040333.html?spm=1001.2014.3001.5482

一、Spring IoC是什么?有什么用?核心思想是什么?
在讲 IoC 之前,我们先回到“没有 Spring”的场景——你会发现,IoC 不是凭空出现的,而是为了解决传统 Java 开发的许多问题。
1. 没有 Spring 的痛点
假设我们要写一个“用户使用手机”的功能,传统 Java 代码会这样写:

// 定义Phone接口
public interface Phone {
void call();
}
// 用户类
public class User {
// 声明Phone类型的属性,不自己创建对象
private Phone phone;
// 提供setter方法,让Spring注入Phone对象
public void setPhone(Phone phone) {
this.phone = phone;
}
public void usePhone() {
phone.call(); // 调用手机的打电话功能
}
}
// 3. 测试
public class Test {
public static void main(String[] args) {
User user = new User();
user.usePhone(); // 输出:用苹果手机打电话
}
}

看起来没问题,但如果需求变了——“把苹果手机换成华为手机”,你要改多少地方?
- 新建
HuaweiPhone类; - 改
User类里的new Phone()为new HuaweiPhone(); - 如果还有
User2、User3也用了Phone,所有用户类都要改!
核心问题:对象的创建和依赖“绑死”在代码里
——用户(
User)要自己创建依赖的对象(Phone),一旦依赖的对象变了,所有用到它的类都要修改,这就是“高耦合”。
2. Spring IoC 是什么?
IoC 的全称是 Inversion of Control(控制反转),直白理解就是:
原来由你(代码)控制“创建对象、管理依赖”,现在把这个“控制权”反转给 Spring 容器——让容器帮你创建对象、维护对象之间的依赖关系。
用上面的“用户用手机”例子,Spring 会怎么处理?
- 在
beans.xml里定义Phone和User的 Bean(告诉容器要创建什么对象):<!-- 定义华为手机对象 --> <bean id="huaweiPhone" class="org.example.test.HuaweiPhone"></bean> <!-- 定义用户对象,依赖手机对象 --> <bean id="user" class="org.example.test.User"> <!-- 告诉容器:给User的phone属性赋值,值是id为huaweiPhone的Bean --> <property name="phone" ref="huaweiPhone"/> </bean> User类不用自己new Phone了,只需声明Phone类型的属性,不自己创建对象
// 用户类
public class User {
// 声明Phone类型的属性,不自己创建对象
private Phone phone;
// 提供setter方法,让Spring注入Phone对象
public void setPhone(Phone phone) {
this.phone = phone;
}
public void usePhone() {
phone.call(); // 调用手机的打电话功能
}
}
- 从容器获取
User对象直接用:ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); User user = context.getBean("user", User.class); user.usePhone(); // 输出:用华为手机打电话
如果要换手机,只需要改 beans.xml 里的 ref 为 applePhone——User 类一行代码都不用动!这就是 IoC 的魔力。
3. Spring IoC 的核心作用
| 作用 | 传统开发(无Spring) | Spring IoC(有Spring) |
|---|---|---|
| 对象创建 | 手动 new,写死在代码里 |
容器创建,配置在 beans.xml 里 |
| 依赖管理 | 自己维护依赖(如 User 自己 new Phone) | 容器注入依赖(ref 配置) |
| 需求变更 | 改所有用到该对象的类(高耦合) | 只改配置文件(低耦合) |
| 对象复用 | 每次用都要 new(浪费资源) |
容器管理单例(默认),直接复用 |
4. Spring IoC 的核心思想
IoC 的思想可以用一句话概括:“不要找我,我会找你”
- 传统开发:你(
User)需要Phone,就自己去找(new Phone()); - Spring IoC:你(
User)只需要告诉容器“我需要Phone”(声明private Phone phone+ 提供setPhone),容器会主动把Phone送到你手里(注入)。
面试题:说一下 Spring 的核心思想?
答:Spring 的核心思想是 IoC(控制反转) 和 DI(依赖注入,IoC 的具体实现)。IoC 指将对象的创建、依赖管理控制权从代码反转给容器;DI 指容器在创建对象时,自动将其依赖的对象注入进来,两者共同实现了“解耦”,让代码更灵活、更易维护。
二、Spring 容器家族
上一篇我们用了 ClassPathXmlApplicationContext,但 Spring 还有个更基础的容器 BeanFactory——它们都是 IoC 容器,但定位完全不同。
1. 容器的核心职责是什么?
不管是 BeanFactory 还是 ApplicationContext,核心职责都是两件事:
- 加载配置文件(如
beans.xml); - 创建并管理 Bean(根据配置创建对象、注入依赖、销毁对象)。
2. BeanFactory
BeanFactory 是 Spring 容器的 顶层接口(最基础的容器),定义了容器的核心功能(如 getBean())。它的特点是:
- 延迟加载 Bean:只有调用
getBean()时,才会创建 Bean 对象(不调用就不创建); - 功能简单:只提供“创建 Bean”“获取 Bean”的基础功能,没有额外特性(如国际化、事件发布);
- 适合场景:资源有限的环境(如嵌入式设备、手机端),追求最小化开销。
代码示例(了解即可,很少用):
// 用 BeanFactory 加载配置文件(需要传入资源路径)
Resource resource = new ClassPathResource("beans.xml");
BeanFactory factory = new XmlBeanFactory(resource);
// 此时 Bean 还没创建,直到调用 getBean()
User user = factory.getBean("user", User.class); // 这一步才创建 User 对象
System.out.println(user);
3. ApplicationContext(开发首选)
ApplicationContext 是 BeanFactory 的 子接口,继承了 BeanFactory 的所有功能,还增加了很多实用特性。它的特点是:
- 预初始化 Bean:容器启动时(加载
beans.xml后),会主动创建所有 单例 Bean(默认scope="singleton") - 功能丰富:支持国际化、事件发布、加载多个配置文件等;
- 适合场景:企业级开发(Web 应用、后台系统),是开发中的“首选容器”。
常用实现类:
| 实现类 | 作用 | 上一篇示例代码 |
|---|---|---|
ClassPathXmlApplicationContext |
从类路径(src/main/resources)加载配置 |
new ClassPathXmlApplicationContext("beans.xml") |
FileSystemXmlApplicationContext |
从文件系统绝对路径加载配置 | new FileSystemXmlApplicationContext("E:\\xxx\\beans.xml") |
4. 两者核心区别
| 对比维度 | BeanFactory | ApplicationContext |
|---|---|---|
| 加载时机 | 延迟加载(getBean 时创建 Bean) | 预加载(容器启动时创建单例 Bean) |
| 功能范围 | 基础功能(getBean、containsBean) | 基础功能 + 增强功能(国际化、事件) |
| 启动速度 | 快(只加载配置,不创建 Bean) | 慢(加载配置 + 创建单例 Bean) |
| 内存占用 | 小(按需创建 Bean) | 大(预创建单例 Bean) |
| 开发推荐度 | 极低(仅了解) | 极高(企业级开发首选) |
为什么开发不用 BeanFactory?
答:预初始化虽然启动慢,但能在容器启动时就发现配置错误(如class路径写错、setter方法缺失)——如果用 BeanFactory,到调用getBean()时才报错,可能线上运行很久才发现问题,风险更高。
三、Spring Bean容器管理的对象到底是什么?
你可能会问:“Bean 不就是 Java 对象吗?为什么要叫 Bean?”——其实 Bean 是“被 Spring 容器管理的 Java 对象”,它的创建、生命周期、作用域都由容器控制,和普通 Java 对象不一样。
1. Bean 的定义
上一篇的 beans.xml 里,<bean> 标签就是 Bean 的“说明书”,告诉容器:“你要创建哪个类的对象,怎么配置它”。
<bean id="user" class="org.example.pojo.User" scope="singleton" init-method="init" destroy-method="destroy">
<property name="name" value="Bob"/>
<property name="age" value="18"/>
</bean>
| 核心属性 | 作用 | 注意点 |
|---|---|---|
id |
Bean 的唯一标识(容器中不能重复) | 只能有一个,如 id="user" |
class |
Bean 对应的 Java 类(全限定类名) | 必须有无参构造(容器默认用无参构造创建) |
name |
Bean 的别名(可多个,用逗号分隔) | 如 name="user1,user2",可和 id 共存 |
scope |
Bean 的作用域(单例/多例) | 默认 singleton(单例),可选 prototype(多例) |
init-method |
Bean 初始化后执行的方法 | 方法名自定义,如 init-method="init" |
destroy-method |
Bean 销毁前执行的方法 | 只对单例 Bean 生效(多例 Bean 容器不销毁) |
问题:当 id 和 name 都为 user 时,会报错吗?
比如你写了这样的配置:
<bean id="user" class="org.example.pojo.User"/>
<bean name="user" class="org.example.pojo.User"/>
会报错! 因为 id 和 name 都是 Bean 的标识,容器中不允许“标识重复”——不管是 id 还是 name,只要值一样,就会抛出“Bean 名称重复”的异常。
2. Bean 的创建方式
容器创建 Bean 最常用的方式是 “默认无参构造器”
问题:如果 User 只有有参构造,容器能创建 Bean 吗?
比如 User 类这样写:
public class User {
private String name;
private int age;
// 只有有参构造,没有无参构造
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
此时直接用 <bean id="user" class="org.example.pojo.User"/> 会报错——容器找不到无参构造。
解决方案:在 beans.xml 里用 <constructor-arg> 配置有参构造的参数:
<bean id="user" class="org.example.pojo.User">
<!-- 对应 User(String name, int age) 的参数 -->
<constructor-arg value="Bob" index="0"/> <!-- index=0 表示第一个参数 -->
<constructor-arg value="18" index="1"/> <!-- index=1 表示第二个参数 -->
</bean>
这样容器会调用有参构造 new User("Bob", 18) 创建对象。
3. Bean 的作用域
假如我们的 test1 方法里有这样的代码:
@Test
public void test1() {
FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("E:\\xxx\\beans.xml");
User user = context.getBean("user1", User.class);
User user1 = context.getBean("user1", User.class);
System.out.println(user == user1); // 输出 false
}
为什么 user == user1 是 false?因为 user1 的 scope="prototype"——这就要讲 Bean 的两种核心作用域:

(1)默认作用域:singleton(单例)
- 含义:容器中只创建一个 Bean 对象,所有
getBean()拿到的都是同一个对象; - 生命周期:容器启动时创建,容器关闭时销毁;
- 代码验证:
<bean id="user" class="org.example.pojo.User"/> <!-- 默认 scope="singleton" -->User user = context.getBean("user", User.class); User user2 = context.getBean("user", User.class); System.out.println(user == user2); // 输出 true(同一个对象)
(2)常用作用域:prototype(多例)
- 含义:每次调用
getBean()时,容器都会创建一个新的 Bean 对象; - 生命周期:容器只负责创建,不负责销毁(用完后由 JVM 垃圾回收);
- 代码验证:
<bean id="user1" class="org.example.pojo.User" scope="prototype"/>User user = context.getBean("user1", User.class); User user1 = context.getBean("user1", User.class); System.out.println(user == user1); // 输出 false(两个不同对象)
作用域对比(开发常用)
| 作用域 | 容器创建时机 | 容器销毁时机 | 适用场景 |
|---|---|---|---|
| singleton | 容器启动时 | 容器关闭时 | 无状态对象(如 Service、Dao) |
| prototype | 调用 getBean() 时 |
不主动销毁(JVM 回收) | 有状态对象(如 User、Order) |
4. Spring Bean生命周期
Bean的生命周期,简单说就是“Bean从创建到销毁的全过程”。但不同作用域(scope)的Bean,生命周期差异很大,我们重点讲最常用的单例(singleton) 和多例(prototype) 。
4.1 单例Bean(scope=“singleton”)
单例Bean是Spring默认的作用域,容器启动时创建,容器关闭时销毁,整个容器中只有一个实例。
(1)核心流程(5步)
- 实例化:容器启动 → 调用Bean的无参构造创建对象;
- 属性注入:通过setter方法给Bean的属性赋值;
- 初始化:调用配置中指定的
init-method方法; - 使用:通过
getBean()获取实例,执行业务逻辑; - 销毁:容器关闭 → 调用配置中指定的
destroy-method方法。
1.2 多例Bean(scope=“prototype”):容器“只创建不销毁”
多例Bean每次调用getBean()时才创建实例,且容器不管理销毁(用完后由JVM垃圾回收)。
(1)核心差异
- 实例化/注入/初始化:仅在
getBean()时执行(而非容器启动); - 销毁:容器不调用
destroy-method,JVM回收。
二、Bean配置继承:告别重复配置
Spring的“Bean继承”不是Java类的继承(如Student extends User),而是配置信息的复用——子Bean可以继承父Bean的属性、初始化方法等配置,还能覆盖父配置。
2.1 核心场景
比如多个Bean都需要设置age=18,如果每个Bean都写一遍<property name="age" value="18"/>,会很冗余。此时用“配置继承”就能复用这行配置。
2.2 实战:父Bean与子Bean配置
1. beans.xml(父Bean + 子Bean)
<!-- 父Bean:定义通用配置(可被继承) -->
<bean id="userParent" class="org.example.pojo.User">
<property name="age" value="18"/> <!-- 所有子Bean默认继承这个age -->
<property name="name" value="默认名称"/> <!-- 子Bean可覆盖 -->
</bean>
<!-- 子Bean1:继承父Bean,覆盖name属性 -->
<bean id="student"
class="org.example.pojo.User"
parent="userParent"> <!-- 指定父Bean的id -->
<property name="name" value="张三"/> <!-- 覆盖父Bean的name -->
</bean>
<!-- 子Bean2:继承父Bean,不覆盖任何属性 -->
<bean id="teacher"
class="org.example.pojo.User"
parent="userParent"/>
2. 测试方法
@Test
public void testBeanInheritance() {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("beans.xml");
User student = context.getBean("student", User.class);
User teacher = context.getBean("teacher", User.class);
System.out.println("学生Bean:" + student); // 继承age=18,覆盖name=张三
System.out.println("老师Bean:" + teacher); // 完全继承父配置
context.close();
}

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的Java-Spring入门指南知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_13040333.html?spm=1001.2014.3001.5482
| 非常感谢您的阅读,喜欢的话记得三连哦 |

更多推荐


所有评论(0)