news 2026/10/15 5:49:07

Spring IoC容器与依赖注入:从原理到实战踩坑全记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring IoC容器与依赖注入:从原理到实战踩坑全记录

做了几年Java后端,Spring和Spring Boot的代码写了不老少,但说句实话,以前那些Bean、IoC、AOP的概念在我脑子里一直是“知道大概,细问就慌”的状态。这次给自己定了个目标,老老实实把Spring的底子重新啃一遍,第一期就先聚焦最核心的IoC容器和依赖注入,也就是Spring(1)学习内容这条主线。这篇文章既是给和我一样半桶水的人做个参考,也是我自己的学习台账,后续还有(2)(3),一层一层往里挖。

第一篇我打算从“为什么非学不可”讲起,再到第一个能跑起来的容器程序,最后把我踩过的坑原原本本列出来。如果你已经在用Spring Boot做业务,但始终没系统看过底层机制,这篇应该正好对路。

1. 为什么学Spring:一个老项目的重构把我逼回了基础

动机这东西不解决,学习很难坚持。我这次不是看了什么技术文章突然燃起来的,是实打实被一个过去的项目折磨了一轮。

1.1 层层new出来的痛苦

当时接了一个接近停运边缘的老接口模块,代码风格非常“原始”。就拿下单来说,OrderService里要做库存校验、扣减库存、生成订单、发消息,一个方法里new了五个对象:

public class OrderService { public void placeOrder(Order order) { StockService stock = new StockService(new StockDao()); CouponService coupon = new CouponService(new CouponDao(), new UserService()); MessageService message = new MessageService(new MessageDao()); // 后面是一堆逻辑... } }

改一个构造参数,所有new过它的地方全要跟着改。想写单元测试?也没法轻易替换依赖,因为对象全都硬编码在方法里了。整个模块就是一堆“为了实例化别的类而存在的代码”。

这个痛点你应该不陌生。Spring的IoC容器解决的就是“谁来创建对象、谁来管理对象之间的依赖关系”这个问题——把对象生命周期交给容器,由容器负责创建、装配、销毁。业务类只管说自己需要什么,不用亲自去new。

1.2 面试和学习路线的双重倒逼

这两年陆陆续续帮朋友做模拟面试辅导,发现Spring相关的问题出现频率高得离谱,而且问法越来越底层:BeanFactory和ApplicationContext的区别?Bean的生命周期有哪些阶段?@Autowired是怎么实现注入的?循环依赖为什么用setter能解决、构造器不行?这些问题,没有真正动手验证过,仅靠背面试题,问到第二轮就会露馅。

更现实的一点:现在主流框架都是用Spring做地基的,Spring Boot只是把配置自动化和启动方式简化了,底层还是容器那一套。不把地基弄清楚,以后排查线上问题时只能靠猜。

1.3 先学的为什么不是Spring Boot

我身边有些人学Spring,直接拿Spring Boot的项目模板开干,配置都是自动的,跑起来也快,但问一句“自动配置到底做了什么”就答不上来。我这次选择先学Spring Framework的XML配置版本,刻意不用Spring Boot。原因很简单:XML配置虽然繁琐,但每一步配置都显式可见。Bean标签怎么写的、属性怎么注入的、扫描路径怎么配的——全都摊在眼前,容器的运行逻辑才能看得清。

把经典方式走一遍,再看Spring Boot的自动配置,才会明白那些“魔法”到底是替我们做了什么。

2. 学习之前先把三件事想透:容器、控制反转、依赖注入

现在网上讲Spring的教程太多了,但很多一上来就贴代码,概念反而没人讲透。我先把三个最基础的东西用自己的话重新捋了一遍。

2.1 什么是容器,它到底管什么

容器这个词听起来抽象,我自己的理解就是“一个替你保管对象的大仓库”。以前对象是你自己new,new完自己用、自己扔,生命周期全靠自觉;有了容器之后,你告诉容器“我需要一个什么样的对象”,容器负责把它创建好、把它的依赖也组装好,最后交到你手上。你不关心它什么时候实例化、怎么找到依赖的,这些全在容器里完成。

Spring里有两个核心接口:

