资讯动态

Spring Cloud 父工程 packaging 必须用 pom?多模块 Maven 骨架搭建详解

发布时间:2026/10/20 19:12:41 来源:尧图企业网站定制
做 Spring Cloud 微服务项目第一步往往不是写业务代码而是把父工程的骨架搭对。很多人第一次接触多模块工程时都会对父项目的packaging到底该填什么感到困惑填jar感觉怪怪的填pom又不太明白为什么直接不写反正默认值好像也能跑。这些疑问背后藏着一个很关键的知识点在 Spring Cloud 体系中父项目打包类型的定义直接决定了整个多模块工程能不能被正确识别、统一管理和顺利构建。这篇文章我会把父项目打包类型的原理、定义方式、常见坑和实际工程里的取舍逻辑一次性讲透适合刚接触微服务架构拆分、或者正在为某个中型项目搭建多模块基础设施的开发者参考。1. packaging 三种取值pom、jar、war 到底分别代表了什么要理解父项目为什么必须定义成某种打包类型先要把 Maven 里packaging这一项的含义理清楚。它不是个随便填填的元数据而是直接影响构建产物、生命周期和模块间依赖关系的核心开关。1.1 jar 是默认值但默认值恰恰是父项目最不该用的东西如果在pom.xml里不写packaging标签Maven 会默认当作jar处理。也就是说父项目会被当作一个普通的 Java 库项目执行mvn package时会走compile、test、package这一整套生命周期最后在target目录下生成一个可被其他模块引用的 jar 包。问题在于一个被设计成只做依赖声明和统一配置的父工程它的src/main/java目录往往是空的或者根本没有这个目录。如果你把父项目定义成jar每次构建它时都会去做 Java 源码编译、单元测试扫描、打包等操作。在只有pom.xml文件、没有任何代码的情况下这套流程多数时候也能跑完但它暗示了一个错误的方向父项目是参与业务构建的一个模块。真正要继承父工程配置的子模块通常只会使用parent标签去引用它并不会把这个父项目当作一个 jar 依赖引入到 classpath 中。把父项目定义为jar除了会产生一个无意义的 jar 文件之外还会在某些构建环境下引发“父项目无法被正确 install”或者“子模块无法解析父项目”这类连锁问题。1.2 war 是 Web 应用专用和微服务父工程完全不沾边war打包类型面向的是传统的 Servlet 容器部署场景产物是一个可以被 Tomcat 等容器自动解压部署的 Web 应用压缩包。Spring Boot 项目如果非要部署到外部容器可以把自身打成 war 包这属于特殊情况。但放在父项目这个位置war是最没有意义的选择。父项目没有 Web 页面、没有 Servlet 初始化类、也没有需要容器管理的资源打成 war 包除了能证明“这个项目确实被构建过一次”之外没有任何实际价值。更麻烦的是子模块在继承父工程时如果父项目是war某些版本的 Maven 插件会对没有src/main/webapp目录的项目发出警告甚至直接构建失败。1.3 pom 才是父项目真正该用的类型聚合与继承的前提当packaging被设置为pom时Maven 会把这个项目当作纯粹的 POM 描述工程。它不再执行 Java 编译和打包流程而是专注于两件事一是聚合子模块modules二是为子模块提供可继承的配置依赖管理、插件管理、属性定义等。从构建顺序的角度看pom类型项目在mvn package时不会生成 jar 文件但依然会执行完整的生命周期校验。它存在的意义从“输出一个产物”转变成了“组织一批产物”。换句话说父项目定义成pom等于向 Maven 声明我是一份工程说明书不要试图把我编译成任何东西我的价值在于告诉所有子模块“你们应该长什么样”。打包类型构建产物是否适合作为父项目典型使用场景jar可被 classpath 引用的 Java 库不适合业务模块、公共代码库warWeb 应用压缩包不适合传统 Servlet Web 应用pom无物理产物只做工程描述最适合多模块聚合工程、父工程、调度中心这里有一个容易忽略的细节当子模块通过parent继承父工程时Maven 会根据relativePath找到父 POM 文件如果找不到会去本地仓库里找。父项目只有 install 到本地仓库之后其他机器或 CI 环境才能稳定地解析到它。pom类型的项目在mvn install时同样会被安装到仓库里只是它以.pom文件的形式存在而不是.jar文件。这点不懂的人很容易搞混——以为“反正父项目不打包不在乎 install”结果在 CI 机器上构建子模块时报错说无法解析父 POM原因就是父工程从来没有发布到仓库。2. 在 Spring Cloud 工程中定义父项目打包类型的具体步骤和配置明白了三种类型的本质区别接下来就可以实操了。父项目打包类型的定义不是单独在某个地方勾选一下就行它要和整个工程的模块结构、依赖管理策略配合起来才算完整定义。2.1 从零创建一个类型的父工程假设你手上有一个全新的 Spring Cloud 微服务项目想把它拆成gateway-service、order-service、user-service三个业务模块外加一个common公共模块。第一件事不是急着建模块而是先创建一个空目录作为父工程的工作目录在里面放一个pom.xml。这个父 POM 的最初形态类似下面这样?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 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdmicroservice-parent/artifactId version1.0.0-SNAPSHOT/version packagingpom/packaging properties java.version17/java.version spring-cloud.version2023.0.1/spring-cloud.version spring-boot.version3.2.5/spring-boot.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring-boot.version}/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version${spring-cloud.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement /project这段配置里最关键的一行就是packagingpom/packaging。它和groupId、artifactId、version一起定义了父工程的 GAV 坐标同时也是子模块parent引用的依据。注意看dependencyManagement里的写法引入 Spring Boot 和 Spring Cloud 的 BOMBill of Materials时typepom/type和scopeimport/scope两个标签缺一不可。这是父工程做依赖版本统一的最常见手段子模块在声明依赖时就不需要写版本号了父工程的依赖管理会自动帮它确定版本。2.2 声明 modules让父工程真正承担聚合职责只有packagingpom/packaging还不够父工程还需要通过modules标签把自己要管理的子模块一一列出来。这样执行mvn install时Maven 会自动按照模块间的依赖关系排序构建先构建common再构建依赖它的order-service和user-service最后构建gateway-service。modules modulecommon/module modulegateway-service/module moduleorder-service/module moduleuser-service/module /modules每个module标签里填的是子模块目录的相对路径。Maven 在解析时会去这些目录下找对应的pom.xml。这里有个很多人栽过的跟头父工程里写了modules子模块自己也写了parent但目录结构对不上比如父 POM 在仓库根目录子模块却在services/order-service这种二级目录里。这时候如果在services/order-service目录下单独执行mvn installMaven 会尝试通过../..找父 POM如果找不到就尝试从本地仓库里解析。为了避免这种混乱我在实际项目里一般会把所有模块放在父目录的一级子目录下并且保证子模块 POM 的relativePath指向正确位置。2.3 新建子模块时锁定父工程坐标子模块的pom.xml里parent段必须正确引用父工程的 GAV 坐标同时指定relativePath。以order-service为例parent groupIdcom.example/groupId artifactIdmicroservice-parent/artifactId version1.0.0-SNAPSHOT/version relativePath../pom.xml/relativePath /parent artifactIdorder-service/artifactId packagingjar/packaging注意这里子模块的packaging要写jar因为它是一个真正会产出可执行 jar 的 Spring Boot 应用。子模块的打包类型不受父工程pom类型的限制它们可以各自独立定义。从继承机制的角度看子模块会从父工程继承dependencyManagement、properties、pluginManagement等配置。但有一个细节要留意父工程中的所有dependencies不是dependencyManagement里的都会被所有子模块无条件继承。这也意味着父工程里那些“所有模块都必须用到的依赖”比如日志门面、公共工具包可以放在dependencies里而那些只是统一版本、不一定每个模块都用到的依赖必须放在dependencyManagement里。2.4 验证定义结果是否生效配置写完后在父工程目录下执行mvn clean install如果一切正常你会看到 Maven 依次构建common、user-service、order-service、gateway-service并且父工程本体会以microservice-parent-1.0.0-SNAPSHOT.pom的形式安装到本地仓库。如果你想更清楚地确认父工程定义是否正确可以单独执行mvn help:effective-pom这个命令会将父工程和每个子模块合并后的完整 POM 输出出来。通过它你能清楚地看到dependencyManagement里的版本约束是否被正确继承子模块最终生效的配置是什么。3. 为什么父项目只能是 pom依赖管理、插件管理与属性继承的底层逻辑父项目定义成pom看似只是改一个标签实际背后牵涉 Maven 多模块构建的整套运作机制。只有理解了这套逻辑你才能在做工程化改造时真正游刃有余而不是遇到问题就靠搜答案碰运气。3.1 dependencyManagement 的版本锁定机制在微服务项目中版本管理是最容易出问题的环节。假如没有父工程的dependencyManagement每个子模块都自己指定 Spring Boot 版本、Spring Cloud 版本、数据库连接池版本结果就是各模块依赖同一组件不同版本运行期出现奇怪的冲突。而packagingpom/packaging的意义在于它让父工程成为“版本锁”的载体。子模块继承父工程后声明依赖时可以省略版本号Maven 沿着继承链向上找最终在父工程或父工程 import 的 BOM 里确定版本。这种机制的工作方式可以用一个生活类比来理解父工程像一份公司统一制定的采购清单上面写清楚所有办公用品推荐用什么品牌和规格。每个子模块像不同部门需要什么物品就按清单去领不需要在每个部门墙上贴一遍规格表。部门不会因为采购清单的存在而自动获得全套设备只有它主动申请时才会领取具体物资。对应到 Maven 里就是dependencyManagement不是dependencies它不会把依赖强塞给子模块只有在子模块声明了某个依赖但没写版本号时它才会帮忙补全版本。如果你把父项目定义成jar而不是pom这套机制仍然在技术上可行但含义就变味了父工程发布到仓库的是一个 jar 包而非一份“纯规格说明”子模块在解析父工程时Maven 会多检查一层“父工程是否真的被编译过”的无意义状态。最明显的问题是一个只有 POM 没有代码的 jar 父工程在执行一些代码质量扫描插件或依赖分析插件时会被判定为异常项目触发无意义告警。3.2 pluginManagement让插件配置只写一遍除了依赖版本插件配置同样面临“每个模块重复写”的问题。比如你想让所有子模块都使用统一版本的maven-compiler-plugin统一设定 Java 17 编译级别或者统一使用spring-boot-maven-plugin来支持 Spring Boot 的可执行 jar 打包。在父工程中可以通过pluginManagement定义插件的版本和公共配置build pluginManagement plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId version${spring-boot.version}/version /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.13.0/version configuration source17/source target17/target encodingUTF-8/encoding /configuration /plugin /plugins /pluginManagement /build这个配置的核心逻辑是pluginManagement只注册不激活子模块需要某个插件时在buildplugins里声明插件版本和基础配置从父工程自动带下来。这样做的好处是升级某个插件的版本时只需要改父工程一处所有子模块统一生效。父项目packagingpom/packaging与pluginManagement是天然搭配。因为父工程本人不会被编译所以它不需要真正执行maven-compiler-plugin的编译目标只是在配置文件层面做一个“统一约定”的声明。如果父工程是jar类型pluginManagement里的编译器配置还会在父工程自身构建时被触发执行这就是一大类“父工程构建比想象中慢”问题的根源。3.3 属性的传递一个 properties 贯穿所有模块多模块工程里版本号、环境标识、路径配置这类属性如果散落在各子模块很难维护。父工程可以通过properties定义公共属性被所有子模块继承。properties java.version17/java.version project.build.sourceEncodingUTF-8/project.build.sourceEncoding spring-boot.version3.2.5/spring-boot.version spring-cloud.version2023.0.1/spring-cloud.version mysql.version8.3.0/mysql.version /properties子模块里直接使用${spring-boot.version}就能引用父工程定义的属性值。这种机制配合dependencyManagement基本能实现版本统一管理的全覆盖。同样属性继承也要求父工程是pom类型。如果打包类型不是pomMaven 在解析父工程时内部的属性解析逻辑虽然不会报错但一旦父工程被某个子模块以 jar 依赖方式引用属性的隔离边界就会变得模糊容易引入不必要的继承链复杂度。3.4 生命周期差异为什么 pom 项目不会被编译和打包父工程定义成pom之后它的生命周期行为和jar项目有一个明显区别执行mvn package时jar项目会执行compile、test、package阶段而pom项目会直接跳过这些中间环节只执行到它能力范围内的阶段。这个设计其实很合理编译和打包动作需要源码目录存在而父工程这类工程的存在目的不是产出可执行代码。定义成pom等于明确告诉 Maven请把我当作一个纯粹的元信息项目不要试图在我的目录里寻找 Java 源码。我见过一些团队因为当初没想清楚把父项目定义成了jar项目本身没有src目录但构建时因为 Maven 的默认插件配置触发告警。虽然不一定构建失败但每次构建日志里都带着红色警告CI 系统里看着特别刺眼。后来改成pom干净利落告警全部消失。这个一秒钟的改动解决了一个困扰了很久的“小问题”。4. 在 Spring Cloud 微服务工程里父项目打包类型最常见的几类坑理论讲完来点实际的。下面这些坑是我在多个团队的实际项目里观察到的有些是新手常犯有些是经验丰富的人也会被绕进去的。我会把这些问题的完整排查链路写出来方便你遇到同类情况时直接对照排查。4.1 坑一只改了子模块的 parent 指向却忘了把父工程 install 到本地仓库现象描述子模块报错“Non-resolvable parent POM: Could not find artifact com.example:microservice-parent:pom:1.0.0-SNAPSHOT”。这个问题在单独构建某个子模块时特别常见。父工程刚创建时你在 IDE 里打开了gateway-service然后直接执行 Maven 构建。IDE 可能会根据相对路径找到父 POM但命令行里的 Maven 不会像 IDE 那样智能它必须通过本地仓库或远程仓库来解析父工程坐标。其实问题不是出在parent的写法上而是出在父工程没有被安装过。解决路径非常清晰在父工程目录下先执行一次mvn install确认本地仓库的com/example/microservice-parent/1.0.0-SNAPSHOT/目录下有microservice-parent-1.0.0-SNAPSHOT.pom文件再回到子模块目录执行构建很多老手会说“我明明写了relativePath../pom.xml/relativePath怎么还报错”其实relativePath只在 Maven 能准确定位父工程 POM 文件时发挥作用。如果父工程和子模块在同一个 Git 仓库正常 checkout 后relativePath应该能解决问题。但如果是用 CI 构建时只拉取了子模块代码或者父工程单独发布、子模块单独维护那么父工程没有发布到仓库relativePath也救不了你。这个问题的本质是relativePath解决的是“源码目录中能找到父 POM”的场景而 Maven 构建最终还是要把父工程当作依赖解析到本地仓库里。养成“父工程改动后立即 install”的习惯能避免绝大多数父工程解析问题。4.2 坑二父工程错把通用依赖写在 dependencies 里导致每个子模块都被污染这是一种隐蔽性较高的坑。假设父工程里写了dependencies dependency groupIdcom.example/groupId artifactIdcommon/artifactId version1.0.0-SNAPSHOT/version /dependency /dependencies这个写法本身没错common模块确实是所有模块都要用的公共代码。但问题是gateway-service这样的网关模块也许需要common但如果另有某个独立的纯工具模块并不需要common里的哪些类它依然会把common的依赖带上无形中增加类库体积甚至可能触发依赖冲突。更严重的场景是父工程把某个数据库驱动或消息队列 client 写进了dependencies但只有部分业务模块需要连接数据库。结果所有模块——包括根本不需要数据库的模块——都被强制引入了数据库驱动依赖。不仅仅是 jar 包体积变大更危险的是在某些 Spring Boot 自动配置场景下类路径下存在数据源相关依赖会触发自动配置导致应用启动时尝试初始化数据源然后失败。排查这个问题的思路是确认父工程的dependencyManagement和dependencies两个区段分别写了什么执行mvn dependency:tree查看某个不需要该依赖的模块确认它是否意外带上了把那些“只有部分模块需要的依赖”从父工程dependencies迁到dependencyManagement中然后在真正需要的子模块里声明依赖不写版本号即可这个问题的根源还是对packagingpom的语义理解不够深刻父工程定义成pom意味着它是一个“约定”层而不是“依赖注入”层。约定可以统一但具体每个模块需要什么还是要让模块自己做决定。4.3 坑三子模块把 packaging 也写成了 pom导致无法生成可执行 jar这种坑一般发生在照搬父工程模板的时候。某个开发者习惯了父工程里的packagingpom/packaging新建子模块时直接复制模板忘了把packaging改成jar。现象是子模块的target目录下确实有classes文件夹和编译产物但没有生成 Spring Boot 的可执行 jar。而且由于spring-boot-maven-plugin的repackage目标没有执行即使你用java -jar去运行也会提示 jar 中没有主清单属性。排查路径查看子模块pom.xml里的packaging标签如果写的是pom改成jar同时确保spring-boot-maven-plugin在子模块的buildplugins里被声明因为父工程的pluginManagement默认只是管理版本不会自动执行重新执行mvn clean package还有些更复杂的版本问题某些子模块如果被另外的子模块依赖自身又需要打包成可执行 jar同时还要作为普通 jar 被依赖这时候直接打包成可执行 jar 会影响依赖解析。但那是另一个话题了和本次的父项目打包类型定义关系不大。4.4 坑四父项目的 packaging 被漏写默认 jar 导致聚合构建顺序异常还有一种非常容易犯的错父工程的pom.xml里忘了写packaging标签默认值jar生效。这时如果你执行mvn install父工程会尝试按 jar 项目方式构建。因为它没有源码目录默认编译器会跳过但 Maven 还是会把它当作一个 jar 项目安装到仓库。真正的问题出现在子模块聚合构建时。如果父工程没有正确声明pom类型某些 Maven 插件在解析工程模型时会把它判定为“父工程是 jar 但子模块通过 parent 引用它”这种模型在某些严格的插件配置下会直接报错。更常见的表现是一些依赖分析工具比如maven-dependency-plugin的某些目标在分析聚合工程时会警告父工程存在异常。我遇到过一个实际案例某团队用 Jenkins 构建微服务工程父工程忘了写packaging标签子模块一直正常构建但后来加了maven-enforcer-plugin后构建开始失败。排查了一整个下午最后才发现是父工程packaging缺失导致模型状态不一致。加上packagingpom/packaging之后问题立即消失。这个坑的教训是创建父工程时顺手把packaging写上不要依赖默认值。默认值是为独立 Java 项目设计的不是为聚合工程设计的。5. 实战中父项目打包类型定义的经验总结与扩展建议光知道操作步骤还不够实际项目里会遇到一些边缘情况和特殊场景。这里分享一些我在实践中沉淀下来的判断逻辑。5.1 如何判断一个工程该不该用 pom 打包类型有三个信号能帮你快速判断这个工程是否有modules标签列出多个子模块这个工程是否只被其他模块通过parent引用而从未被当作dependency引入这个工程是否有真实的src目录和 Java 代码三个信号只要满足一个那这个工程都大概率应该使用pom打包类型。尤其是第二个信号它是很多人容易搞混的点一个工程被其他模块当作 parent 引用时它通常是pom类型被当作依赖引用时它通常是jar类型。注意这里有个灰色地带common这类公共代码模块它可能既被很多兄弟模块依赖又和父工程一起被打进聚合构建中。这种情况下common的打包类型应该是jar而不是pom。它不是一个纯粹的配置描述工程它是真的能产出代码的模块。5.2 在公共模块的 package 类型选择上不要惯性复制父工程配置我观察到一个普遍现象当团队刚把父工程新建好开始创建第一个公共模块时很多新人会照着父工程的pom.xml改artifactId其他所有配置原样保留。结果公共模块也被定义成了pom后来想写一些公共工具类时发现src/main/java里的代码不会被编译打包。这在 Spring Cloud 微服务工程里尤其危险注册中心、配置中心、公共模块这些都需要将代码打成 jar 供业务模块依赖如果打成pom类型业务模块在dependency里引用它时会解析失败整个工程直接编译不过。所以我的习惯是父工程统一是pom所有能产出代码的模块一律显式jar绝不复制粘贴父工程标签。5.3 多级父工程的场景下packaging 传递关系不要想当然大型微服务工程有时会拆出“基础父工程”和“业务父工程”两层。基础父工程管理公司级的依赖基线业务父工程继承基础父工程同时管理某个业务域内各子模块的公共配置。这种场景下基础父工程的packaging是pom业务父工程的packaging也必须是pom。因为业务父工程仍然扮演的是“描述工程”角色它不会产出可执行代码。如果你把业务父工程写成jar它在继承基础父工程时虽然不报错但会在本地仓库里生成一个没有意义的 jar 包同时它自身的子模块在依赖解析时会多一层不必要的模型判断。多级继承时另一个要注意的点是relativePath链的完整性。基础父工程被作为业务父工程的 parent 时业务父工程中间层如果要通过relativePath指向基础父工程路径计算要特别仔细。如果层级变深强烈建议先将基础父工程install到本地仓库依靠 GAV 坐标解析而不是一层层用相对路径找避免路径错误导致解析失败。5.4 关于 packaging 定义与 Spring Boot 可执行 jar 的关系最后提醒一点父项目定义成pom不影响 Spring Boot 子模块生成可执行 jar。子模块如果用到spring-boot-maven-plugin它会在package阶段对生成的 jar 做repackage产生一个带BOOT-INF/classes结构的可执行 jar。父工程负责统一控制插件的版本但真正执行 repackage 的还是子模块自己。还有一批工程会碰到 Spring Cloud 与 Spring Boot 版本不兼容的问题这往往不是packaging能解决的而是dependencyManagement里 import 的 BOM 版本搭配不对。比如 Spring Boot 3.2.x 对应 Spring Cloud 2023.0.x如果你的父工程里两个 BOM 版本不匹配子模块编译期会出现各种 class 找不到的诡异报错。这时候不要怀疑packaging配置去查版本矩阵才是正确方向。最后分享一个我在实际工作中推荐的骨架microservice-parent (packagingpom) ├── common (packagingjar) ├── gateway-service (packagingjar) ├── order-service (packagingjar) └── user-service (packagingjar)父工程统一管理依赖版本、插件版本和公共属性各子模块按需声明依赖、独立构建。这套结构配合packagingpom/packaging定义是最稳定、最不容易出幺蛾子的工程形态。如果你刚开始搭建 Spring Cloud 微服务工程直接把packagingpom/packaging放在第一位写清楚可以少踩很多看不见的坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