前言

  • 在上一篇博客中,我们成功用 IDEA 搭建了第一个 Spring 系统:从配置 Maven 依赖、编写 beans.xml,到通过容器获取 User 对象——但操作完成后,你大概率会有这些疑问:
    • “Spring 帮我创建对象的背后,IoC 到底是个什么东西?它解决了什么本质问题?”
    • “BeanFactory 和 ApplicationContext 都是容器,到底有啥区别?为啥推荐用后者?”
    • “scope="prototype" 加不加,对象居然不一样?init-method 和 destroy-method 又是怎么生效的?”
  • 所以这一篇,我们不再只做“操作步骤”,而是从“问题本质”出发,把 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(); // 输出:用苹果手机打电话
    }
}

在这里插入图片描述

看起来没问题,但如果需求变了——“把苹果手机换成华为手机”,你要改多少地方?

  1. 新建 HuaweiPhone 类;
  2. 改 User 类里的 new Phone() 为 new HuaweiPhone();
  3. 如果还有 User2、User3 也用了 Phone,所有用户类都要改!

核心问题:对象的创建和依赖“绑死”在代码里

——用户(User)要自己创建依赖的对象(Phone),一旦依赖的对象变了,所有用到它的类都要修改,这就是“高耦合”。

2. Spring IoC 是什么?

IoC 的全称是 Inversion of Control(控制反转),直白理解就是:

原来由你(代码)控制“创建对象、管理依赖”,现在把这个“控制权”反转给 Spring 容器——让容器帮你创建对象、维护对象之间的依赖关系。

用上面的“用户用手机”例子,Spring 会怎么处理?

  1. 在 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>
     
    
  2. 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(); // 调用手机的打电话功能
    }
}

  1. 从容器获取 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,核心职责都是两件事:

  1. 加载配置文件(如 beans.xml);
  2. 创建并管理 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. 两者核心区别

对比维度BeanFactoryApplicationContext
加载时机延迟加载(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>
核心属性作用注意点
idBean 的唯一标识(容器中不能重复)只能有一个,如 id="user"
classBean 对应的 Java 类(全限定类名)必须有无参构造(容器默认用无参构造创建)
nameBean 的别名(可多个,用逗号分隔)如 name="user1,user2",可和 id 共存
scopeBean 的作用域(单例/多例)默认 singleton(单例),可选 prototype(多例)
init-methodBean 初始化后执行的方法方法名自定义,如 init-method="init"
destroy-methodBean 销毁前执行的方法只对单例 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步)
  1. 实例化:容器启动 → 调用Bean的无参构造创建对象;
  2. 属性注入:通过setter方法给Bean的属性赋值;
  3. 初始化:调用配置中指定的init-method方法;
  4. 使用:通过getBean()获取实例,执行业务逻辑;
  5. 销毁:容器关闭 → 调用配置中指定的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

非常感谢您的阅读,喜欢的话记得三连哦

在这里插入图片描述

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