  • BeanFactory:最底层的容器接口,提供了最基本的getBean能力,是“最小可用版”的容器。
  • ApplicationContext:继承自BeanFactory,加了事件发布、国际化、资源加载等功能,也是绝大多数项目实际使用的容器。

我第一次用ClassPathXmlApplicationContext的时候,其实就是把XML配置文件喂给ApplicationContext,让它去解析、创建所有Bean。后面的所有学习内容,几乎都绕不开这一步。

2.2 控制反转(IoC)到底反转了什么

“控制反转”这四个字容易把人绕晕,我用一个生活类比:以前你做饭,食材要自己去菜市场买、洗、切、炒,全程你自己控制;现在你点外卖,你和食材生产、烹饪过程完全解耦,你只下单,餐厅把成品端到你面前。

对于代码来说:

  • 控制正转:A类里主动new出B类,A决定B何时创建、何时销毁,控制权在A手里。
  • 控制反转:A想用B,只需要定义一个B类型的属性,由容器在运行时把B的实例注入给A,控制权从A转移到了容器手里。

反转的不是功能,而是“创建和管理依赖对象”的职责。这个理解一旦建立,再看到xml里的bean配置或者@Autowired,就不会再觉得奇怪。

2.3 IoC和DI的关系,不是一回事

我见过有人把IoC和DI混着说,其实它们一个是思想,一个是实现方式。控制反转是设计原则,方向是“依赖的管理权交给外部”;依赖注入是这条原则的具体落地手段——外部容器把依赖“注入”到对象内部。就好比“健康饮食”是一种理念,“少油少盐”是具体做法,两者相关但不是一个层面。

Spring实现IoC的方式,主要就是依赖注入。了解到这个程度就够用了,后面的精力应该花在怎么配置、怎么注入、怎么处理生命周期上。

3. 搭建第一个能跑的Spring程序:从零到getBean

光有概念不写代码,等于白学。第一周的周末,我花了一下午从空目录开始搭了一个最小Spring项目,完全没依赖Spring Boot。

3.1 环境准备

我用的工具相对朴素,都能免费搞定:

组件版本/说明
JDK1.8(Spring 5.x完全兼容,后面学Spring 6再上JDK17)
构建工具Maven 3.6+,用IDEA自带的和命令行都行
IDEIDEA Community版,或者Eclipse都行,对Spring项目差别不大

这里提醒一下:Spring 5.x默认适合JDK 8+,Spring 6.x开始强制要求JDK 17+。如果你电脑装了多个JDK,注意在IDEA的Project Structure里看清楚项目用的到底是哪个版本,不然会出现编译通过但运行报UnsupportedClassVersionError的情况。

依赖就一个核心包,在pom.xml里引入:

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

spring-context会连带拉入spring-core、spring-beans、spring-aop等基础模块,一个依赖基本够入门了。如果导入依赖时下载很慢,在Maven的settings.xml里配一个国内镜像源,速度和稳定性都会好很多。

3.2 定义接口和实现类

Spring虽然不强制要求面向接口编程,但实际项目中几乎都是一套接口加实现类。我写了一个简单的问候服务:

public interface GreetingService { String greet(String name); } public class GreetingServiceImpl implements GreetingService { private String prefix; public void setPrefix(String prefix) { this.prefix = prefix; } @Override public String greet(String name) { return prefix + ", " + name; } }

注意这里的setPrefix方法,后面XML配置里会用到setter注入,容器会通过这个方法把配置的值传进来。

3.3 写XML配置并加载容器

在src/main/resources目录下创建applicationContext.xml:

<?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 https://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="greetingService" class="com.example.demo.GreetingServiceImpl"> <property name="prefix" value="Hello"/> </bean> </beans>

然后写一个带main方法的启动类:

public class Application { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); GreetingService greetingService = context.getBean("greetingService", GreetingService.class); System.out.println(greetingService.greet("Spring Learner")); } }

运行后控制台输出:Hello, Spring Learner。就这么简单,但背后发生的事情一点也不简单——容器读取XML,解析bean定义,实例化GreetingServiceImpl,调用setPrefix注入值,把成品放进容器缓存里,最后getBean时取出来交给你。

3.4 第一次用注解方式替代XML

跑通XML之后,我又试了注解驱动的方式。其实Spring从2.5就开始支持注解,3.0以后逐渐成为主流。注解方式的核心就是:

@Component public class GreetingServiceImpl implements GreetingService { @Value("Hello") private String prefix; @Override public String greet(String name) { return prefix + ", " + name; } }

同时把XML改成:

<context:component-scan base-package="com.example.demo"/>

这样容器会去扫描com.example.demo包下所有标了@Component的类,把它们注册成Bean。省去了一行行写bean标签的功夫,但本质还是一样:告诉容器有哪些类、怎么管理它们。

两种方式我都推荐亲手跑一遍。跑完你才会理解,注解只是“配置的另一种写法”,底层还是同一套Bean注册和依赖注入逻辑。

