☰
Spring入门必懂:IoC容器、依赖注入与Bean生命周期全解析
2026/10/14 6:22:02 网站建设 项目流程

很多同学学 Spring 的时候,上来就背概念,控制反转、依赖注入、Bean 容器这些术语倒是记了不少,但一动手写配置就懵,报个错也不知道去哪查。我自己最开始也是这个状态,啃了几天官方文档,越看越觉得虚。后来换个思路,把 Spring 当成一个帮你管理对象的"工具人",从 IoC 容器入手,跟着写几个最小示例,反而很快就把框架的核心脉络理清了。这篇博客就结合我的学习路径,把 Spring(1) 阶段最该掌握的东西拆开揉碎讲一遍,重点是 IoC 和 DI 的底层逻辑、XML 配置的实操细节、Bean 的生命周期,最后再整理一些新手必踩的坑和排查思路。内容会比较干,适合刚接触 Spring 的 Java 学习者,也适合那些背了概念但没动手写过配置的人拿来对照自查。

1. 为什么框架那么多,偏偏要先学 Spring

1.1 Spring 到底干了什么活

先别管那些花哨的定义,Spring 的核心就一句话:它帮你把对象创建出来,管好它们之间的关系,然后在你需要的时候把对象给你。放到传统代码里,你写一个new UserService(),这个对象的创建和销毁都是自己控制的;在 Spring 里,你只需要告诉容器"我要一个 UserService",剩下的创建、赋值、组装都是容器的事。这个机制就是 IoC(控制反转),控制权从程序员手里反转到了容器手里。

很多人在这一步就卡住了,觉得控制反转反直觉。我举个例子你就明白了:你去餐厅吃饭,传统方式是你在家自己买菜、洗菜、做饭、洗碗,全程自己控制流程;而 IoC 相当于你直接坐下点菜,厨师(容器)把菜做好端上来,你只负责吃。对象就是那道菜,你只声明需求,厨房怎么配菜、怎么做你不用管。

除了管对象,Spring 还能帮我们管理对象之间的依赖。打个比方,UserService 里面要调 UserDao,传统写法是在 UserService 里new UserDao(),这样两个类就硬绑在一块了。Spring 的做法是:UserService 声明"我需要一个 UserDao",容器在创建 UserService 的时候,把一个已经准备好的 UserDao 实例"注入"进去。这就是依赖注入(DI, Dependency Injection),也是 Spring 框架最核心的编程思想。

1.2 Spring(1)阶段的学习边界

Spring 全家桶很大——Spring Boot、Spring Cloud、Spring Security、Spring Data 这些都是构建在 Spring Framework 之上的。但第一阶段学习,不需要一上来就碰这些。你就盯住 Spring Framework 里最基础、也最关键的三块内容:

  • IoC 容器:理解 Bean 是什么、容器怎么创建和管理 Bean、ApplicationContext 和 BeanFactory 有什么区别。
  • 依赖注入:掌握 XML 配置和注解两种方式怎么写,尤其是 setter 注入和构造器注入的使用场景。
  • Bean 的生命周期与作用域:搞清楚一个 Bean 从创建到销毁经历了哪些阶段,singleton 和 prototype 有什么区别,这在后面排查问题时会用到无数次。

这三块学扎实了,后面的 Spring Boot 其实就是帮你做了大量自动配置和约定优化,你会觉得上手特别快,因为底层思路完全一致。如果第一遍就囫囵吞枣地往下追,后面遇到问题往往连排查的入口都找不到。

2. IoC 容器的核心机制与设计思路

2.1 Bean 和容器,到底谁是主角

在 Spring 的世界里,所有交给容器管理的对象统称为Bean。容器可以理解为一张巨大的注册表,里面登记了每个 Bean 的"生产说明书"——这个 Bean 是什么类、构造参数有哪些、需要什么属性、生命周期是单例还是多例。你向容器要某个 Bean 的时候,容器会按照说明书现场创建(或者从缓存里直接取)一个实例给你。

初学者最容易混淆的是 Bean 和"new 出来的对象"之间的区别。用new创建的对象,生命周期完全由你控制,创建完引用就散了,没人管后续的事情。而 Spring 管理的 Bean,它的创建时机、初始化过程、销毁动作都在容器的掌控之中,你自己不需要写一行new,甚至某些场景下连类型都不需要写,只需要声明"我要这个东西"。这就是控制反转最直接的体现——你从创建者变成了需求方。

2.2 BeanFactory 和 ApplicationContext 怎么选

