做 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 库 | 不适合 | 业务模块、公共代码库 |
| war | Web 应用压缩包 | 不适合 | 传统 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 version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>microservice-parent</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> <java.version>17</java.version> <spring-cloud.version>2023.0.1</spring-cloud.version> <spring-boot.version>3.2.5</spring-boot.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>这段配置里,最关键的一行就是<packaging>pom</packaging>。它和<groupId>、<artifactId>、<version>一起定义了父工程的 GAV 坐标,同时也是子模块<parent>引用的依据。
注意看dependencyManagement里的写法:引入 Spring Boot 和 Spring Cloud 的 BOM(Bill of Materials)时,<type>pom</type>和<scope>import</scope>两个标签缺一不可。这是父工程做依赖版本统一的最常见手段,子模块在声明依赖时就不需要写版本号了,父工程的依赖管理会自动帮它确定版本。
2.2 声明 modules:让父工程真正承担聚合职责
只有<packaging>pom</packaging>还不够,父工程还需要通过<modules>标签把自己要管理的子模块一一列出来。这样执行mvn install时,Maven 会自动按照模块间的依赖关系排序构建,先构建common,再构建依赖它的order-service和user-service,最后构建gateway-service。
<modules> <module>common</module> <module>gateway-service</module> <module>order-service</module> <module>user-service</module> </modules>每个<module>标签里填的是子模块目录的相对路径。Maven 在解析时,会去这些目录下找对应的pom.xml。
这里有个很多人栽过的跟头:父工程里写了<modules>,子模块自己也写了<parent>,但目录结构对不上,比如父 POM 在仓库根目录,子模块却在services/order-service这种二级目录里。这时候如果在services/order-service目录下单独执行mvn install,Maven 会尝试通过../..找父 POM,如果找不到,就尝试从本地仓库里解析。为了避免这种混乱,我在实际项目里一般会把所有模块放在父目录的一级子目录下,并且保证子模块 POM 的<relativePath>指向正确位置。
2.3 新建子模块时锁定父工程坐标
子模块的pom.xml里,<parent>段必须正确引用父工程的 GAV 坐标,同时指定<relativePath>。以order-service为例:
<parent> <groupId>com.example</groupId> <artifactId>microservice-parent</artifactId> <version>1.0.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>order-service</artifactId> <packaging>jar</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 版本、数据库连接池版本,结果就是各模块依赖同一组件不同版本,运行期出现奇怪的冲突。
而<packaging>pom</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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <source>17</source> <target>17</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </pluginManagement> </build>这个配置的核心逻辑是:pluginManagement只注册不激活,子模块需要某个插件时,在<build><plugins>里声明插件,版本和基础配置从父工程自动带下来。这样做的好处是,升级某个插件的版本时,只需要改父工程一处,所有子模块统一生效。
父项目<packaging>pom</packaging>与<pluginManagement>是天然搭配。因为父工程本人不会被编译,所以它不需要真正执行maven-compiler-plugin的编译目标,只是在配置文件层面做一个“统一约定”的声明。如果父工程是jar类型,pluginManagement里的编译器配置还会在父工程自身构建时被触发执行,这就是一大类“父工程构建比想象中慢”问题的根源。
3.3 属性的传递:一个 properties 贯穿所有模块
多模块工程里,版本号、环境标识、路径配置这类属性如果散落在各子模块,很难维护。父工程可以通过<properties>定义公共属性,被所有子模块继承。
<properties> <java.version>17</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-boot.version>3.2.5</spring-boot.version> <spring-cloud.version>2023.0.1</spring-cloud.version> <mysql.version>8.3.0</mysql.version> </properties>子模块里直接使用${spring-boot.version}就能引用父工程定义的属性值。这种机制配合dependencyManagement,基本能实现版本统一管理的全覆盖。
同样,属性继承也要求父工程是pom类型。如果打包类型不是pom,Maven 在解析父工程时,内部的属性解析逻辑虽然不会报错,但一旦父工程被某个子模块以 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> <groupId>com.example</groupId> <artifactId>common</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> </dependencies>这个写法本身没错,common模块确实是所有模块都要用的公共代码。但问题是:gateway-service这样的网关模块也许需要common,但如果另有某个独立的纯工具模块并不需要common里的哪些类,它依然会把common的依赖带上,无形中增加类库体积,甚至可能触发依赖冲突。
更严重的场景是:父工程把某个数据库驱动或消息队列 client 写进了<dependencies>,但只有部分业务模块需要连接数据库。结果所有模块——包括根本不需要数据库的模块——都被强制引入了数据库驱动依赖。不仅仅是 jar 包体积变大,更危险的是,在某些 Spring Boot 自动配置场景下,类路径下存在数据源相关依赖会触发自动配置,导致应用启动时尝试初始化数据源,然后失败。
排查这个问题的思路是:
- 确认父工程的
dependencyManagement和dependencies两个区段分别写了什么 - 执行
mvn dependency:tree查看某个不需要该依赖的模块,确认它是否意外带上了 - 把那些“只有部分模块需要的依赖”从父工程
dependencies迁到dependencyManagement中,然后在真正需要的子模块里声明依赖,不写版本号即可
这个问题的根源,还是对packaging=pom的语义理解不够深刻:父工程定义成pom,意味着它是一个“约定”层,而不是“依赖注入”层。约定可以统一,但具体每个模块需要什么,还是要让模块自己做决定。
4.3 坑三:子模块把 packaging 也写成了 pom,导致无法生成可执行 jar
这种坑一般发生在照搬父工程模板的时候。某个开发者习惯了父工程里的<packaging>pom</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在子模块的<build><plugins>里被声明(因为父工程的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缺失导致模型状态不一致。加上<packaging>pom</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 (packaging=pom) ├── common (packaging=jar) ├── gateway-service (packaging=jar) ├── order-service (packaging=jar) └── user-service (packaging=jar)父工程统一管理依赖版本、插件版本和公共属性,各子模块按需声明依赖、独立构建。这套结构配合<packaging>pom</packaging>定义,是最稳定、最不容易出幺蛾子的工程形态。如果你刚开始搭建 Spring Cloud 微服务工程,直接把<packaging>pom</packaging>放在第一位写清楚,可以少踩很多看不见的坑。