前言

在上一篇文章中,我们初识了 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"/>
属性说明
idBean 的唯一标识,同一个容器中 不能重复
classBean 的全限定类名,必须写实现类,不能写接口(接口无法实例化)
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 完整生命周期流程
  1. 创建对象(内存分配)
  2. 执行构造方法
  3. 执行属性注入(setter 方法)
  4. 执行 bean 初始化方法(afterPropertiesSet 或 init-method)
  5. 使用 bean(执行业务操作)
  6. 关闭/销毁容器
  7. 执行 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 配置虽然繁琐,但理解了它,后面的注解开发将无比通透。如果觉得有帮助,欢迎点赞、收藏、关注,我们下节课见!

更多推荐