4. IoC与DI的底层运行逻辑:容器启动时到底发生了什么

跑通demo只是第一步,我第二周做的事是断点调试,把Spring初始化核心流程的关键代码一行行走了一遍,这个收获比看十篇博客都大。

4.1 BeanFactory的加载过程

ApplicationContext启动的时候,远不只是读个配置文件那么简单。我梳理了自己跟踪代码看到的几个阶段:

  1. 定位:根据传入的XML路径找到配置文件。
  2. 加载:把XML里面的 定义解析为BeanDefinition,也就是“Bean的说明书”,类名、属性、作用域、懒加载标记全都在里面。
  3. 注册:把BeanDefinition注册到容器内部的一个注册表里,相当于登记造册。
  4. 实例化:对于非懒加载的单例Bean,容器启动的时候就直接创建实例;懒加载的bean则等到getBean时才创建。
  5. 依赖注入:创建实例后,容器会根据BeanDefinition中的属性信息,调用setter或构造器把依赖注入进去。

当时我在实例化那段代码里看到一堆DefaultListableBeanFactory、AbstractAutowireCapableBeanFactory之类的类名,枯燥是枯燥,但配合断点一步步看,之前背的概念全部落地了。

4.2 单例与原型:默认为什么是单例

Spring默认的scope是singleton(单例)。也就是说,同一个Bean在容器里只有一个实例,所有地方拿到的都是同一个对象。这样设计的好处很直接:

  • 创建开销小,对象只new一次。
  • 无状态对象天然线程安全。
  • 节省内存。

坏处也明显:如果Bean里存了可变状态,多线程共用同一个实例就可能出问题。Spring里常见的Service、Dao基本都是无状态的,所以单例是合理默认。

我们再看下prototype(原型)scope,每次getBean都会创建一个全新实例,适合需要保存用户状态之类的场景。配置很简单:

<bean id="demoBean" class="com.example.demo.DemoBean" scope="prototype"/>

但实际业务不要滥用prototype,频繁创建对象会带来不小的性能损耗。

4.3 三种依赖注入方式的对比如今怎么选

我入门时试了三种注入方式,都写过demo:

注入方式优点缺点常见场景
setter注入灵活,可在注入后修改依赖可能不完整可选依赖
构造器注入依赖必须全部提供,对象创建即完整参数多时代码冗长必选依赖
注解注入(@Autowired)写起来最简单隐藏了依赖关系,排错稍难日常业务开发主流

成败的关键点在于:建议优先用构造器注入,因为一个对象创建后就应该处于完整可用状态,而不是先new出来再慢慢set。现在Spring官方也是这个建议。至于@Autowired,它核心是byType匹配,如果有多个同类型候选Bean,需要配合@Qualifier指定名称。这些细节,不多写几个用例是体会不到坑在哪的。

4.4 生命周期:从创建到销毁要经历什么

面试经常问Bean生命周期,我之前背过一遍八股文,但直到跑代码加日志才真正明白。简化版流程是:

  1. 实例化:容器根据BeanDefinition创建一个原始实例。
  2. 属性填充:通过setter等方式把配置的属性和依赖注入进去。
  3. 初始化:执行BeanPostProcessor的前置处理,然后调用afterPropertiesSet或init-method指定的方法。
  4. 使用:把成品Bean交给业务使用。
  5. 销毁:容器关闭时,调用destroy-method或DisposableBean的destroy方法。

我用代码验证了一下,在XML里配init-method="init"和destroy-method="destroy",类里加了对应方法,启动时容器创建单例Bean确实会调用init方法,关闭容器时也会自动执行destroy。整个过程配合日志观察,比纯靠记忆清晰得多。

另外,lazy-init="true"可以把Bean的实例化推迟到第一次getBean时执行。对于启动时需要快速响应的应用,把不着急用的Bean设置懒加载,可以缩短启动时间,但对排查问题来说会多一层迷惑,用的时候权衡一下。

5. 第一周踩过的坑:从报错到恢复的真实排查过程

说句实话,我第一周一半时间用在调错上,但恰恰是这些报错让Spring机制在我脑子里扎了根。下面这几个坑我觉得值得专门记录。

5.1 ClassNotFoundException:spring-context依赖背后藏了什么

我最早在pom.xml里只加了spring-core,以为自己“能用核心就行”,结果运行时报ClassNotFoundException,提示找不到org.springframework.context.ApplicationContext。当时查了一圈才发现,ApplicationContext在spring-context模块里,spring-core只是最底层。