Spring 里有两个核心接口,初学者经常分不清:BeanFactory和ApplicationContext。一句话总结:BeanFactory 是最小化的容器,ApplicationContext 是增强版的容器。ApplicationContext 在 BeanFactory 的基础上扩展了国际化支持、事件发布、资源加载等功能,而且在启动阶段就会预加载所有单例 Bean。

在实际项目中,你基本不会直接使用 BeanFactory,都是基于 ApplicationContext 编程。最常见的是ClassPathXmlApplicationContext,它从 classpath 路径加载 XML 配置文件。下面是最基础的入门代码:

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); UserService userService = context.getBean("userService", UserService.class); userService.doSomething();

这里有两个细节我要着重提醒一下。第一,getBean时可以只传 Bean 名称,也可以传入类型;在容器里存在多个同类型 Bean 时,只传类型会直接抛异常,这时候必须通过名称进一步指定。第二,ApplicationContext 初始化完成后,单例 Bean 其实已经创建好了(默认是启动时预加载),所以你不必等到getBean的时候才触发创建。

2.3 依赖注入的三种姿势,哪个更好用

依赖注入本质上就是把外部依赖通过某种方式送进对象内部。Spring 支持三种方式:构造器注入、setter 注入、字段注入(注解方式)。

构造器注入是官方最推荐的方式,因为它能保证对象在创建时依赖就是完整的、不可变的。比如下面的写法,UserService 一旦创建出来,userDao 就一定是不为空的:

public class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao = userDao; } }

setter 注入适合那些可选依赖的场景,也就是说这个属性有没有都行,有就用,没有也能跑。但 setter 注入的缺点也很明显:依赖可能会在运行期间被重新设置,导致对象状态不稳定,迭代过程中容易出现"依赖被悄悄换掉"这种问题。

字段注入(@Autowired写在字段上)写起来最简洁,也是很多代码仓库里最泛滥的写法。方便是真方便,但它有两个比较大的隐患:一是依赖和类之间形成了隐蔽的耦合,单元测试时想替换依赖很麻烦;二是对象在创建时依赖可能还没准备好,容易到运行期才暴露问题。我的建议是:如果是新写的代码,优先用构造器注入;setter 注入只在可选依赖时使用;字段注入尽量少用,尤其在写公共组件和框架代码时。

下面我整理了三种方式的对比,方便你直观理解:

注入方式优点缺点推荐场景
构造器注入依赖完整、不可变、方便测试参数多时代码比较冗长必需依赖
setter 注入灵活、可重新赋值依赖可能为空、状态不稳定可选依赖
字段注入代码简洁耦合隐蔽、不利于测试、依赖初始化时机不明确尽量避免

3. 基于 XML 的配置实操详解

3.1 环境搭建:从零创建一个 Spring 工程

我先说明一下,这里演示的是传统的 XML 配置方式。虽然在 Spring Boot 里大部分场景直接用注解和自动配置,但 XML 配置能让你看到 Spring 容器最原始的运作方式,对理解框架本质非常有帮助。

第一步,准备一个普通的 Java 工程(Maven 项目就行),在pom.xml里加上 spring-context 依赖:

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.30</version> </dependency>

版本号建议用 5.3.x 系列,稳定且广泛使用;如果你后续要升级 Spring Boot,也可以直接选用 Spring Boot 对应的 Spring Framework 版本。加完依赖之后,Maven 会把这一个 jar 连带它依赖的 spring-core、spring-beans、spring-expression 等一起拉下来,这就是配置阶段最容易出问题的一个环节——网络不畅或仓库地址不对会导致依赖下载失败,IDE 里一堆编译错误。

第二步,在src/main/resources目录下创建applicationContext.xml,这是配置文件的大本营。文件根节点是<beans>,里面可以声明多个<bean>:

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans>

第三步,写一个最简单的接口和实现类,例如UserDao接口和UserDaoImpl实现类,然后在 XML 里注册:

<bean id="userDao" class="com.example.dao.impl.UserDaoImpl"/>

到这里,一个最基础的 Spring 工程就搭好了。你可以在main方法里用ClassPathXmlApplicationContext加载配置文件,然后调用getBean("userDao")验证是否成功创建对象。我第一次跑通的时候还挺有成就感的,因为终于看到了"容器帮我创建对象"这个过程。

3.2 Bean 标签里的常用属性

<bean>标签是 XML 配置的核心,它有好几个属性,每个属性的含义和使用场景都不一样,我用一个例子带过:

