
提到 Spring很多人的第一反应是“面试必考”“注解一大堆”“Bean 容器”但你要是真在项目里写了两三年 Java被问一句“Spring 到底帮你解决了什么问题”大概率会卡壳。说出来无非是“管理对象”“依赖注入”“解耦”这些词但再往深一问“三级缓存为什么要三级”“事务为什么有时候不生效”“AOP 到底是怎么织入的”就容易露馅了。这篇文章我就是想把“Spring 到底解决了什么问题”这条线完整捋一遍从 Java 开发最原始的痛苦开始讲清楚 IoC 容器、Bean 生命周期、AOP、Spring Boot 自动配置这些核心机制到底在解决什么每个机制背后的“为什么”是什么最后结合我这些年踩过的坑整理一份偏实战的排查笔记。不管你是刚开始学 Spring 的入门者还是用了几年的老手这篇都能帮你把脑子里零散的知识重新串成体系。1. 从 new 对象说起Spring 到底解决了什么“老问题”想理解任何一个框架的价值都得回到它诞生的年代去看当时的人被什么问题折磨。1.1 早期 Java 开发代码是怎么一步步“拧巴”起来的在 Spring 出现之前或者说在没有 IoC 容器的开发方式里最常见的写法就是一个类里直接 new 它依赖的类。举个例子一个订单服务需要调用用户服务和库存服务public class OrderService { private UserService userService new UserService(); private StockService stockService new StockService(); public void createOrder(Order order) { userService.getUser(order.getUserId()); stockService.deduct(order.getProductId()); } }这段代码表面上看没什么问题但它在真实项目里会让维护变得非常难受。第一耦合太重。OrderService和UserService、StockService的实现类死死绑定在一起一旦UserService的构造方法需要传参数比如要传入数据源、配置项、日志客户端你这里就得跟着改。类与类之间不再是“我依赖一个接口”而是“我依赖一个具体的实现类”系统的弹性几乎为零。第二没法替换。假如某天你想把UserService替换成一个带缓存代理的实现你必须改动所有 new 过它的地方。你可能说“全局搜索替换不就行了”但大型系统里被替换的实现可能被几十个类引用替换成本极高而且极易漏改。第三测试极其困难。单元测试时你想给OrderService注入一个 Mock 的UserService根本做不到因为OrderService内部直接 new 了真实对象。早期为了写一个测试得把整个环境搭起来一跑就依赖数据库、依赖网络、依赖一堆外部服务。这个痛点经历过的人都懂。这些问题的根源不是“某个类写错了”而是对象的创建和组装方式错了。对象之间的关系完全由代码硬编码系统一复杂这个“装配关系”就变成了一团乱麻。1.2 控制反转把新建对象这件事交出去Spring 给出的方案用一句话说就是别自己 new把创建和组装对象的活交给容器。这就是控制反转IoCInversion of Control。它反转的是什么是“对象创建权”的控制方向。正常的控制流是OrderService自己控制着UserService的创建它需要的时候就 new 一个。反转之后UserService的创建不由OrderService决定而是由一个独立的容器统一管理容器创建好之后再“注入”给OrderService。这就是依赖注入 DIDependency Injection。我用一个生活化的类比来理解以前你吃饭是自己买菜、自己洗、自己炒、自己刷碗一切由你控制麻烦但独立。IoC 容器就像请了一个全能管家你只需要说“我要吃鱼香肉丝”管家就把菜做好端到你面前。你不再关心食材从哪来、怎么做、盘子谁来洗你的关注点只放在“吃什么”上。对于代码而言你关心的只是“我需要一个UserService类型的对象”至于它到底是谁、从哪构造、要不要包装代理统统交给容器。这样一来类与类之间依赖的是抽象接口而不是具体实现类。1.3 一个贴近业务的对比换实现要改几行代码咱们直观感受一下 IoC 带来的差异。没有 Spring 的写法public class MessageSender { private SmsSender smsSender new SmsSender(); private MailSender mailSender new MailSender(); public void send(String type, String message) { if (sms.equals(type)) { smsSender.send(message); } else { mailSender.send(message); } } }如果现在新增一个微信通知你得在MessageSender里加一个WechatSender属性再加一行else if。发消息这个核心业务逻辑被频繁地“打开修改”。有了 Spring 的写法public interface MessageSender { void send(String message); } Component public class SmsSender implements MessageSender { public void send(String message) { ... } } Component public class MailSender implements MessageSender { public void send(String message) { ... } } Component public class MessageService { private final ListMessageSender senders; public MessageService(ListMessageSender senders) { this.senders senders; } }以后再加新的发送渠道只需要新增一个Component实现类MessageService一行都不用动。你会发现代码从“驱动方自己控制依赖”变成了“容器自动收集并注入依赖”这就是开闭原则在真实工程里的落地。1.4 边界感什么时候真的不需要 Spring但也得说句公道话。Spring 不是银弹也不是任何场景都必须上。一个只有几百行的脚本工具一个一次性执行的定时任务或者一个纯粹的内部算法模块用 Spring 反而会把简单问题复杂化。你要维护容器配置、理解代理机制、处理加载顺序这套复杂度可能比直接 new 对象还高。我个人的判断标准很简单**如果你的项目里对象之间的关系超过二十个或者存在频繁替换实现、需要单元测试、需要事务控制这些需求那就值得上 Spring。**如果只是一个写给自己用的工具类直接 new 反而是更合理的选择。Spring 解决的是“复杂度管理”问题而不是“消灭复杂度”问题。它把代码里的耦合搬进了容器配置换来的是统一管理和可扩展性。2. 核心机制拆解IoC 容器、三级缓存与 Bean 的一生知道了容器解决什么问题接下来就得看看容器内部是怎么干的。这部分是 Spring 原理的重头戏也是面试最爱深挖的地方。2.1 IoC 容器的一次完整工作流程从 BeanDefinition 到单例池Spring 容器管理一个 Bean从“定义”到“使用”大概要走这么几步配置解析。容器读取 XML、注解或 Java Config 配置得到一批BeanDefinition。你可以把BeanDefinition理解为 Bean 的“图纸”上面记录了类名、作用域、是否懒加载、依赖关系、初始化方法等元信息。注意图纸本身不是对象只是描述对象怎么造。注册。BeanDefinition被注册到容器内部的一个 Map 里key 是 Bean 的名字value 是 BeanDefinition。实例化。容器开始为每个 BeanDefinition 创建真正的对象实例。默认情况下单例 Bean 在容器启动时就会被创建而不是在第一次使用时才创建。这一步实际上还是执行构造方法new出来但调用者是容器而不是业务代码。依赖注入。实例化之后容器会扫描这个 Bean 的属性字段或者构造参数发现它依赖其他 Bean就继续去容器里找对应的 Bean 并设置进去。注意这里有个关键点对象已经被 new 出来了但此时它还不是一个“完整”的 Bean。初始化。执行各种初始化逻辑比如PostConstruct标注的方法、InitializingBean接口的afterPropertiesSet、自定义 init-method。这一步之后Bean 才真正“可用”。使用。容器将 Bean 返回给调用方业务代码开始处理逻辑。销毁。容器关闭时执行PreDestroy、DisposableBean.destroy等销毁逻辑释放资源。这里有一个必须区分的概念BeanFactory和ApplicationContext。BeanFactory是最底层的 IoC 容器接口只提供基本的 Bean 获取能力ApplicationContext是更上层的封装在 BeanFactory 基础上加上了事件发布、国际化、资源加载、自动扫描等功能。实际开发中你接触的几乎都是ApplicationContext但面试被问到底层机制时要能说出来这两个的关系。2.2 三级缓存Spring 是怎么“忍”住循环依赖的循环依赖问题在 Spring 源码里是个绕不开的话题。先看场景A依赖BB依赖A你让 Spring 同时创建这两个 Bean如果不做任何处理就会出现“要创建 A发现需要 B去创建 B又发现需要 A再回去创建 A……”这样永远无解的循环。Spring 处理单例 Bean 循环依赖的方式就是那张非常有名的三级缓存表缓存层级存放内容作用一级缓存singletonObjects完全初始化好的成品 Bean最终获取 Bean 时从这里拿二级缓存earlySingletonObjects尚未完成初始化的“早期引用”的半成品 Bean用于提前暴露实例解决注入问题三级缓存singletonFactoriesObjectFactory工厂对象用于延迟生成早期引用是解决 AOP 代理问题的关键整个流程可以这样理解。创建A时A的实例先被 new 出来然后容器往三级缓存里放一个ObjectFactory这个工厂将来可以生成A的早期引用如果 A 需要被代理真正被提前引用的对象应该是代理对象。接着A开始执行属性填充发现自己需要B容器就去创建B。B同样被 new 出来往三级缓存里放工厂然后B开始填充属性发现自己需要A。此时B去找A一级二级缓存都没有但三级缓存里有A的工厂。于是B通过工厂拿到A的早期引用完成了注入。B初始化完成后A继续执行也从容器里拿到B并注入最终A初始化完成被放入一级缓存。那“为什么非要三级而不是二级”这是面试里最容易答不清的问题。关键就在第三级缓存存的是ObjectFactory而不是直接存一个半成品对象。因为 Spring 的 Bean 在最终返回时可能需要被 AOP 代理。如果只有二级缓存那就意味着在A刚被 new 出来的时候就必须决定它最终是原始对象还是被代理后的对象或者干脆先存原始对象等初始化完再替换。但问题是在B注入A的那个时刻如果A需要代理B拿到的就必须是代理对象如果A不需要代理B拿到的就应该是原始对象。这个决定在“早期暴露”时其实还没有定论。三级缓存的ObjectFactory把“怎么生成早期引用”这件事延迟到了真正有人需要的那一刻对象工厂可以在那一刻去检查是否需要创建代理。这就是多出第三级的根本原因。顺便说一句构造器循环依赖是无法通过三级缓存解决的因为在构造阶段对象还没被 new 出来早期引用无从谈起。如果遇到这种情况通常要改设计或者用Lazy延迟注入打破循环。还有一个重要前提只有单例作用域的 Bean 才能靠三级缓存解决循环依赖。原型作用域的 Bean 本身不缓存每次都是新对象根本没有“提前暴露”的机会。2.3 Bean 生命周期那些值得关注的扩展点Bean 的一生从“图纸”到“回收”中间藏着大量扩展点。理解这些扩展点比背面试题更重要的是它能帮你排查很多奇怪的问题比如“为什么我的PostConstruct方法没有执行”“为什么拦截器没生效”。我把单例 Bean 的完整生命周期按顺序列一下记住这张表后续排查基本够用实例化前阶段允许通过BeanPostProcessor的postProcessBeforeInstantiation直接返回一个代理对象从而跳过默认的实例化过程。实例化阶段通过构造器创建对象实例。属性填充阶段执行依赖注入把容器里其他 Bean 赋给当前实例的字段。执行各种Aware接口BeanNameAware、BeanFactoryAware、ApplicationContextAware。这个阶段是让 Bean 感知自己所在容器的环境和身份。BeanPostProcessor.postProcessBeforeInitialization初始化前增强比如Autowired注解的解析就是在这个阶段完成的。初始化阶段执行PostConstruct标注的方法然后执行InitializingBean.afterPropertiesSet最后执行自定义的 init-method。BeanPostProcessor.postProcessAfterInitialization初始化后增强。AOP 动态代理通常就是在这里生成的所以这个阶段返回的对象可能已经是一个代理对象了。使用阶段对象进入单例池持续被业务代码引用。销毁阶段容器关闭时执行PreDestroy、DisposableBean.destroy、自定义 destroy-method。这张表里最容易出问题的就是第 4 步和第 7 步的混淆。Aware是让 Bean 自己感知容器而BeanPostProcessor是容器对 Bean 做手脚。在实现自定义扩展时要分清你要干预的是“Bean 初始化前后”还是“Bean 创建之前”两者执行的时机完全不同。2.4 AOP 其实是在做“代理”这件事Spring AOP 的核心思想并不复杂**不是改动业务代码本身而是给目标对象生成一个代理对象在代理对象里插入额外的逻辑。**这样日志、权限、事务这些横切逻辑就能和业务逻辑分开。AOP 的关键实现机制是动态代理。Spring 在运行时给目标类生成代理对象代理对象和目标对象实现同样的接口调用方拿到的是代理对象。调用方法时代理对象先执行切面逻辑再通过反射调用目标对象真正的方法。这里有两个技术分支需要区分如果目标对象实现了接口Spring 默认使用JDK 动态代理生成的代理类实现同样的接口。如果目标对象没有实现接口Spring 使用CGLIB子类代理通过继承目标类并改写方法来实现。Spring Boot 2.x 之后的行为是默认强制使用 CGLIB 代理即使目标类实现了接口。这个变更的好处是代理行为一致不会再出现“明明实现了接口却被 JDK 代理搞得类型对不上”的问题代价是 CGLIB 基于继承被代理的类不能是 final 的被代理的方法也不能是 final 的。理解 AOP 的代理机制有几个非常实际的价值。比如你写了一个类类内部一个方法调用本类的另一个方法切面往往不生效。为什么因为 Spring 的代理是“外部调用代理对象才生效”内部方法调用是通过this调用目标对象的原始方法绕过了代理事务注解自然失效。这也是事务失效最常见的坑之一。另外一个值得理解的概念是Advisor。Aspect注解的类会被解析成多个Advisor每个 Advisor 由“切点 通知”组成。切点决定哪些方法需要增强通知决定增强的逻辑是什么。Spring 在执行目标方法时会先通过切点匹配找到适用的 Advisor 链再按顺序执行。这个模型的背后是经典的“责任链”模式和 Servlet Filter 的执行方式有相似之处。3. 从 Spring 到 Spring Boot、Spring AI生态在延续同一个理念Spring 最大的生命力在于它的生态一直在进化。从 XML 配置到注解驱动再到 Spring Boot 的自动配置最后到 Spring AI思路一脉相承把复杂性收敛起来让开发者专注于业务。3.1 Spring Boot 的“开箱即用”是怎么实现的很多人用 Spring Boot 时会觉得“它就是个内置 Tomcat、自动配好了数据库依赖的启动器”。但 Spring Boot 真正厉害的地方在于它的自动配置机制。你写一个最简单的接口只需要一个带SpringBootApplication注解的主类一个RestController然后就能跑了。这个“能跑”的背后发生了什么关键在于SpringBootApplication里包含了一个EnableAutoConfiguration它的实现原理可以拆成三步Spring Boot 启动时AutoConfigurationImportSelector会扫描所有依赖 jar 包里的META-INF/spring/...AutoConfiguration.imports文件旧版本是spring.factories文件把所有的自动配置类名加载进来。这些自动配置类上通常有一堆条件注解比如ConditionalOnClass判断 Classpath 里有没有某个类、ConditionalOnMissingBean判断容器中是否已经有这个 Bean、ConditionalOnProperty判断配置项是否开启。只有当条件满足时对应的配置才生效。所以你引入spring-boot-starter-web后Classpath 里有了DispatcherServlet、Tomcat这些类ServletWebServerFactoryAutoConfiguration的条件就成立了自动帮你在内存里启动一个内嵌 Tomcat你引入mybatis-spring-boot-starter后Classpath 里有了SqlSessionFactory相关自动配置就生效自动帮你创建数据源和 SqlSessionFactory。这套机制的核心价值是**“约定大于配置”不再是一句口号而是变成了代码层面的判断逻辑。**传统 Spring 项目里你得自己在一个巨大的 XML 里维护各种 Bean 定义Spring Boot 里框架根据依赖推断出你大概率需要什么然后提前帮你准备好你只需要覆盖自己的个性化配置。我在实际项目里用这个特性最多的场景是“条件装配”。比如某个监控类库我希望它只在生产环境生效测试环境不加载就给它加一个ConditionalOnProperty(name monitor.enabled, havingValue true)非常干净不需要写任何 if 判断。3.2 配置管理从 XML 到属性绑定的演进配置这个问题在 Spring 里也经历了三代演进。第一代是 XML 的bean标签里写property nameurl valuejdbc:mysql://.../写起来繁琐且没有任何类型安全。第二代是Value注解加.properties文件简单了但项目里到处是魔法字符串一旦配置项改名编译期根本发现不了。第三代是ConfigurationProperties这是我现在最推荐的方式Component ConfigurationProperties(prefix app.oss) public class OssProperties { private String endpoint; private String bucket; private String accessKeyId; private String accessKeySecret; // getter / setter }对应配置文件里的一段app: oss: endpoint: https://oss.example.com bucket: my-bucket access-key-id: xxxx access-key-secret: xxxx这样配置和代码之间建立起结构化的绑定关系IDE 可以自动补全配置项拼写错误在启动时就会被校验出来。演进的核心逻辑是让配置越来越“类型安全”、越来越“可维护”这和 Spring 整体追求的方向是一致的。还有一个知识点值得一提Spring 的Environment抽象统一了配置来源支持启动参数、环境变量、配置文件、配置中心等多种来源并且有严格的优先级顺序。所以你会看到有些项目里 YAML 文件里写了个值但不起作用很可能就是被更高优先级的配置来源覆盖了。排查这类问题直接注入Environment打印一下实际值比看半天配置文件管用得多。3.3 Spring AI用类似 Spring 的思路对接大模型近几年 AI 应用爆发Spring 也没有缺席。**Spring AI 这个项目做的事情就是给大模型的接入提供一个统一抽象层。**它的思路和 Spring 对待数据库的方式非常像过去 Java 要连各种数据库每种数据库的驱动 API 都不一样于是有了 JDBC 这一层标准化接口。Spring AI 想做的就是大模型界的 JDBC。这样说可能有点抽象我举个具体例子。在没有 Spring AI 之前你要对接 OpenAI、通义千问、智谱这类大模型得分别研究他们的 HTTP 接口、SDK、鉴权方式写一堆胶水代码。有了 Spring AI它统一了聊天的ChatClient接口、向量存储的抽象、Prompt 模板渲染等一堆东西。你写一段业务代码调用ChatClient底层具体是哪个模型切换起来相对容易。Spring AI 里最近讨论比较多的是 RAG 和 Agent 场景。RAG检索增强生成的核心思路是先把文档拆分成片段、做向量化、存入向量库每次提问时先从向量库里检索相关内容再和问题一起拼给大模型。这套流程在 Spring AI 里被封装成了VectorStore和DocumentRetriever等抽象开发者不用自己去拼向量检索的逻辑。我也要提醒一句Spring AI 目前迭代非常快版本变动也大很多 API 处于快速演进中把它用在生产环境之前一定要确认好版本兼容性和社区活跃度。我自己的习惯是AI 接入这类高风险、频繁变更的功能尽量在底层再做一层自己的封装避免框架的 API 变动影响核心业务代码。4. 实战中的坑与排查笔记事务、注入与常见面试题原理讲再多落到项目里还是会遇到各种奇奇怪怪的问题。这一部分我把自己实际踩过、帮别人排查过的坑整理出来这些才是面试里那些“为什么”的真实出处。4.1 事务失效的几个典型场景Transactional是最常用也最容易“莫名其妙不生效”的注解。我见过的失效场景绝大多数可以归到下面几类失效场景原因解决方案类内部方法自调用调用方是this原始对象不是 Spring 代理对象事务拦截器根本没机会执行拆分成两个类或注入自身代理或使用AopContext.currentProxy()方法不是 publicSpring 默认基于 CGLIB 生成子类代理非 public 方法本质不可被代理覆盖改成 public 方法异常被 catch 吞掉事务拦截器只能感知方法向外抛出的异常如果异常被 catch 了它认为方法正常返回不会回滚不要在事务方法内吞异常或者手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()抛出的是受检异常Spring 默认只回滚 RuntimeExceptionIOException 这类受检异常不会触发回滚在注解上显式指定rollbackFor Exception.class数据库不支持事务比如 MySQL 的 MyISAM 引擎不支持事务使用 InnoDB 引擎多数据源场景下事务管理器没指定容器里有多个PlatformTransactionManagerSpring 不知道用哪个在注解上指定transactionManager我印象最深的一个坑是自调用问题。有一个服务类一个方法调用了同类里的另一个Transactional方法排查了好久没找出问题最后发现是内部方法调用绕过了代理对象。后来我养成了一个习惯事务方法尽可能放到独立的 Service 类里不要让调用关系形成“类内部互相调用”。还有一个小技巧排查一个方法到底有没有走代理时在方法里打印this.getClass()看看类名里有没有$$。CGLIB 生成的代理类类名通常长这样OrderServiceImpl$$EnhancerBySpringCGLIB$$xxxxx。如果看到的还是原始类名那基本可以断定事务不生效是因为代理没生效。4.2 Bean 注入相关的坑依赖注入本身很简单但项目复杂之后会有一些边界情况。第一个坑同一类型有多个 Bean。比如你有SmsSender和MailSender都实现了MessageSender这时候Autowired一个MessageSender会报错因为容器里有两个候选者。解决方式有三种用Primary标注首选 Bean用Qualifier(smsSender)指定名字或者干脆全部注入成ListMessageSender按需遍历。我个人最推荐第三种它既避免了依赖具体实现名又保留了扩展性。第二个坑构造器注入和字段注入的选择。字段注入代码最简洁但它有两个问题一是不显式声明依赖会让测试时替换困难二是类加载时依赖可能还没有准备好导致一些隐蔽的初始化顺序问题。我现在推荐的原则是能用构造器注入就用构造器注入。它让依赖在对象创建时就被强制声明清楚对象一旦构造成功状态就是完整的。Spring 官方也一直推荐构造器注入。第三个坑循环依赖的另一种解法。前面说三级缓存能解决循环依赖但那是针对单例 Bean 的“缓存”方案。如果你用的是构造器注入循环依赖就没法通过三级缓存解决。这时候优先考虑重构代码把循环依赖的结构拆开。实在拆不开可以在其中一个依赖上加Lazy让 Spring 先注入一个代理占位真正使用时再解析目标 Bean。这是最后的手段但至少能绕开问题。4.3 面试常问的 Spring 核心问题我是怎么理解的这部分帮大家串一下面试常考的问题把我自己的理解写出来不是标准答案是思考方式。问题一什么是 Bean 的生命周期我建议不要背列表而是从“对象从创建到销毁经历了哪些时机”去组织答案。先分大阶段实例化、属性填充、初始化、使用、销毁。再插入扩展点Aware接口、BeanPostProcessor的 before/after、PostConstruct、InitializingBean、PreDestroy、DisposableBean。最后加一句“AOP 代理是在初始化后阶段通过 BeanPostProcessor 生成的”把生命周期和 AOP 连起来面试观感完全不同。问题二三级缓存为什么能解决循环依赖关键是三个缓存的语义一级放成品二级放早期引用三级放对象工厂。二级和三级的分工核心是为了延迟“早期引用应不应该被代理”这个决策。这个问题要讲清楚“为什么不是二级”才算真的理解。回答时把我的 2.2 小节里的流程用“创建 A、发现 B、B 反取 A”这个故事串一遍就很有说服力。问题三Spring AOP 和 AspectJ 有什么区别Spring AOP 是运行时的动态代理方案只能在 Spring 管理的 Bean 方法被调用时增强AspectJ 是编译期或类加载期的字节码增强功能更强大能支持字段访问、构造器拦截等 Spring AOP 做不到的切点。绝大多数业务场景用 Spring AOP 就够了它的短板是自调用问题没法解决因为代理只对容器外部的调用生效。问题四Spring 事务的传播行为是什么传播行为解决的是“多个事务方法互相调用时事务边界怎么合并”的问题。最常用的REQUIRED是“如果当前有事务就加入没有就新建”这也是默认值。REQUIRES_NEW是“不管当前有没有事务都新建一个把当前事务挂起”。理解传播行为最好的方式是实际写几个方法互相调用来测试光看书很难形成直观感受。写在最后我的一点实际体会用了这么多年 Spring我最深的体会是**不要止步于“会用”但也不要陷入“读源码才算懂”的焦虑。**框架的学习分三层第一层是知道怎么用注解能完成开发任务第二层是理解背后的机制出了问题能顺着线索排查第三层是能看懂源码有能力参与框架定制和二次开发。大多数开发者需要达到的是第二层而这篇博文想帮你到达的也正是这一层。如果你想更进一步我特别推荐一个学习方式**找一个周末尝试手写一个极简版 Spring。**不需要实现全部功能只需要做到能扫描注解、能创建单例 Bean、能按字段注入依赖、能处理一个最简单的循环依赖。这半天的折腾胜过看一百篇源码解析。我当年就是在仿写过程中才真正理解了三级缓存和 BeanPostProcessor 的价值——当你自己写的容器没办法注入依赖时你才会对 Spring 的设计有切身的敬畏。最后再分享一个经验**在排查 Spring 相关问题时先确认“容器里到底有多少个同类型 Bean”“当前对象是不是代理对象”“配置是否被更高优先级覆盖”这三个问题能解决九成以上的困惑。**工具方面IDEA 的 Spring 插件可以直接看到 Bean 的依赖图和注入关系比在代码里翻来翻去高效得多。希望这篇总结能帮你理清思路下次再遇到“Spring 到底解决了什么问题”的追问时你会发现自己已经能给出一个很完整的答案了。