更要命的是Spring模块之间有传递依赖,手动只加某一个模块经常漏掉其他配套包。我后来直接用了spring-context,它会把spring-beans、spring-core、spring-aop等一起带进来。如果确实需要用Spring的其他功能,也尽量让依赖通过传递方式引入,而不是手动东拼西凑。

5.2 XML头部的xsd路径报错

用IDEA新建XML配置文件时,经常出现IDEA提示无法解析schema或找不到spring-beans.xsd。老项目里常见的写法是:

http://www.springframework.org/schema/beans/spring-beans-3.2.xsd

Spring 5以后不需要在schemaLocation里加具体版本号了,直接写spring-beans.xsd反而更稳。我当时把版本号去掉后,一切恢复正常。另外网上很多旧教程给的xsd地址是http开头的,换成https更稳妥,否则在部分网络环境下会失败。

5.3 NoSuchBeanDefinitionException:getBean的参数写错了

启动成功之后,又遇到一个NoSuchBeanDefinitionException,提示No bean named 'greetingService' available。我去检查XML,id没写错,class也写对了,但就是找不到。

后来发现是我在main方法里用了getBean(GreetingServiceImpl.class),而XML里bean配置的id是greetingService,这个不乱。真正的问题是我把base-package搞错了,当时XML里配置了component-scan扫描com.example.demo,但类放在com.example.demo.model包下,扫描路径没覆盖到。把包结构调整到扫描范围之内后,问题解决。

这个坑提醒我:按类型拿Bean和按名称拿Bean,条件完全不同。按类型拿,容器里得恰好只有一个匹配的Bean;按名字拿,id必须完全一致。排查时先确定自己到底用的哪种方式。

5.4 循环依赖:setter能解决,构造器直接卡死

为了验证循环依赖,我写了一个A依赖B、B依赖A的demo:

@Component public class A { private B b; @Autowired public void setB(B b) { this.b = b; } } @Component public class B { private A a; @Autowired public void setA(A a) { this.a = a; } }

setter注入时,Spring先创建A实例,把它放到“早期曝光”的缓存里,再注入B,B又需要A时可以直接从缓存里拿到,整个过程能走通。但如果把@Autowired放在构造器上,Spring创建A时就需要B,创建B时又需要A,两边都在等对方,直接抛BeanCurrentlyInCreationException。

所以Spring解决循环依赖靠的是三级缓存,但构造器注入没法提前暴露实例,自然无解。实际开发里,想通过循环依赖来“解决问题”本身就是设计问题,更合理的做法是抽出一个新的依赖层级,让依赖关系变成单向的。

5.5 配置文件路径与打包路径的错位

main方法里用ClassPathXmlApplicationContext("applicationContext.xml"),如果配置文件没有恰好放在resources根目录下,运行时会找不到。我一开始把文件放在resources/config目录下,路径就得写成config/applicationContext.xml。如果用的是IDEA,注意target/classes目录里有没有把配置文件复制过去,有时清理缓存后文件没同步,也会导致“找不到配置文件”。

这类问题看起来蠢,但在真实项目里出现频率极高。写代码之前先确认一下文件的位置,比在报错里折腾半小时有效得多。

6. 第一遍学完的验收清单和下一站规划

第一遍Spring基础学完,我给自己的要求不是“看完了”,而是能完完整整回答下面这些问题,并且随手能写出一个十几行的demo来验证:

6.1 自测清单,逐项过一遍

  • 能说清楚Spring容器是干什么的,BeanFactory和ApplicationContext区别在哪。
  • 能写出XML配置和注解配置两种方式注册Bean。
  • 能说明构造器注入、setter注入、@Autowired各自特点和适用场景。
  • 能列出Bean生命周期中实例化、属性填充、初始化、销毁这几个阶段。
  • 能解释singleton和prototype的区别,以及默认使用singleton的原因。
  • 能说清楚Spring解决setter循环依赖的基本思路,以及为什么构造器循环依赖无法解决。

这六项如果都能顺利讲清楚,那第一部分的基础算是打牢了。如果有一两项讲不顺,我的建议是先回去再看对应源码,而不是急着往下一章赶。

6.2 下一步的学习顺序

Spring涉及的模块很多,我的计划是排好优先级,不东一榔头西一棒子。排在下一篇的是:

  1. 注解驱动开发,把@Component、@Autowired、@Configuration、@Bean这些注解用熟,理解它们和XML的等价关系。
  2. AOP的入门,搞清楚切面、切点、通知是怎么织入到目标方法上的。这部分是Spring另一个核心能力,做日志、权限、事务的时候都会用到。
  3. Spring对数据库事务的封装,重点是声明式事务的实现思路。
  4. Spring MVC的请求处理流程,看看DispatcherServlet如何把请求路由到控制器。