<bean id="userService" class="com.example.service.impl.UserServiceImpl" scope="singleton" init-method="init" destroy-method="destroy" lazy-init="false" primary="true"> </bean>

逐个解释一下:

  • id:Bean 的唯一标识,通过getBean("id")获取时要用。如果没有配 id,Spring 默认会用类的全限定名作为标识。
  • class:Bean 对应的全限定类名,这是必须的配置项。
  • scope:Bean 的作用域,常见值包括singleton(默认)、prototype、request、session,后面会重点讲解。
  • init-method / destroy-method:指定初始化和销毁时调用的方法,比如连接池创建后的初始化连接资源,关闭前的释放资源。
  • lazy-init:是否延迟初始化。单例 Bean 默认容器启动时就创建,配置为true后,改为第一次获取时才创建。
  • primary:当容器中存在多个同类型 Bean 时,标注为primary的那个会被优先注入。

这里最容易踩的坑是id 和 name 混用。id 命名必须唯一,且 XML 规范要求以字母开头;name 理论上可以包含特殊字符,也可以有多个别名。新手建议只用 id,不要同时写 id 和 name,否则后面排查时容易混乱。

3.3 依赖注入在 XML 中的两种写法

在 XML 时代,依赖注入主要通过<constructor-arg>和<property>两个子标签完成。前者对应构造器注入,后者对应 setter 注入。

构造器注入写法:

<bean id="userDao" class="com.example.dao.impl.UserDaoImpl"/> <bean id="userService" class="com.example.service.impl.UserServiceImpl"> <constructor-arg ref="userDao"/> </bean>

ref表示引用另一个 Bean,注意不要和<value>混淆,<value>是给普通字符串类型的属性赋值的。如果你的构造器有多个参数,可以通过index(从 0 开始的参数位置)、name(参数名)、type(参数类型)来精确指定:

<constructor-arg index="0" value="admin"/> <constructor-arg index="1" ref="userDao"/>

setter 注入写法:

<bean id="userService" class="com.example.service.impl.UserServiceImpl"> <property name="userDao" ref="userDao"/> <property name="userName" value="张三"/> </bean>

name对应的是类里面的属性名,Spring 会调用对应的 setter 方法来赋值。比如name="userName"就会去调用setUserName()方法。如果类里没有这个 setter 方法,报错是很常见的,这一点新手要注意——不是 XML 里写了<property>就万事大吉,前提是类里面确实有对应的 setter。

3.4 一个完整的配置示例串联流程

写一个稍微完整点的场景:用户注册功能,包括 Controller 调用 Service,Service 依赖 Dao,Dao 里面配置了一个数据源参数。我用 XML 把整个链条串起来:

<bean id="dataSource" class="com.example.datasource.DruidDataSource"> <property name="url" value="jdbc:mysql://localhost:3306/test"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="userDao" class="com.example.dao.impl.UserDaoImpl"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="userService" class="com.example.service.impl.UserServiceImpl"> <constructor-arg ref="userDao"/> </bean>

这个示例里用了两种注入方式:DataSource 的配置用的是 setter 注入,因为 DruidDataSource 的连接池参数本身就是可选的、可以后期调整的;UserDao 注入 UserService 用的是构造器注入,因为一个 Service 没有 Dao 是没有意义的,属于强制依赖。"强制依赖用构造器,可选配置用 setter"这个原则,你在后面自己写代码时可以直接套用。

4. Bean 的生命周期与作用域

4.1 一次完整的 Bean 生命周期

Bean 生命周期这个概念,刚开始学的时候觉得抽象,等你真正排查问题、做框架整合的时候,就会发现它的重要性。一个 Bean 从诞生到销毁,大致经历这么几个阶段:

  1. 实例化(Instantiation):Spring 通过构造器(默认无参构造器)创建 Bean 实例。没有无参构造器且 XML 中配置了构造参数,则走有参构造。
  2. 属性填充(Populate Properties):根据 XML 的<property>或注解配置,把依赖属性和配置值填充进 Bean 实例。
  3. 初始化前阶段:Spring 会调用BeanPostProcessor的postProcessBeforeInitialization方法,这是很多框架做功能增强的入口(AOP 代理就是在这里介入的)。
  4. 初始化(Initialization):执行初始化逻辑。如果 Bean 实现了InitializingBean接口,会调用afterPropertiesSet()方法;如果配置了init-method,会调用对应方法。
  5. 初始化后阶段:调用BeanPostProcessor的postProcessAfterInitialization方法,此时 Bean 已经准备好被使用了。
  6. 使用阶段:Bean 实例被容器缓存(单例模式下),供调用方通过getBean获取和使用。
  7. 销毁(Destruction):容器关闭时,单例 Bean 会被销毁。如果实现了DisposableBean接口,会调用destroy()方法;如果配置了destroy-method,会调用对应方法。

