本节摘要:Spring 家族有几十个模块,全学一遍既不现实也无必要。本节按"请求走到哪一站会碰到哪个模块"给模块分类,建立一张以旅程为坐标系的选型地图,而不是按官方文档的字母序去啃。
Spring 的模块列表读起来像字典:Core、Context、Beans、AOP、Tx、JDBC、ORM、MVC、WebFlux、Security、Messaging、Test、Boot、Cloud……初学者常见的做法是逐个查、逐个试,结果每个都懂一点,串不起来。换一个坐标系就好了:模块不是并列的名词,而是分布在请求旅程的不同站点上的"关卡设备"。你在哪一站施工,就只需要哪几个模块。
按请求旅程,我把常用模块划成四组:
| 功能区 | 旅程位置 | 模块 | 什么时候需要 |
|---|---|---|---|
| 地基 | 请求到达之前 | Core、Beans、Context、SpEL | 一定需要,容器装配一切对象 |
| 通道 | 请求进出应用 | MVC、WebFlux、Security、WebSocket | 做 Web 服务就需要 |
| 业务支撑 | 请求处理之中 | AOP、Tx、JDBC、ORM、Data、缓存、校验 | 有业务规则和数据就要 |
| 外延 | 请求结束之后 | Messaging、Batch、Cloud、Test | 异步化、批处理、微服务、质量保障 |
一个具体例子帮你想清楚。假设要做"下单"接口,依赖清单大致长这样:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
三个 starter 背后是"通道 + 业务支撑"两个功能区:web 带来 MVC 全套与内嵌 Tomcat,data-jpa 带来数据访问与事务,validation 带来参数校验。地基部分被所有 starter 共同依赖,永远在场。不需要的模块不进依赖清单,应用里就没有对应的那道关卡——这也是 Spring 模块化的实际含义:装了才有,没装就没有。

模块之间有依赖关系,版本不匹配是新手最常踩的坑。经验法则有三条:其一,用 Spring Boot 的依赖管理统管版本,业务工程的依赖声明一律不写版本号;其二,Spring 框架自身(Framework 6.x)与 Boot(3.x)、Java 版本(17+)有对应关系,升级要整套一起动;其三,遇到 ClassNotFoundException 或 NoSuchMethodError,第一反应查依赖树里同一模块是否混入了两个版本——
mvn dependency:tree -Dincludes=org.springframework
这类错误的报错位置往往离真正的病因很远,因为模块是在请求旅程的不同站点上被分别触发的。
背景:很多人引入了 starter 却不知道它悄悄带进了哪些模块,出了问题无从排查。操作:在工程目录执行 mvn dependency:tree(Gradle 工程用依赖报告任务),把输出里属于 Spring 家族的行挑出来,按四个功能区归类。
以只加了 web 与 data-jpa 两个 starter 的工程为例,依赖树里能看到:spring-core、spring-beans、spring-context(地基);spring-web、spring-webmvc(通道);spring-aop、spring-tx、spring-jdbc、spring-orm、spring-data-jpa(业务支撑)。结果:三行 starter 声明换来二十多个模块,这就是"成套设备"的含义。解读:尝试从依赖树里删掉一个模块(比如手工排除 spring-aop)再启动,应用往往仍能启动,但走到对应站点才报错——依赖可以缺,旅程不能缺。变式:再引入 spring-boot-starter-security,重新看依赖树与启动日志,会发现过滤器链上多了十来个安全过滤器,第 6 章的关卡在这一步就埋下了。
还有一个值得亲手做的对照实验:分别在加了与没加 validation starter 的两个工程里,给控制器参数加上校验注解。没加的那个,注解只是个摆设,非法参数原样进入方法——印证了"装了才有":注解本身不产生任何行为,行为来自装进来的模块。
其一,能否只引框架核心不引 Boot?可以,但你要自己写配置类、自己注册分发器、自己管理依赖版本——Boot 的价值正是把这些装配自动化,脱离它等于放弃旅程地图里"实验场"那一站。其二,starter 会不会让依赖越滚越大?确实会,所以交付前值得做一次依赖瘦身:移除没用到的模块,应用启动日志会明显变短,攻击面也随之收窄。其三,看到陌生注解不知来自哪个模块怎么办?在开发环境里按住跳转键点进注解定义,看它所在的包名——包名的第一段就是答案,这个习惯比搜索引擎快。沿着旅程记模块、按站点查归属,这两条方法比背依赖清单可靠得多。