把这些学完,才会真正有把握去啃Spring Boot的自动配置。

6.3 学习方式上的一个建议

如果你和我一样是边工作边学,不建议直接看系统性的视频课程逐集刷,那样太慢也太被动。我的做法是围绕一个目标问题(比如“什么是循环依赖”),先把基础文档浏览一遍,然后立刻写最简demo去复现,复现成功后再去看源码验证自己的理解。这样一次学一个点,每个点都动手,记忆特别牢固。

网上资料质量参差,最靠谱的首选还是Spring官方文档,版本对得上,内容准确。唯一问题是英文阅读量有点大,但坚持啃几章之后,后面会越来越快。如果英语不是问题,官方文档加上源码调试,是性价比最高的学习路径。

6.4 结束前的几句体会

经过这段时间的三番五次折腾,我最大的感受是:Spring基础这东西,看会和自己写会是两码事,自己写会和伺候好一个正在运行的Spring程序又是两码事。很多人止步于“能跑起来”,但稍微遇到点环境变化或复杂依赖,就不知所措。真正把容器运行逻辑、Bean生命周期这些沉到底,后面再学什么都顺。下一篇我准备写注解驱动和AOP的应用,到时会把这次总结的踩坑经验继续加进去。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 5:49:04

Python校园兼职平台:轻量可信匹配与订单状态机实战

简介&#xff1a;本资源是一份完整的Python毕业设计文档&#xff0c;面向计算机专业本科生&#xff0c;聚焦学生兼职平台系统的设计与实现&#xff0c;解决校园兼职信息不对称、申请流程不透明、线上签约与支付缺失等实际问题。文档涵盖用户管理、兼职信息发布与筛选、在线申请…

作者头像 李华
网站建设 2026/10/15 5:48:32

JavaScript汉字拆分:解决UTF-16代理对导致的字符误切问题

简介&#xff1a;这是一份面向前端开发者与汉字信息处理学习者的JavaScript汉字结构分析工具包&#xff0c;聚焦于汉字部首与部件的程序化拆分逻辑&#xff0c;适用于汉字教学系统、输入法开发、搜索引擎优化等场景。资源以单个Word文档&#xff08;.doc&#xff09;形式提供&a…

作者头像 李华
网站建设 2026/10/15 5:48:18

Spring Boot宠物医院管理系统:从需求分析到接口设计的完整实践

前阵子一位做传统生意转行的朋友找到我&#xff0c;说他接了一家宠物医院的数字化改造小活&#xff0c;想让我帮忙看看系统该怎么搭。我去现场转了一圈&#xff0c;印象最深的是&#xff1a;前台手写预约登记、病历纸堆了两大箱、药房盘点靠感觉、收费还在用Excel对账——整个就…

作者头像 李华
网站建设 2026/10/15 5:48:17

从“0x3f 第48天 复习”说起:如何把复习变成可追踪的高效学习

我一直觉得&#xff0c;手机备忘录里那种只写了半行、连标点都来不及打的内容&#xff0c;才是我们最真实的学习状态。“0x3f 第48天 复习 10:24-12&#xff1b;00”——标题里的分号是用中文输入法敲出来的&#xff0c;说明当时手速很快&#xff0c;根本没空切换英文标点。就是…

作者头像 李华
网站建设 2026/10/15 5:47:47

无需GPU部署大模型:CPU+INT4量化+内存映射实战指南

1. 项目概述&#xff1a;为什么“无需GPU”部署大模型正在成为真实可行的选择最近在多个技术社区和开发者群聊里&#xff0c;频繁看到一个关键词组合&#xff1a;“OpenAI 开源替代”“自己部署大模型”“不用GPU”。这三者叠加在一起&#xff0c;乍看有点反常识——毕竟过去三…

作者头像 李华
网站建设 2026/10/15 5:47:41

SentiWordNet+NLTK情感分析:轻量、可解释、可教学的词典法实战指南

1. 项目概述&#xff1a;为什么SentiWordNetNLTK仍是情感分析的“稳态基线” 你是不是也遇到过这样的场景&#xff1a;刚学完Python基础&#xff0c;想做个能跑通的情感分析小项目&#xff0c;结果一搜“情感分析”&#xff0c;满屏都是BERT、Transformer、微调大模型……配置…

作者头像 李华