用代码演示一下初始化钩子方法会更直观。假设我们定义了一个UserService,并配置了init-method="myInit":

public class UserService { public void myInit() { System.out.println("初始化资源连接"); } public void myDestroy() { System.out.println("释放资源连接"); } }
<bean id="userService" class="com.example.service.impl.UserServiceImpl" init-method="myInit" destroy-method="myDestroy"/>

注意一个细节:prototype 作用域的 Bean 销毁方法不会被容器自动管理。容器只管创建它、初始化它、交给调用方,但后续的销毁和资源释放逻辑你得自己处理。这个坑在面试中经常被问到,在项目里如果用到了非单例 Bean 且里面持有外部资源,就很容易造成连接泄漏。

4.2 单例与多例:scope 到底怎么影响你

Spring 默认的 scope 是singleton,也就是说容器启动时创建所有单例 Bean,之后每次getBean拿到的是同一个实例。这样做的好处是省内存、省创建开销,在无状态的服务类中非常合适。但如果你是写有状态的组件,比如一个计数器、一个持有用户会话状态的对象,就要特别小心了——多线程环境下共享同一个实例,极容易产生数据错乱。

prototype作用域则相反,每次getBean都会创建一个新的实例,适合那些需要隔离状态的对象。代价是创建次数增多,性能开销变大,而且容器不会帮忙管理销毁。下面是对比表格:

作用域实例数创建时机特点典型场景
singleton1容器启动时(默认)共享实例、节省资源Service、Dao 等无状态类
prototype每次获取都新建每次 getBean 时状态隔离、创建开销大有状态组件、原型对象
request每个 HTTP 请求一个请求处理时Web 环境下有效Controller、请求处理对象
session每个 Session 一个会话创建时Web 环境下有效用户会话信息封装

在 Spring Boot 等 Web 项目中,你还会用到request和session作用域,但这两个只在 Web 容器环境下才生效。如果你在普通 Java 应用里配置了 request 作用域,启动时不会报错,但一旦触发创建就会直接报错,这个到时候注意一下就好。

4.3 初始化与销毁的三种实现方式对比

实现初始化和销毁逻辑,Spring 提供了三种途径:XML 的init-method/destroy-method配置、InitializingBean/DisposableBean接口、JSR-250 的@PostConstruct/@PreDestroy注解。三者的优先级顺序是:@PostConstruct→InitializingBean→init-method。

日常工作里,最推荐的是 JSR-250 注解方式,因为它是标准 Java 规范,不绑定 Spring,而且代码直观。init-method的方式适合在不改源码的情况下对第三方类做初始化配置,因为第三方类的源码你改不了,但可以在 XML 里指定编译后的方法名。InitializingBean接口则把 Bean 和 Spring 的 API 强绑定了,一般不推荐在业务代码里直接实现。

我自己实际用下来,很多资源初始化的需求(比如初始化线程池、预加载缓存、建立连接)采用@PostConstruct就够了,简单、清晰、可追踪。但有一个重要提醒:@PostConstruct的执行时机要比 Spring 容器把代理对象装配完毕早,如果在里面调用了依赖对象,偶发的空指针问题要重点关注。

5. 常见问题与排查技巧实录

5.1 新手最容易踩的五个坑

第一个坑:找不到配置文件或 Bean。报错信息一般是ClassNotFoundException或者NoSuchBeanDefinitionException。前者八成是 class 全限定名写错了,后者多半是 Bean 根本没注册,或者注册的名字和getBean传的名字不一样。排查思路:先检查 XML 文件路径是否正确,再确认<bean>的 id 是否和调用代码一致。IDE 里通常会有 config 文件的图标提示,鼠标点进去能直接跳转到<bean>定义,用这个功能会快很多。

第二个坑:Bean 的注入依赖找不到。报错NoSuchBeanDefinitionException或者BeanCreationException,比如你给 UserService 注入了 UserDao,但 UserDao 没有在 XML 里注册。本质上就是"说明书里写了要用某个部件,但仓库里没这个零件"。检查方法很简单,把 XML 里所有<bean>列出来,数一下有没有遗漏。

