很多同学用了好几年 Spring,写接口、做微服务都很麻利,但被问一句“你用的 Spring 到底包含了哪些模块”,大概率会愣住。原因也简单,日常开发里大家接触最多的是 Spring Boot 的 starter 依赖,一个坐标拉进来,框架就自己跑起来了,很少有人会去关心背后那一堆 jar 是谁、干什么用的。这篇文章专门把 Spring 的模块结构全景拆一遍,从 Spring Framework 的内核模块,到 Spring Boot 的 starter 组装方式,再到 Spring Cloud 的微服务模块家族,最后落到实际问题排查和版本搭配。适合想弄懂框架本质、准备面试、或者经常被依赖问题折磨的 Java 开发者,这篇读完你至少能清楚自己项目里那些临时的“第三方 jar”分别来自哪个模块。
1. 模块结构全景:先搞清楚你用的“Spring”到底是哪一层
1.1 从“Spring”到“Spring Boot”再到“Spring Cloud”:三层认知
很多人把 Spring 理解成一个东西,实际它是三层结构的生态。最底层是 Spring Framework,这才是真正定义 IoC 容器、AOP、事务抽象、MVC 框架这些核心能力的项目。中间层是 Spring Boot,它不替代 Framework,而是基于 Framework 做自动配置和 starter 封装,目标是让你少写配置。最上层是 Spring Cloud,一组基于 Spring Boot 的微服务组件集合,负责服务发现、配置中心、网关、熔断这些分布式场景下的问题。
这个区分很重要,因为市面上至少一半的“Spring 依赖冲突”问题,都源于没分清自己在用哪一层。比如你项目里 import 了 org.springframework.stereotype.Service,这个类来自 spring-context 模块,属于 Spring Framework;而你配置里写的 spring.datasource.url 前缀,处理它的逻辑来自 spring-boot-autoconfigure,属于 Spring Boot。如果只对着报错堆栈里的类名去查,很容易查错方向。
Spring AI、Spring Data、Spring Security 这些项目也是类似的定位:它们是 Spring 官方在 Framework/Boot 之上扩展出来的垂直领域模块,各有各的 artifact 和命名空间。所以理解模块结构,本质上是理解“什么东西是地基、什么东西是装修”。
1.2 为什么必须拆模块:依赖管理和按需加载的工程考量
早期 Java 开发有一种风格:一个框架把什么都塞进一个大 jar,你哪怕只用到其中一个类,也得把几百兆的依赖全部拉进来。随着项目增多,类路径冲突、启动变慢、二进制不兼容的问题会一起爆发。Spring 从一开始就选择了模块化路线,把不同职责拆成不同的 artifact,你用什么就引什么。
这样做的好处摆在明面上:依赖可控,升级某一个领域的模块不影响其他领域;按需裁剪,一个纯后端服务完全不需要引入 spring-webmvc;类路径清晰,出问题的时候能更快定位是哪一层出了问题。缺陷也有,就是上手门槛变高,新手面对一堆 jar 名容易懵。
不过别担心,Spring Boot 的 starter 已经把“选择恐惧症”的问题解决了大半。理解模块结构,并不是要求你手工拼 jar,而是让你知道 starter 内部到底替你拼了什么。比如 spring-boot-starter-web 里光是直接依赖就有 spring-web、spring-webmvc、spring-boot-autoconfigure、tomcat-embed-core、jackson-databind 等一堆,每一个都有明确的模块定位,出了问题也不至于两眼一抹黑。
提示:现在真的不建议去官网手工下载 Spring jar 包了。官方发布文件都走 Maven 中央仓库或者 Spring 官方仓库,用构建工具引入坐标是唯一推荐的方式。手工下 jar 再扔到 WEB-INF/lib,那是十年前的做法。
2. Spring Framework:真正定义“框架内核”的五组模块
2.1 Core Container:spring-core、spring-beans、spring-context
Core Container 是整个 Spring 的地基,由 spring-core、spring-beans、spring-context 三个 artifact 组成。spring-core 提供最基础的工具类、ASM 字节码处理、资源访问抽象等,它不依赖其他 Spring 模块,是所有模块的公共底座。spring-beans 负责 BeanDefinition 的解析、BeanFactory 接口的定义、以及 Bean 的实例化逻辑,IoC(控制反转)最核心的“注册加获取”机制就在这一层。spring-context 则是在前两者之上提供 ApplicationContext,把事件发布、国际化、资源加载、生命周期管理这些东西补全。
很多同学在面试里被问“BeanFactory 和 ApplicationContext 有什么区别”,答案其实就在模块分工里。BeanFactory 是 spring-beans 的产物,它只负责最基础的 Bean 容器能力;ApplicationContext 是 spring-context 的产物,它继承了 BeanFactory 的全部能力,并扩展了企业级功能。实际项目里你用的几乎都是 ApplicationContext 的实现,比如 AnnotationConfigApplicationContext。
这个模块组还有一个隐藏知识点:spring-context 依赖 spring-aop、spring-expression 等模块。也就是说,你只引入 spring-context,其实已经间接有了基础 AOP 和 SpEL 表达式的能力。很多开源框架的轻量级集成,就是只依赖 spring-context,不带 Spring Boot,照样能跑起来。
2.2 AOP 与 Aspects:横切逻辑的模块落点
AOP 相关的能力分布在两个 artifact 里。spring-aop 是核心,提供 ProxyFactory、Advisor、Pointcut、AspectJExpressionPointcut 这些编程式 AOP 的基础设施,以及 Spring 自己的基于 JDK 动态代理和 CGLIB 的代理机制。spring-aspects 则是用来集成 AspectJ 注解生态的模块,平时写的 @Aspect、@Before、@Around 这些注解虽然语法来自 AspectJ,但解析和执行的框架支撑在 Spring 这一侧。
这里有个经常被误解的点:Spring AOP 不是完整的 AspectJ,它只是借用 AspectJ 的注解语法,底层还是 Spring 自己的动态代理。所以切面只能作用在 Spring 容器管理的 Bean 上,对容器外的对象无能为力。理解了这一点,你就能明白为什么 @Transactional 自调用会失效、为什么内部方法调用不会走代理逻辑。
从模块结构看,AOP 是很多上层能力的基石。事务管理、Spring Security 的方法级权限、异步注解 @Async 的开启,全部依赖 spring-aop 的代理机制。如果你不需要这些,可以不引入 AOP 模块;一旦引入,就相当于给容器加了一层“代理生成器”,Bean 创建流程会变复杂,这也是为什么某些场景下 Bean 初始化顺序问题会突然冒出来。
2.3 Data Access:JDBC、事务、ORM 的模块家族
数据访问相关的模块是实际业务里最常打交道的。spring-jdbc 提供 JdbcTemplate、DataSourceUtils、SimpleJdbcInsert 等,把 JDBC 的样板代码消灭掉。spring-tx 是事务抽象模块,定义 PlatformTransactionManager、TransactionDefinition、TransactionStatus,以及声明式事务注解 @Transactional 的处理入口。spring-orm 负责与 Hibernate、JPA 等 ORM 框架做桥接,MyBatis 虽然不是 Spring 官方模块,但它的 Spring 集成也基于这些基础抽象。
平时你在 Spring Boot 里配一个 spring-boot-starter-data-jpa,背后自动配置会创建 LocalContainerEntityManagerFactoryBean 并装配一个 JpaTransactionManager,这个管理器实现的正是 spring-tx 里的 PlatformTransactionManager 接口。报错里看到的 CannotGetJdbcConnectionException 则来自 spring-jdbc 的异常转换体系,Spring 把 JDBC 的 SQLException 转换成了统一的 DataAccessException,这也属于模块内建能力。
面试里常问的“Spring 事务传播机制”,在编程模型中你只需要记住七种传播行为,但想深入一点,建议直接去看 spring-tx 模块里 AbstractPlatformTransactionManager 的源码。这个类是事务逻辑的核心,传播行为、回滚规则、事务同步都在这里实现。至少要知道:默认传播行为是 PROPAGATION_REQUIRED,默认回滚策略是只对 RuntimeException 和 Error 回滚,受检异常默认不回滚。
2.4 Web 与 MVC:spring-web 与 spring-webmvc 的分工
Web 层面的模块经常被混为一谈,实际上 spring-web 和 spring-webmvc 是两回事。spring-web 提供 HTTP 客户端 RestTemplate/RestClient、HttpMessageConverter、过滤器链、文件上传解析等通用 Web 能力,它本身不需要 Servlet 容器就能存在。spring-webmvc 则在 Servlet 技术栈上实现了 MVC 模式,DispatcherServlet、HandlerMapping、@Controller、@RequestMapping 都来自它。
如果项目只是要用 RestClient 调用第三方接口,不需要启动一个 Web 应用,那引入 spring-web 就够了。如果写的是传统 Spring MVC 接口项目,则需要 spring-webmvc。Spring Boot 的 spring-boot-starter-web 一步到位地把这两个都带上,同时内置了 Tomcat 和 Jackson,所以你会看到引入一个坐标,类路径里就多出来几十个 jar。
WebSocket 的场景也一样:spring-websocket 是单独模块,基于它再配合 spring-messaging 的 STOMP 支持,能实现服务端主动推送。你在 Spring Boot 里写 WebSocket 配置类的时候,发现需要引入 spring-boot-starter-websocket 或者单独加 spring-websocket、spring-messaging 两个坐标,原因就在这里——Web 容器帮你启动了,但协议处理模块得自己点进来。
2.5 Test 与其他支撑模块
除了上面几组,Spring Framework 还提供 spring-test,用来做单元测试和集成测试,比如 @ContextConfiguration 加载容器、MockMvc 测试 Controller、TestContext 框架管理测试上下文。现代 Spring Boot 项目的 @SpringBootTest 本质上也是建立在 spring-test 之上的。
还有一些支撑性模块:spring-context-support 提供对 Quartz、FreeMarker、邮件发送等第三方库的集成支持;spring-expression 提供 SpEL 表达式语言的解析和执行;spring-messaging 提供消息抽象;spring-jms 提供 JMS 标准消息服务支持。这些模块平时不一定显式关注,但会在某些功能点里被间接引入,比如 @Value 里的 SpEL 表达式能生效,就依赖 spring-expression。
3. Spring Boot:把模块结构重新打包的“组装者”
3.1 Starter 机制:一个依赖等于一组模块
Spring Boot 最大的贡献不是发明了新容器,而是创造了一套 starter 依赖规则。每个 starter 是一个空壳工程,本身几乎没有任何业务代码,它的唯一作用是用 Maven 依赖传递把一组相关模块打包带进来。比如 spring-boot-starter-web 的 pom 文件里声明了对 spring-web、spring-webmvc、spring-boot-starter-json、spring-boot-starter-tomcat 等的依赖,拉一个坐标等于拉一整套 Web 开发工具链。
starter 命名规则也需要看懂。官方 starter 统一叫 spring-boot-starter-xxx,比如 spring-boot-starter-security、spring-boot-starter-actuator。第三方开源组件一般叫 xxx-spring-boot-starter,比如 mybatis-spring-boot-starter、spring-boot-starter-data-caffeine 这类自己命名的。看到带 spring-boot-starter 的坐标,基本可以确认它是在为 Spring Boot 提供自动配置入口。
那 Spring Boot 怎么知道要启用哪些模块呢?靠的不是扫描全部类路径(那样太慢),而是读取 auto-configuration 配置文件。Spring Boot 3.x 里是 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 这个文件,里面按顺序列了所有需要尝试加载的自动配置类。每个自动配置类再用 @ConditionalOnClass、@ConditionalOnMissingBean 等条件判断当前类路径里有没有对应模块,有才生效。所以你的 pom 里没引入某模块,对应的自动配置就会静默跳过。
3.2 自动配置:spring-boot-autoconfigure 才是真正干活的模块
starter 只负责“带依赖”,真正的自动配置逻辑集中在 spring-boot-autoconfigure 模块。这个模块里有数百个 AutoConfiguration 类,比如 DataSourceAutoConfiguration、HttpMessageConvertersAutoConfiguration、WebMvcAutoConfiguration。它们做的事大同小异:检查条件、装配默认 Bean、绑定配置属性。
拿数据源举例:类路径里有 HikariCP 和 javax.sql.DataSource,DataSourceAutoConfiguration 就会自动创建一个 HikariDataSource,并把 spring.datasource.url、username、password 这些配置通过 DataSourceProperties 绑定上去。如果你手动声明了自己的 DataSource Bean,@ConditionalOnMissingBean 就会让自动配置让步,避免两边打架。
配置属性绑定也一样:spring-boot-autoconfigure 里的 ConfigurationProperties 扫描机制,把 application.yml 里的 spring.xxx 前缀映射到 Java 属性类。你写 spring.redis.host 能被 Redis 自动配置识别,就是因为 RedisProperties 这个类标着 @ConfigurationProperties(prefix = "spring.redis")。看日志的时候如果发现某个配置项没生效,优先怀疑的是匹配的自动配置没被触发,而不是配置本身写错了。
3.3 常见 starter 内部结构演示
为了直观理解,“拆一个 starter”是很好的学习方法。下面用表格把几个常见 starter 的核心构成列一下,你可以照着自己项目的依赖树验证。
| Starter 坐标 | 核心传递依赖 | 解决什么问题 |
|---|---|---|
| spring-boot-starter-web | spring-web、spring-webmvc、spring-boot-starter-tomcat、jackson-databind | Web 应用、REST 接口 |
| spring-boot-starter-data-jpa | spring-orm、spring-data-jpa、hibernate-core | JPA 持久化、Repository 数据访问 |
| spring-boot-starter-security | spring-security-config、spring-security-web | 认证授权、安全过滤链 |
| spring-boot-starter-actuator | micrometer-core、spring-boot-actuator | 健康检查、指标监控、应用信息暴露 |
| spring-boot-starter-validation | hibernate-validator、jakarta.validation-api | 参数校验、@Valid 注解支持 |
看这个表你会发现一个规律:几乎所有 starter 都依赖 spring-boot-autoconfigure 和 spring-boot-core。这就是“组装者”的角色定位——starter 不带业务代码,只做打包分发。如果你用 IDEA 打开 Maven 工具窗口,展开某 starter 的依赖树,会看到树枝底下挂着一长串模块名,那才是真正的运行时代码。
3.4 配置模块:从 application.yml 到各个模块的属性绑定
Spring Boot 的配置文件模块本质上是一个“转发器”,它把 yml 或 properties 里的 key 映射到各个模块的配置类上。这个设计让模块之间的配置风格保持统一,又不互相干扰。比如 spring.datasource 前缀只影响数据源模块,spring.web 前缀只影响 Web 模块,即使写错了前缀,最坏的结果也只是配置项没被识别,不会把别的模块搞坏。
实际项目中常见到 WebSocket 的配置写在 yml 里,比如设置端点路径、限制帧大小、配置心跳间隔。但要注意:yml 里的 spring.websocket 属性只是把参数绑定到 WebSocketProperties,具体生效的服务器端点还是要你自己写配置类注册 Handler 和拦截器。yml 不是万能的,它管理的是模块认可的配置项,业务层面的代码逻辑还得自己写。
聊到新版本,Java 21 之后结合 Spring Boot 3.5 可以在 yml 里用 spring.threads.virtual.enabled=true 打开虚拟线程,让 Tomcat 容器用虚拟线程处理请求。这个配置绑定的是 spring-boot 核心模块里新增的虚拟线程相关配置类。版本越新,模块里补充的配置项越多,因此如果你在看老教程,很可能会发现新配置不生效、旧配置被废弃,这是模块演进带来的必然结果。
4. Spring Cloud 与微服务模块:按场景继续拆
4.1 Spring Cloud 是模块集合而非单一框架
Spring Cloud 不是一个单独下载的框架,它是一篮子独立项目的总称,所有组件都以 spring-cloud- 前缀的 artifact 独立发布。为什么要这样设计?因为微服务的组件选择权应该交给开发者:有的系统只需要服务发现,有的需要网关,有的需要熔断,没必要把全部组件绑死在一起。Spring Cloud 做的事情是制定了一套公共抽象,比如 DiscoveryClient、LoadBalancerClient,各实现模块只要实现这些接口就能接入生态。
常见的 Spring Cloud 组件模块有:spring-cloud-gateway(网关)、spring-cloud-openfeign(声明式 HTTP 客户端)、spring-cloud-config(配置中心)、spring-cloud-circuitbreaker(熔断抽象)、spring-cloud-loadbalancer(客户端负载均衡)。再加上 spring-cloud-commons,它提供了服务发现、负载均衡、路由等核心接口,真正决定选哪个实现的,是你 pom 里引入了哪个具体组件。
注意区分:spring-cloud-gateway 用的是 WebFlux 技术栈,底层是 reactive 的,所以它不能和传统 spring-webmvc 共用一个 Web 容器。如果你把网关模块加到一个普通 MVC 项目里,启动报错多半是容器冲突。这个问题在 Spring Cloud 早期版本里比现在更明显,现在架构上已经分离得很干净,但概念上还是要搞清楚。
4.2 常用 Spring Cloud 组件模块速查
| 模块坐标 | 核心作用 | 常见替代 |
|---|---|---|
| spring-cloud-starter-gateway | 基于 WebFlux 的 API 网关、路由转发、过滤器链 | 无明显替代 |
| spring-cloud-starter-openfeign | 声明式 REST 客户端,简化服务间调用 | RestTemplate + LoadBalancer |
| spring-cloud-starter-loadbalancer | 客户端负载均衡,集成服务发现 | Ribbon(已进入维护期) |
| spring-cloud-starter-config | 配置中心客户端,从 Config Server 拉取外部配置 | Nacos Config |
| spring-cloud-starter-circuitbreaker | 熔断降级的统一抽象 | Sentinel、Hystrix(已停更) |
选型建议其实很简单:网关用 Spring Cloud Gateway,服务间调用如果用 Feign 风格就选 OpenFeign,负载均衡默认走 spring-cloud-loadbalancer。不要因为看到“熔断器”就急着引入,很多内部系统根本不需要熔断,加上反而增加运维负担。
4.3 Spring Cloud Alibaba 的模块补充
Spring Cloud Alibaba 是阿里开源组件在 Spring Cloud 生态里的适配集合,它的模块和官方组件形成互补。最常见的是 spring-cloud-starter-alibaba-nacos-discovery(服务发现与配置中心,用 Nacos)、spring-cloud-starter-alibaba-nacos-config(Nacos 配置管理)、spring-cloud-starter-alibaba-sentinel(流量控制与熔断降级)、spring-cloud-starter-alibaba-seata(分布式事务)。
Nacos 在国内项目里用得非常多,一个重要原因是它同时解决服务注册发现和配置管理两件事,部署成本比 Eureka 加 Spring Cloud Config 两套组件更低。Sentinel 则更像一个流量治理组件,支持限流、熔断、热点参数防护,这些在官方组件里没有对应模块。Seata 是分布式事务方案,适合强一致性的跨服务事务场景,也属于官方 Spring Cloud 不覆盖的领域。
模块结构上理解 Spring Cloud Alibaba,关键在于知道它依然是基于 Spring Cloud 抽象实现的:Nacos 注册中心实现了 DiscoveryClient 接口,Sentinel 适配了 CircuitBreaker 抽象,Seata 提供独立的事务协调器。所以你的业务代码不直接依赖具体实现,只调抽象接口即可,想从 Eureka 切到 Nacos,只需要换 starter 改配置,业务代码基本不用动。
4.4 模块选择建议:按场景拆解,不要全家桶
我见过太多团队,一上来就把 Spring Cloud 全家桶坐标全引进去,结果服务启动慢、日志刷屏、依赖冲突不断,最后排查半天发现很多组件根本没用上。靠谱的做法是按场景清单来:
- 只有两三个服务,不需要配置中心,不需要分布式事务,那 Spring Cloud Gateway 加一个服务发现组件就够了。
- 服务间调用频繁,可以加 OpenFeign,配合 LoadBalancer 做客户端负载均衡。
- 外部依赖多、稳定性要求高,再加 Sentinel 或 CircuitBreaker 模块做防护。
- 配置经常变化、服务多到不适合改配置重发,才上配置中心。
- 一旦涉及跨库强一致性事务,再评估 Seata,不要为“分布式事务”这个名词而引入。
另外还要考虑团队熟悉程度。引入一个模块,意味着团队未来要长期维护它,它的升级策略、与 Boot 版本的匹配关系都是成本。Spring Cloud 每套版本和 Boot 版本是严格绑定的,升级 Boot 很可能要求跟着升级 Cloud,有时候一个组件跟不上就得全部阻塞。
5. 模块间的协同机制:IoC、三级缓存、Bean 生命周期、事务边界
5.1 Bean 生命周期:模块如何配合完成一次创建到销毁
Bean 的生命周期不是某一个模块单独完成的,而是多个模块接力配合。bean 定义解析发生在 spring-beans:读取配置类的 @Bean、XML 里的 bean 标签、@ComponentScan 扫出来的组件,全部转成 BeanDefinition。实例化和属性填充也发生在 spring-beans,依赖注入在 populateBean 阶段完成。再往后,ApplicationContext 和扩展模块开始介入:比如 spring-context 的 EventListenerMethodProcessor 会把 @EventListener 方法注册成事件监听器,spring-aop 的 AbstractAutoProxyCreator 会在初始化后阶段为符合条件的 bean 生成代理。
初始化阶段的前后顺序也非常关键:BeanNameAware、BeanFactoryAware、@PostConstruct、InitializingBean、BeanPostProcessor,这些环节面向不同模块,写代码时如果不注意顺序,就会出现“明明注解了,但执行时机跟预期不一样”的问题。销毁阶段同理,@PreDestroy、DisposableBean、自定义 destroyMethod 的顺序也是固定的。
从模块协同角度看,Bean 生命周期是理解整个 Spring 源码的最佳入口。这也是“手写 Spring”类教程都喜欢让大家先实现一个简化版容器来模拟这个过程的原因:你会发现核心就是扫描定义、创建实例、执行回调、存进单例池这么几步,其余全是扩展钩子。
5.2 三级缓存原理:为什么需要三层 Map
Spring 单例 Bean 解决循环依赖靠的是三级缓存,这三级缓存在 DefaultSingletonBeanRegistry 的三个 Map 里:
| 缓存名称 | 存储内容 | 解决什么问题 |
|---|---|---|
| singletonObjects | 成品单例 Bean,完整初始化后的对象 | 供外部直接获取 |
| earlySingletonObjects | 提前暴露的半成品对象(可能被代理过但未完成完整初始化) | 让循环依赖中的另一半拿到引用 |
| singletonFactories | ObjectFactory 工厂,能在需要时生成早期引用 | 延迟代理生成,避免普通 Bean 提前被代理 |
为什么循环依赖需要三层而不是两层?核心在于代理和普通对象的区别。如果 Bean A 和 Bean B 互相依赖,B 需要拿到 A 的引用时,A 还没执行完初始化后处理器。如果直接用 ObjectFactory 生成早期引用,还能把 AOP 代理延后到真正需要时再生成。如果 A 有切面,B 拿到的引用必须是代理对象而非原始 Bean,否则切面逻辑不对。三层设计把“创建半成品”和“生成代理”两个动作解耦了,第一次引用时调用 getEarlyBeanReference 生成代理,没有循环依赖则不用走这条路径,性能影响最小。
三级缓存只对单例 scope 的 setter/字段注入循环依赖有效,构造器注入和 prototype 作用域的循环依赖直接报错,这是 Spring 设计上就放弃的场景。Spring Boot 3.x 里也可以关闭循环依赖支持,默认已经不允许了,但可以通过配置打开。
5.3 AOP 与事务:模块边界和原理
@Transactional 之所以能生效,是因为 Spring 事务模块挂在 AOP 机制上面。具体说,spring-tx 提供了一个名为 TransactionInterceptor 的增强器,它实现了 MethodInterceptor,在方法调用前开启事务、执行后提交或回滚。有没有这个增强器,取决于你的容器里是否注册了 transactionAdvisor,而 Spring Boot 的自动配置会在检测到 spring-tx 和 DataSource/JPA 等模块后自动注册。
关键教训:事务是在代理层拦截生效的,所以同类内部调用不会走代理,导致 @Transactional 失效。这跟 5.1 里讲的 Bean 生命周期和 5.2 里讲的代理逻辑直接相关。解决办法是注入自身代理、拆分成两个 Bean、或者用 AopContext.currentProxy(),但最推荐的还是拆分 Bean,让事务方法定义在另一个类里。
事务回滚边界同样值得记住:默认只有 RuntimeException 和 Error 回滚,受检异常不会回滚。想让受检异常也回滚,得在注解里写 rollbackFor = Exception.class,这个写法和 spring-tx 模块的回滚规则实现有关,也经常是面试题里考察“为什么我的事务没回滚”的标准答案。
5.4 模块缺失的常见报错模式
模块结构不清,最终都会体现在报错上。最常见的几类:
- ClassNotFoundException / NoClassDefFoundError:某个类找不到,多半是缺依赖或依赖被排除了。比如启动时看到 org.springframework.web.context.ContextLoaderListener 找不到,说明项目引了 spring-webmvc 但没引 spring-web,或者传依赖冲突被裁掉了。
- NoSuchBeanDefinitionException:容器里没有某个类型的 Bean,两件事需要查:模块是否引入(自动配置没跑)、Bean 是否被扫描到。
- BeanCurrentlyInCreationException:循环依赖,而且是 Spring 解决不了的那种,比如构造器注入循环依赖或 prototype 循环依赖。
- NoSuchMethodError:类版本不一致,最常见是传递依赖里有两个不同版本的 jar,拿 V2 编译却加载到了 V1。
- 端口占用或容器启动异常:Web 模块的 jar 版本和 Servlet 容器不匹配,比如用了 Tomcat 9 却编的是 Servlet 5 API。
排查这类问题有一套固定思路:先看异常属于哪个包的类,再明确这个类对应哪个模块,最后打开依赖树看该模块是否存在以及版本是否正确。很多时候问题不是缺模块,而是“多模块”把版本搞乱了。
6. 实战:按需搭建自己的 Spring 模块组合 + 常见问题速查
6.1 最小化模块清单:从零拼一个 Spring 项目
想验证自己对模块结构的理解,建议不要用 Spring Boot 脚手架,而是手工建一个最小 Maven 项目,只引用最底层的 Spring Framework。下面这个 pom 只引入 spring-context,就足够支撑一个 IoC 容器加上事件、国际化等能力。
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.2.0</version> </dependency> </dependencies>注意,spring-context 会传递依赖 spring-core、spring-beans、spring-aop、spring-expression,所以这个项目已经具备基础 AOP 和 SpEL 能力。之后你再按需添加 spring-jdbc 拿到 JdbcTemplate,添加 spring-tx 拿到事务抽象,添加 spring-webmvc 拿到 MVC 控制器,添加 spring-test 拿到测试上下文。这样每一步都能看见模块变化带来的新能力,非常直观。
需要提醒的是,Spring Framework 版本不是越新越好。Spring Framework 6.x 默认基于 Jakarta EE 9,Servlet API 是 jakarta.servlet 前缀,如果你还在用老项目的 javax.servlet,版本必须回退到 Spring 5.x。这种前缀变化就是模块升级带来的破坏性变更,不看模块版本清单直接升级,项目必然编译失败。
6.2 查看自己项目的模块构成:dependency:tree 实用操作
想知道“你用的 Spring 到底包含了什么”,最直接的方法就是执行依赖树命令:
mvn dependency:tree -Dincludes=org.springframework用 IDEA 的也可以直接打开 Maven 面板,点开某个依赖节点展开传递依赖。执行之后你会看到类似输出:
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.5.0:compile [INFO] | +- org.springframework:spring-web:jar:6.2.0:compile [INFO] | +- org.springframework:spring-webmvc:jar:6.2.0:compile [INFO] | +- org.springframework.boot:spring-boot-autoconfigure:jar:3.5.0:compile [INFO] | +- org.apache.tomcat.embed:tomcat-embed-core:jar:10.1.20:compile [INFO] | +- com.fasterxml.jackson.core:jackson-databind:jar:2.17.0:compile这份输出直接告诉你:一个 starter-web 最终把 Web 技术栈、Servlet 容器、序列化库全部带进来了。多跑几次这个命令,你对自己项目里每个模块的来源会越来越有把握。
Gradle 项目对应的方式是 dependencies 任务:
gradle dependencies --configuration compileClasspath6.3 版本兼容矩阵:Framework、Boot、JDK 之间的绑定关系
Spring Boot 的版本管理会锁定其依赖的所有 Spring Framework 模块版本,所以项目里不该手写 Spring Framework 的版本号,而是用 Boot 的 BOM 或 parent 管理。下面这张表是实践中最常用的对应关系:
| Spring Boot 版本 | Spring Framework 版本 | 最低 JDK | 主要变化 |
|---|---|---|---|
| Boot 2.7.x | Framework 5.3.x | JDK 8 | 最后一个 2.x 维护线,javax API |
| Boot 3.0.x-3.1.x | Framework 6.0.x-6.1.x | JDK 17 | 迁移到 jakarta API |
| Boot 3.2.x-3.4.x | Framework 6.1.x-6.2.x | JDK 17 | 支持虚拟线程(部分版本支持) |
| Boot 3.5.x | Framework 6.2.x | JDK 17/21 | 完善虚拟线程、模块更精简 |
Spring Cloud 和 Boot 的版本绑定更严格,官方发布了一个带版本名的发布线,比如 2024.0.0 这个云版本号对应 Boot 3.4。所以升级的时候不能只升 Boot 不管 Cloud,也不能只升 Cloud 不管 Boot,需要查 Spring Cloud 官方文档里的版本对照表。Spring AI 模块同样对 Boot 版本有要求,引入前务必看对应版本的 README。
6.4 常见问题速查表:模块类报错排查对照
| 报错现象 | 直接原因 | 排查思路 | 推荐解法 |
|---|---|---|---|
| NoClassDefFoundError: jakarta/servlet/... | Servlet API 缺失或版本不匹配 | 检查 spring-boot-starter-web 是否引入、Tomcat 版本 | 加入对应 starter,确认 JDK 迁移情况 |
| NoSuchMethodError: xxx(参数) | 类路径里有多个版本的 jar | mvn dependency:tree 找重复版本 | 在 pom 里排除旧版本,或统一用 BOM 管理 |
| NoSuchBeanDefinitionException | 容器中没有目标 Bean | 检查扫描路径、自动配置是否生效 | 增加 @ComponentScan 覆盖范围、或手动声明 Bean |
| BeanCurrentlyInCreationException | 构造器循环依赖或 prototype 循环依赖 | 梳理 Bean 引用关系 | 重构依赖,避免互相构造注入 |
| Failed to introspect Class | 类缺少运行期依赖 | 看完整异常堆栈找缺哪个类 | 补依赖或调整模块组合 |
| Property 'dataSource' is required | 数据源 Bean 缺失 | 检查 spring-boot-starter-jdbc 是否引入 | 引入对应 starter 或手动配置 DataSource |
排查模块类问题有一个很实用的节奏:先看堆栈第一行定位缺的类,再判断这个类应该属于哪个模块,然后用依赖树确认该模块的状态。我见过不少人一看到 ClassNotFound 就往 pom 里乱加依赖,结果越加越乱。先明确“它在哪个 jar 里”,比你猜重要一百倍。
最后再分享一个经验:新项目启动前,先花五分钟把 pom 里的依赖树拉出来看一遍,搞清楚哪些模块是自己主动引的、哪些是传递依赖。这个习惯在遇到稀奇古怪的类路径问题时能帮你省下大量排查时间。Spring 的模块结构说复杂也复杂,说简单也简单,它本质上就是一套按职责切分、按需组合的工程方案。把这套结构装进脑子里,你读任何 Spring 相关的代码和配置,都会比原来顺畅得多。