Spring Boot多模块项目实战:从Maven父工程到模块依赖与打包

📅 发布时间:2026/10/2 10:28:27
Spring Boot多模块项目实战:从Maven父工程到模块依赖与打包
1. 新建 module 之前先把这几件事想明白很多同学第一次接触 Spring 多模块项目就是在 IDEA 里对着父工程右键点几下 New Module然后 project 面板里多出来一个目录。至于这个目录和父工程之间到底什么关系、依赖怎么引、打包能不能过心里其实没底。新建 module 这件事本身确实不难但背后串着四根线Maven 的父子继承、模块间的依赖坐标、Spring 容器扫描、最终的打包集成。这四根线只要有一根没理清楚后面必然会遇到“明明建了 module却引用不了另一个模块的类”“Bean 找不着”“打出来 jar 没有主清单”这类问题。这篇文章就把完整流程从头到尾过一遍把每一步的“为什么”也顺带讲清楚。适用场景很明确空项目从零搭多模块骨架、单体项目拆模块、面试复习多模块构建原理。不管你是刚学 Spring Boot 的新手还是已经写过一些系统但一直没细抠模块化构建细节的人这篇文章里都有可以直接照抄的操作也有一些常规文档里不会写的坑。2. 新建 module 之前先搞懂多模块的本质2.1 多模块项目的真实结构父 pom 子 pomMaven 的多模块结构说白了就是一个父 pom 管一群子 pom。父 pom 里用modules声明“我有哪些儿子”子 pom 里用parent声明“我爹是谁”。!-- 父工程 pom.xml -- groupIdcom.demo/groupId artifactIdspring-multi-module/artifactId version1.0.0-SNAPSHOT/version packagingpom/packaging modules moduless-common/module moduless-dao/module moduless-service/module moduless-web/module /modules注意父工程的packaging必须是pom因为父工程本身不产出 jar它只是聚合点。你不需要在父工程里写任何 Java 代码它的任务就是统一管理依赖版本、插件配置、公共属性。每个子 module 的 pom 长这样parent groupIdcom.demo/groupId artifactIdspring-multi-module/artifactId version1.0.0-SNAPSHOT/version /parent artifactIdss-dao/artifactId子模块不需要再写 groupId 和 version因为会从parent里继承。这个继承机制跟 Java 的继承类似子 pom 自动获得父 pom 里绝大多数配置。有一点要特别注意父 pom 里如果定义了dependencies子模块会无条件继承如果只定义了dependencyManagement子模块则必须显式声明依赖坐标不带版本号才能使用父工程管理好的版本。后者才是大型项目的标准做法因为不是每个子模块都需要 spring-boot-starter-web。2.2 为什么非要把项目拆成 module单模块项目在一开始写起来很爽Controller、Service、Mapper全塞一个工程里跑起来也快。但项目一旦上了规模问题就来了业务之间互相耦合你不敢随便改某个公共类想单独测试某个服务得把整个项目都 build 一遍代码量大了之后任何一个团队成员的改动都可能把别人的功能带崩。拆成 module 之后的效果是什么呢以最常见的分层为例ss-common通用工具类、统一返回结构、常量ss-dao数据库实体、Mapper 接口ss-service业务逻辑实现ss-webController 层也就是启动模块每一层的职责边界都钉死了模块与模块之间只依赖接口不依赖实现细节。比如ss-service只依赖ss-dao的接口将来换个 ORM 框架只要接口不变service 层代码不用动。这就是分层架构和模块化构建结合的典型好处不只是“目录分成几块”而是编译级别上的隔离。还有一层实际的好处增量编译。单体项目改一行代码整个项目重新编译多模块项目里某个模块改了只需要重新构建这个模块和依赖它的模块省出来的时间在大型项目里非常可观。2.3 选型Spring Initializr 还是纯 Maven新建 module 时有两条路一条是在 IDEA 里新建 Module 时选择Spring Initializr让向导帮你生成包含 spring-boot-maven-plugin、spring-boot-starter 依赖的模块骨架。这条路适合“从零开始搭新业务模块”比如要新建一个独立的微服务子模块。注意一点IDE 里很多 Spring Initializr 服务是联机的如果网络不好可能一直转圈。离线环境下选不了 Spring Initializr不如下面这条。另一条是选择普通Maven然后手动往 pom.xml 里补 Spring Boot 相关依赖。这条路看着麻烦其实更稳妥。因为你最终一定会手工调整 pom.xml与其让向导生成一堆没用依赖再删不如从一开始就明确知道自己加了什么依赖。我就是典型的前者用多了之后转向后者的因为向导生成的依赖版本有时候会跟父工程维护的统一版本冲突最后还是要手工改。如果想跟 IDEA 图形界面彻底脱钩纯命令行也能创建 module直接在父工程目录下手动建立文件夹把 pom.xml 和目录结构补全然后在父 pom 的modules里加上新模块的 artifactId。原理完全一样只是操作路径不同。下面这一章的步骤我就按“IDEA 图形界面 手动调整 pom”的混合方式来写这也是实际开发中最常用的组合。3. 新建 module 的完整实操流程3.1 准备一个 Maven 父工程作为聚合根第一步是创建一个父工程。在 IDEA 里新建项目类型选择 Maven不要勾选任何 archetype 模板Archetype 基本都是些过时还带敏感工程的模板空项目最干净按下图把 groupId、artifactId、version 填好。实际上这一步有两条常见路径File → New → Project → 选择 Maven → 填写com.demo、spring-multi-module、1.0.0-SNAPSHOT或者更推荐的方式先用 Spring Initializr 创建一个空的 Spring Boot 工程然后手动把它改造成父 pom。后者适合你本来就要用 Spring Boot 全家桶的场景因为父 pom 里可以直接继承spring-boot-starter-parent后续所有 starter 的版本都不用再写了。两条路我最后都走通过区别在于第二个方案更“Spring Boot 原生”版本管理省心。如果你的父 pom 不继承 spring-boot-starter-parent而是用自己的自定义父 pom那 Spring Boot 相关的依赖版本就得自己在dependencyManagement里维护工作量大很多。我的建议是父工程直接继承spring-boot-starter-parent子模块再继承这个父工程。这听起来有点绕——儿子继承父亲父亲又继承了爷爷 Spring Boot但这是完全合法的链式继承也是最常见的实践。这种多级继承下版本统一管理和子模块解耦可以同时实现。3.2 亲手创建一个 Maven 子模块父工程创建好后右键父工程 → New → Module弹窗里选择 Maven如果你是走纯 Maven 路径输入子模块的 artifactId比如ss-service。这时 IDEA 会帮你自动完成几件事在父工程目录下创建一个ss-service子目录生成一个pom.xml生成默认的src/main/java、src/main/resources目录结构注意如果你在 New Module 弹窗里选了 Spring Initializr那 Java 的包名会自动跟随 groupId 生成比如com.demo.ss_service这个包名很有迷惑性因为子模块的目录名是ss-service但包名里却带着下划线。后面 Spring 扫描包的时候容易记混建议还是手动改成com.demo.ss.service这类清晰的结构跟 artifactId 保持一致性。子模块刚创建完成时它的 pom.xml 里只有一个parent指向刚才的父工程?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdcom.demo/groupId artifactIdspring-multi-module/artifactId version1.0.0-SNAPSHOT/version /parent artifactIdss-service/artifactId packagingjar/packaging /project到这里“新建 module”这个动作就算完成了。接下来要做的就是往这个骨架里填东西。3.3 手动补全目录结构和 Spring Boot 启动类很多教程到这一步就结束了但实际开发里你还需要检查 src 目录结构是否完整。IDEA 新建的 Maven 模块有时不会自动生成src/main/resources你需要手动右键创建。这个目录下面放application.yml、logback.xml、mapper文件等。如果你的子模块是启动模块即内含main方法的模块那还需要创建一个启动类。这里有一个很容易让新人困惑的点每个子模块都可以有自己的启动类但真正只跑一个。比如ss-web模块里建一个WebApplication.javapackage com.demo.ss.web; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; SpringBootApplication(scanBasePackages com.demo.ss) public class WebApplication { public static void main(String[] args) { SpringApplication.run(WebApplication.class, args); } }为什么scanBasePackages要写com.demo.ss而不是默认的com.demo.ss.web因为默认的SpringBootApplication只会扫描启动类所在的包以及子包。如果你其他模块的 Bean 在com.demo.ss.xxx包下根本扫不到。把扫描范围扩展到共同父包这是多模块下最基础也最关键的配置。举个反例模块分别叫了com.demo.service和com.demo.web启动类在com.demo.web包下那么 service 模块里的ServiceBean 就永远不会被扫描到。处理办法有两个一是统一包名前缀推荐二是启动类上显式指定scanBasePackages指向各模块的包路径。还有一种情况有些项目里会把 service 模块也做成 Spring Boot 可启动模块用来本地单独调试这个服务。这本身没问题但要注意每个启动模块都会引入 spring-boot-starter-web 或 spring-boot-starter它们都是 spring-boot-maven-plugin 的触发点。如果只想让一个模块作为最终可执行 jar其他模块的 spring-boot-maven-plugin 一定要配置skiptrue/skip否则会导致 jar 冲突。3.4 把模块挂到父 pom 的 modules 列表里如果你用 IDEA 右键新建的子模块IDEA 会自动更新父 pom 的modules。但如果你是通过命令行或者手动建目录方式新建子模块这一步必须手动做。写得再隐蔽一点父 pom 里没有module声明的子模块即便在 IDEA 里能看到文件夹Maven 生命周期构建时也完全不会把它当成模块处理包括不会编译它、不会 install 它。这个问题会导致后面别的模块引用它时报“程序包不存在”。建议按顺序明确好 modules 的排列modules moduless-common/module moduless-dao/module moduless-service/module moduless-web/module /modules顺序上有讲究不一定要严格按依赖顺序但尽量把底层依赖模块写在前面。这样执行父工程mvn clean install时Maven 会按照 modules 声明的顺序和你配置的 reactor 逻辑来构建。虽然 Maven 具备根据依赖关系智能排序的能力但写好顺序毕竟更容易读懂项目结构少一些不可预期的行为。3.5 子模块之间如何互相引用模块建好之后下一步就是把它们串起来。以ss-web依赖ss-service、ss-service依赖ss-dao为例在各自 pom.xml 里分别加dependency!-- ss-web 模块 pom.xml -- dependencies dependency groupIdcom.demo/groupId artifactIdss-service/artifactId version${project.version}/version /dependency /dependencies注意这里的 version 用${project.version}意思子模块版本跟随父工程版本这样升级版本时不需要逐个模块手改。这是多模块项目里很实用的一个细节。依赖引用有两个容易踩的雷第一个是忘记在父 pom 的dependencyManagement里加版本。如果你用纯 Maven 方式创建子模块且子 groupId 与父工程不一致容易出现依赖解析慢甚至失败。建议所有内部模块的 version 都统一用${project.version}后续发版时只改父 pom 的 version 一处。第二个是依赖了IDEA 却提示找不到类。这种情况多半是 IDEA 的缓存问题。Maven 面板里点一下 Reload All Maven Projects通常能解决。如果还不行把 IDEA 的 Maven 仓库设置核对一下特别是本地仓库路径是否与配置一致。3.6 验证多模块结构是否正确验证结构是否正确最直接的办法就是执行一次 Maven 构建。在 IDEA 右侧 Maven 面板里选中父工程执行 clean install。构建日志里会依次打印所有模块的构建结果[INFO] Reactor Summary for spring-multi-module 1.0.0-SNAPSHOT: [INFO] [INFO] spring-multi-module ......................... SUCCESS [INFO] ss-common .................................. SUCCESS [INFO] ss-dao ..................................... SUCCESS [INFO] ss-service ................................. SUCCESS [INFO] ss-web ..................................... SUCCESS看到 Reactor Summary 这几行说明你的多模块结构从 Maven 视角看是成立的接下来才开始真正写代码。4. 模块间依赖与 Spring 容器整合的细节4.1 一次完整的依赖传递过程用一个例子走一遍ss-dao模块里有一个UserMapper接口ss-service模块里有一个UserService类它通过Autowired注入UserMapper。这里有一个关键认知ss-service即使依赖了ss-dao由ss-dao传递而来的第三方依赖在ss-service的 classpath 里是可以直接用的。比如ss-dao里引入了 MyBatis 相关依赖ss-service的代码里能引用 MyBatis 的注解吗答案是能。因为 Maven 的依赖具有传递性只要ss-dao里的依赖没有标记optionaltrue/optional就会一级一级传下去。那为什么要管这件事因为依赖传递会造成不可见的版本冲突。ss-service可能用到HttpClient4.5 版本而ss-dao间接依赖了HttpClient4.4Maven 默认的最近依赖原则会让你搞不清到底生效的哪个版本。排查的时候第一件事不是翻 pom而是看 IDEA 的 Maven 面板里 Dependencies 那一栏右键 Show Dependencies 可以弹出依赖图。依赖图比 pom 文本直观一百倍。4.2 Spring 容器怎么跨模块装配 Bean模块建了、依赖也加了但 Spring 容器能装配到其他模块的 Bean 吗取决于包扫描。以前面SpringBootApplication(scanBasePackages com.demo.ss)这种配置为例Spring 启动时会在 classpath 下扫描com.demo.ss及下面所有子包里的Component、Service、Repository、Controller等。这里要理解一个底层机制Spring 扫描的是 classpath 下的类跟它们来自哪个模块没有任何关系。也就是说只要ss-service被ss-web依赖它的 class 文件就在当前 classpath 里就能被扫描到。所以模块间 Bean 装配失败原因无非两个一个是不在扫描路径下另一个是模块间没建立依赖关系。排查的时候可以先看 Maven 面板里ss-web的 dependencies 有没有ss-service再看启动类的 scanBasePackages 能不能覆盖 service 的包。这里补充一个细节如果用SpringBootApplication时没有指定scanBasePackages它的默认值会取启动类所在包路径。比如启动类在com.demo.ss.web包下扫描范围就是com.demo.ss.web.含子包。这种情况下 service 模块如果放在com.demo.ss.service是扫不到的。把各模块统一放在同一个根包下是最省心的做法。4.3 模块间的事务管理如何配置多模块下事务是个常踩的坑。很多人习惯在启动模块的配置类上加EnableTransactionManagement但事务最终作用于 service 模块的代码上如果 service 模块自己不是启动模块事务配置能生效吗结论是能只要你用 Spring Boot且在启动类所在的模块配置了事务管理这个配置就会作用于整个应用上下文。因为EnableTransactionManagement是对容器全局生效的不区分 Bean 在哪个模块定义。真正要小心的是AOP 增强类跨模块失效的问题。比如ss-common里定义了一个Aspect切面在ss-service模块中使用。如果ss-common被ss-service依赖切面类能不能生效能但有一个条件ss-common必须在ss-service的包扫描路径范围内。如果你只Import了它而没扫描到它Spring AOP 无法自动识别这个切面类。换成大白话被依赖模块中的 Aspect 类需要在启动模块的组件扫描覆盖范围内否则你切不进去。这个坑特别隐蔽因为启动不会报错只是某个方法没有走切面逻辑。排查时可以在切面方法里打个日志确认是否进切面再逐个检查包扫描范围。4.4 子模块各自的配置文件怎么处理多模块项目的配置文件不全在各子模块自己的 resources 里大多数情况下application.yml 只需要放在启动模块里。比如启动模块是ss-web那么ss-web/src/main/resources/application.yml就是全局配置入口其中可以配置 MyBatis 扫描路径、Redis 连接、数据源等。其他模块如果需要自己的配置比如ss-dao有独立的 mapper 文件通常有两种处理方式把 mapper XML 放在ss-dao/src/main/resources/mapper/下启动模块的配置里加mybatis.mapper-locations: classpath*:/mapper/**/*.xml。注意这里要写classpath*:而不是classpath:因为classpath:只会搜索第一个匹配的 jar 包而classpath*:会搜索所有 jar 包的所有匹配路径。多模块场景下mapper XML 在ss-dao的 jar 里必须用classpath*:才能找到。模块间共享的配置项放到ss-common的 resources 里比如common-config.yml在启动模块的application.yml里用spring.config.import导入或者通过PropertySource加载。Spring Boot 2.4 之后推荐spring.config.import这种方式。我跟不少同事交流过很多人早期配置上的奇奇怪怪错误一半以上是classpath:和classpath*:没分清。多模块环境下动静态资源跨 jar 访问的情况很多一律用classpath*:准没错。5. 打包构建与问题排查实录5.1 mvn install 的顺序为什么必须正确多模块工程构建时内部模块的依赖必须被安装到本地 Maven 仓库其他模块才能引用到。这意味着单纯执行mvn compile不会把模块 jar 安装到本地仓库只有mvn install才会。所以在 IDE 里执行整个项目的正确姿势是对父工程执行clean install或者直接执行mvn clean install。这样 Maven 会按 Reactor 顺序先构建ss-common、再ss-dao、再ss-service、最后ss-web并且每一步 install 的时候都把 jar 安装到本地仓库供下一个模块引用。有一个场景非常容易出错你在某个子模块里改了代码然后单独对另一个子模块执行 run。比如你改了ss-service的代码然后去ss-web里按运行按钮IDE 可能不会自动重新 installss-service的改动。你要么在 Maven 面板里先对ss-service执行 install要么干脆使用 IDEA 的Build Project对整个项目做增量编译。最省心的做法是在Run Configuration里把Before launch步骤加一个 Maven 的clean install。但这样每次启动很慢所以我更推荐开发时先把整个父工程 install 一次后面改动不涉及模块间接口的情况下IDE 的增量构建就能用。第二个常见问题是ss-web打包时提示实现类找不到比如NoClassDefFoundError。这个错误和ClassNotFoundException不太一样后者是编译期类路径也没找到前者是编译期有运行时却没了。多模块环境下出现NoClassDefFoundError多半是运行时你只把ss-web打包了而没有附带它依赖的ss-service、ss-dao这些内部 jar。Spring Boot 最终打成 fat jar 时会把它们带进去但如果你配置文件不对也可能漏掉。排查时直接jar tf xxx.jar看里面有没有其他模块的 class。5.2 spring-boot-maven-plugin 在哪个模块配置这是多模块打包最容易踩的坑之一。spring-boot-maven-plugin的repackage目标会把模块打包成可执行 fat jar它要求该模块必须是带main方法的启动类。如果你在子模块ss-common、ss-service上也配置了这个插件它们的 jar 也会被 repackage 成 fat jar而这种 fat jar 里的类结构是被重组织过的BOOT-INF/classes别的模块引用它时根本找不到对应的类。我见过最典型的现象ss-service里放了 spring-boot-maven-plugin然后ss-web依赖ss-service编译没问题运行时各种ClassNotFoundException最后排查发现是ss-service被repackage了class 全部跑到了BOOT-INF/classes下面。正确姿势是父 pom 的pluginManagement中统一声明 spring-boot-maven-plugin然后在真正需要打成可执行 jar 的模块通常是 web/启动模块里才激活使用。或者给非启动模块配置skiptrue/skip。代码如下plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration skiptrue/skip /configuration /plugin这也是为什么很多前辈说“多模块项目里能打成可执行 jar 的只有入口模块其他模块一律是普通 jar”。模块划分得越细这个规则越重要。5.3 内部模块版本管理与 release 流程多模块项目的版本号统一也是一个实操重点。前面提到用${project.version}来引用内部模块那升级版本时只需改父工程一个 version。比如父工程1.0.0-SNAPSHOT三个子模块都是1.0.0-SNAPSHOT。发版时先改父 pom 的 version 为1.0.0-RELEASE子模块全部跟随变化。使用 Maven 的versions-maven-plugin可以自动完成这个版本切换mvn versions:set -DnewVersion1.0.0-RELEASE这个命令会把所有模块的 version 都改成目标版本并自动更新模块间的引用关系。执行完检查一下 pom 的改动没问题就提交。这里踩过一个坑如果子模块里某个依赖写死了version是旧版本号执行 versions:set 之后它不会被自动更新。所以内部依赖永远用${project.version}这是硬性建议。还有一种场景某个子模块已经独立发布到远程仓库给其他系统用了版本号跟父工程不同步。这种模块建议从父工程的modules中移除单独管理。不然父工程一升级版本这个模块也被迫升级容易破坏其他系统的稳定性。5.4 常见问题速查表多模块开发中的问题很集中把常见问题整理成下面这张速查表定位的时候先对着看一遍问题现象可能原因解决思路另一个模块的类找不到编译期子模块没加dependency检查目标模块 pom加上对应依赖编译提示找不到符号但依赖已经加了IDEA Maven 缓存未刷新点击 Maven 面板 Reload All ProjectsBean 找不到NoSuchBeanDefinitionException启动类扫描范围没覆盖该模块调大scanBasePackages指向共同父包运行时 ClassNotFoundException依赖方打包漏掉了内部模块 jar检查 spring-boot-maven-plugin 配置确认非启动模块 skip打包提示没有主清单属性启动模块未配置 spring-boot-maven-plugin在启动模块 pom 中激活 repackage 目标配置文件里的 mapper 找不到mapper XML 在其他模块 jar 里使用classpath*:/mapper/**/*.xml依赖版本冲突行为莫名其妙传递依赖导致的版本覆盖用 Maven 依赖图分析在父 pom 里强制指定版本内部模块改动后运行其他模块没生效子模块改动未 install先 install 该模块再启动依赖方5.5 几个我从实际项目里攒下来的经验再展开几个建议这些都不太好从官方文档里学到。第一点子模块的 artifactId 用纯小写中划线风格。比如ss-common不要用ss_common或SsCommon。包名可以用com.demo.ss.common但 artifactId 保持中划线Maven 对这块有约定。IDEA 会自动帮你在目录名中使用与 artifactId 一致的名字这个一致性不要打破你后续的构建脚本和 CI 配置都会少很多字符串替换的坑。第二点不要把 spring-boot-starter 相关依赖滥用。很多新手建模块时贪多一下把所有 starter 都塞进去做成公共模块。比如在ss-common里引入spring-boot-starter-web结果每个依赖它的模块都被动引入了 Web 容器内存开销、端口冲突、启动速度全受影响。原则是你的模块真正需要 Web 能力才引入 web starter。第三点新拉取项目后的第一件事不是写代码是先在父工程执行一次mvn clean install。这一步会验证整个项目结构是否完整同时把内部依赖灌装到本地仓库。很多人直接从仓库 clone 代码在 IDEA 里苦等依赖下载然后发现一堆红叉最后发现是本地仓库缺内部模块的快照安装一次就全部消失。第四点如果需要把一个单体应用改造成多模块不要一口气拆。先把不依赖业务上下文的通用模块拆出来比如 common 模块再拆数据库层、服务层、接入层。每拆一层就整跑一次测试。只要测试不蹦再继续下一步。宁愿多花几天时间循序渐进也别一次性把代码分成七八个模块结果连环报错无从下手。6. 几个值得反复思考的细节6.1 “新建 module”和“新建独立服务”的区别很多教程把“新建 module”和“新建一个 Spring Boot 服务”混在一起其实完全不是一回事。module 实际上只是 Maven 工程中的一个子项目它的存在有两种可能它是一个普通 jar 包模块被其他模块引用比如 common、service。它是一个可启动服务的聚合模块带 main 方法和打包插件。如果需求是“给现有微服务系统新增一个下游服务”那你新建的 module 应该是一个带 main 方法的 Spring Boot 启动模块而且必须有独立的端口、独立的配置、独立的打包命令。如果只是出于代码结构的管理你新建的模块往往只是 jar 库模块不直接对对外暴露任何接口。这个区分要在动手前想清楚因为它直接决定你模块 pom 里的 packaging 类型和插件配置。6.2 模块划分粒度的经验模块不是越多越好。我见过把“一个 Controller、一个 Service、一个 Mapper 就拆成模块”的工程pom 文件比业务代码还多光是反应构建顺序就要花半天。模块划分的核心原则是按业务变更的频率和边界通用工具、基础组件 → 独立模块数据访问层的实体和 Mapper → 独立模块尤其是使用 MyBatis 时核心业务域 → 一两个聚合模块就够Controller 层和启动入口 → 独立模块整体控制在三到七个模块之间比较合理。超过十个模块项目的阅读成本、构建成本都会显著上升。如果你的功能边界足够清晰可以在微服务级别拆独立服务而不是全部塞在一个单体多模块项目里。6.3 从 IDEA 的 Maven 面板能看出什么IDEA 里右侧 Maven 工具窗口对多模块项目的可视化很直观。父工程会以粗体显示下面带着所有子模块。展开某个子模块Lifecycle 里有 clean、validate、compile、test、package、install、deploy 等常用生命周期阶段。随时展开看看当前模块有哪些依赖点刷新图标重新构建。有个很实用的操作双击执行父工程 → Lifecycle → install相当于整个项目做一次本地构建。右键父工程选择Run Maven Build打击更快。新人问得最多的“为什么我执行了 install 还是报找不到类”大概率是没看 IDEA 底部的 Build 输出里面已经写了某个模块 build 失败的原因先看输出再动手。6.4 多模块下的测试策略最后提一下测试。多模块项目里单元测试应该写在各个子模块内部比如ss-service/src/test/java里测试 service 逻辑ss-dao/src/test/java里测试 mapper 层。因为每个模块只关注自己的职责测试代码才能真正做到“给定输入、验证输出”的全过程。跨模块的集成测试通常放在启动模块里因为那里才能把整个容器拉起来。如果某个子模块的执行测试需要用另一个模块的 mock 对象建议把测试公共类和 MockUtil 放在ss-common的src/test/java下然后其他模块在 pom 里加一个 special type 为 test 的依赖。如果你不需要跨模块共享测试类最简单的办法就是把测试放在启动模块里。但记住一点测试代码和业务代码要分开放永远不要让测试类进入生产 jar 包。多模块项目里测试报错最常见的问题就是“测试类找不到某个 Bean”除了扫描问题还有一个原因是 Spring 测试上下文没有加载其他模块的配置。这种情况建议在测试类上加SpringBootTest和ActiveProfiles(test)测试专用配置文件放在启动模块的src/test/resources里。别忘了配好 test 数据源别把测试数据写到生产库里。7. 收尾一点个人体会多模块的坑说到底都是“看不见的类路径”和“看不见的依赖范围”造成的。你在 IDEA 里写代码时感觉不到模块边界但 Maven 和 Spring 在后台默默受这些边界约束。新建 module 本身很简单难的是意识到这个边界在哪里、怎么维护它。如果让我给新人一个最实在的建议建完模块后先别急着写业务代码花二十分钟把父 pom 和子 pom 读明白在 Maven 面板里看一遍依赖关系再手动执行一次 install。把这三件小事做完你对多模块项目的掌控感会完全不一样。这套东西踩过的坑、趟过的雷写下来就是你在 Spring 上真正进阶的开始。