第三个坑:setter 注入失败。报了Property 'userDao' not found或者BeanNotOfRequiredTypeException,基本可以判断是类里没有对应的 setter 方法,或者 setter 的参数类型和 XML 里配置的不一致。举个容易踩的案例:类里setUserDao(UserDao userDao)写的是接口类型,但 XML 里注入的是一个实现类,类型理论上没问题;如果 XML 里误填了value而不是ref,就会尝试把字符串转换成 UserDao,直接报类型转换异常。

第四个坑:单例 Bean 里混用了有状态变量。这个问题在运行期才会暴露,特别隐蔽。比如在一个 singleton 的 Service 里放了一个private int count = 0;,多请求进来时会互相覆盖,输出结果完全不可预期。排查时要留意 Bean 的属性里是否包含可变字段。如果确实需要有状态,就要把状态对象单独做成 prototype Bean,并且通过 lookup method 或者ObjectProvider来获取。

第五个坑:构造器循环依赖。Spring 解决循环依赖有一个著名的三级缓存机制,但它只对 setter 注入的 singleton Bean 生效。如果你用的是构造器注入,且两个类互相把对方当作构造参数,那么容器启动时就会直接抛BeanCurrentlyInCreationException。遇到这种循环依赖,不要先想怎么调配置,应该先审视设计是不是有问题——互相依赖的两个类通常意味着职责边界没划清楚,可以考虑引入接口拆解、依赖倒置,或者把某个依赖改成 setter 注入来绕过问题。

5.2 排查思路:从报错信息倒推问题根因

Spring 的报错信息虽然长,但里面往往直接包含问题根因,关键是别被长堆栈吓到。我的排查习惯是:

  1. 先看最开头的Exception类型,判断是哪一类问题(类加载?Bean 定义?注入?循环依赖?)。
  2. 再看Caused by:部分,这里才是根因所在。比如ClassNotFoundException后面的那一串类名,就是要检查的类。
  3. 最后回到配置文件检查对应的 Bean 定义,核对 id、class、constructor-arg、property 是否齐全且正确。

分享一个我早期踩过的真实案例:我在 XML 里用<property name="dataSource" ref="dataSource"/>注入一个连接池,启动时报Cannot resolve reference to bean 'dataSource'。当时盯着配置看半天没找出问题,后来才发现我把<bean>的 id 写成了dataSource1,和 ref 对不上。这种小问题新手很容易犯,所以记牢一条原则:id 是 Bean 在容器中的身份证,ref 是住户,两者不一致,Spring 就会找不到人。

排查建议:在 Spring 的调试日志里,把org.springframework.beans.factory的日志级别调成 DEBUG,启动时能看到 Bean 的创建顺序、注入关系,几乎是排查 Java Bean 配置问题的一等利器。

5.3 推荐一套值得反复翻阅的学习顺序

最后聊聊我自己梳理出来的 Spring(1) 阶段学习顺序,按这个链路走会把框架根基打得更牢固:

  1. 先搭环境、跑通一个最小 XML 配置示例,亲眼看到 Bean 被容器创建。
  2. 用 5 到 10 个小练习,分别尝试构造器注入、setter 注入、集合属性注入、value与ref的区别。
  3. 研究 Bean 的生命周期,给每个阶段打印日志,看输出顺序。
  4. 对比 singleton 和 prototype 的行为,理解内存和创建时机的差异。
  5. 最后再回头看 IoC/DI 的定义,你会发现那些概念根本不需要背,因为你已经在代码里见过它们了。

我现在带新人的时候,都会让他们先走完上面这五步,再进入 Spring Boot 的学习。效果比一上来就抱着 Spring Boot 自动配置文档啃好很多,因为 Boot 在你面前铺了一层"黑话",但底层的地基还是一样的——你懂 IoC 和 Bean 的机制,遇到任何自动装配异常都能精准定位到问题入口。

写在最后的一点感悟

Spring(1) 阶段看起来内容不多,好像就 IoC、DI、Bean 几个概念,但真正吃透它其实决定了你和框架之间是"使用关系"还是"理解关系"。依赖注入的取舍、Bean 作用域的选择、生命周期钩子的利用,这些设计思想会在你后面写 Spring Boot、整合 MyBatis、做定时任务、配消息队列时反复出现。我在实际项目中排查过的不少运行期怪问题,最后都能追溯到这个阶段某个概念没搞清楚。所以别嫌基础,也别急着跳到下一个框架,把 IoC 和 Bean 这条线亲手走一遍,后面会省出大把踩坑的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询