用 Maven Archetype 管理微服务 Scaffold:为什么我这里暂时没用 Spring Initializr
目录
📦 本文基于的完整项目源码:https://github.com/meirongdev/shop
背景#
在 Shop Platform 里,每新增一个服务,开发者要做的第一件事不是写业务代码,而是把平台基线搬进去:父 POM 继承、内部 starter 依赖、application.yml 里的 OpenTelemetry / Spring Boot Actuator / 日志格式、Testcontainers 测试基座、K8s deployment / service / HPA 模板……这些内容对每个服务都是一样的。
如果这些样板靠手抄,结果通常有两种:要么漏掉某一项,要么各个服务的写法开始出现细微差异,最终在 code review 和 ArchUnit 检查时才发现,修复成本往往比一开始就收敛要高。
我们需要一种机制:把平台标准前置到生成阶段,而不是等代码写完再回头治理。 Maven Archetype 是解决这个问题的工具。
为什么不用 Spring Initializr#
Spring Initializr 的定位是"从零开始创建 Spring Boot 项目"。它解决的是:一个开发者刚接触 Spring Boot,需要一个可以直接跑起来的骨架。对于独立项目或者小型 monolith,它工作得很好。
但是在 Shop Platform 这个场景里,它有结构性的局限:
1. 无法感知内部依赖体系
Spring Initializr 生成的 pom.xml 引用的是 Spring 官方依赖坐标。它不知道我们有 shop-common-core、shop-starter-http-client、shop-starter-kafka 这些内部 starter,也不知道父 POM 是 dev.meirong.shop:shop-platform。要把这些全部接入,要么手工改生成结果,要么自己部署一套 Initializr 服务。
2. 部署和维护成本
自托管 Spring Initializr 是一个独立的 Spring Boot 应用,需要单独部署、版本升级和维护。每次内部依赖版本变化时,还要同步更新 Initializr 的配置。这是一个额外的系统边界。
3. 与仓库生命周期解耦
Spring Initializr 是一个在仓库之外运行的服务,它生成的结果和当前仓库的 main 分支实际状态可能有偏差。而 Maven Archetype 作为 Maven 模块存在于仓库内部,和平台代码一起演进,版本和提交历史是对齐的。
4. 生成结果不可测试
Spring Initializr 支持 CLI(spring init 或直接 curl start.spring.io),这一点和 Archetype 没有区别。真正的差异在于:Initializr 生成的是一个独立压缩包,和仓库的 Maven 生命周期没有关联,生成结果不能用 maven-invoker (Maven Invoker Plugin) 之类的方式做端到端验证。Archetype 作为 Maven 模块存在,可以在同一个 Maven reactor 里编写集成测试,对生成 → 编译 → 测试的全流程做自动化验证。
技术选型背景#
社区对"内部项目 scaffold 应该用什么工具"的讨论在近年已有比较清晰的分层:
Maven Archetype 在 Maven 单仓场景下,对我们来说仍然是摩擦较低的方案。它是 Maven 生态的原生工具,模板文件和平台代码共用同一套版本管理,archetype:generate 可以直接在 CI 里调用,生成结果也能用 maven-invoker 做端到端集成测试。官方文档见:Maven Archetype Plugin(archetype:generate、archetype-metadata.xml、archetype:create-from-project 的官方参考)。
值得注意的反方视角:JHipster 在其大版本迭代中放弃了 Maven Archetype,改为自研 CLI。原因是当模板逻辑需要大量条件分支时(比如"选择 SQL / NoSQL / 不需要数据库"),Archetype 的 Velocity 模板会膨胀成难以维护的文本。这也是为什么我们把每类 Archetype 的职责限定在"单一服务类型的结构骨架",而不是设计一个参数驱动的通用模板。
更大规模的替代方案:当平台规模扩大到需要跨仓库、多语言、UI 驱动的 scaffold 时,Backstage Software Templates 是当前主流的 IDP(Internal Developer Platform)选择,支持权限控制、GitOps 接入、可视化创建流程。对于 Maven 单仓项目,这个方案引入的基础设施成本远高于收益。
结论:在单仓、纯 Maven、需要和仓库代码版本对齐的场景下,Maven Archetype 目前更贴合这个仓库的需求。如果项目规模扩大到需要多仓库管理、UI 支持、跨语言 scaffold,再评估迁移到 Backstage Templates。
当前项目的六类 Archetype#
Shop Platform 的服务从职责上天然分成了几种类型。每种类型的依赖栈、测试基座、K8s 配置、包结构都有明显区别,分开建模而不是用一个"通用模板+大量参数"是刻意的选择。
分类逻辑#
| Archetype | 适用场景 | 核心依赖栈 | 测试基座 |
|---|---|---|---|
gateway-service-archetype |
边缘接入 / 路由聚合 | Spring Cloud Gateway MVC | MockMvc |
auth-service-archetype |
登录 / 身份 / 令牌 | Spring Security + JWT | MockMvc + WireMock |
bff-service-archetype |
Buyer / Seller 聚合接口 | Spring Web + Spring RestClient | MockMvc + WireMock |
domain-service-archetype |
事务型领域服务 | Spring Data JPA + Flyway + MySQL | Testcontainers MySQL |
event-worker-archetype |
Kafka 消费 / 异步处理 | Kafka Listener + Flyway | Testcontainers Kafka + MySQL |
portal-service-archetype |
Kotlin + Thymeleaf 门户 | Kotlin + Thymeleaf | MockMvc(Kotlin) |
为什么要做这些区分#
1. 依赖集的差异是实质性的,不是偏好
domain-service-archetype 生成的 pom.xml 包含 spring-boot-starter-data-jpa (Spring Data JPA)、flyway-core (Flyway)、flyway-mysql、mysql-connector-j、spring-boot-testcontainers 和 testcontainers:mysql(Testcontainers MySQL)。这些依赖对 bff-service-archetype 来说是无意义的——BFF 不直接操作数据库,不需要 Flyway,不需要 MySQL connector。
如果用一个"全能模板"加很多条件开关,结果是 archetype-metadata.xml 里充满了条件,Velocity 模板里充满了 #if 指令,模板自身变成需要维护的复杂代码。
2. 测试基座不同意味着骨架代码不同
Domain Service 的集成测试继承 AbstractMySqlIntegrationTest,使用 @ServiceConnection 自动配置 Testcontainers MySQL。Event Worker 需要额外配置 KafkaContainer (Testcontainers Kafka). BFF 的集成测试使用 WireMock stub 下游服务。这些差异体现在生成出来的测试骨架里,从一开始就对齐了平台测试规范。
3. K8s 资源配置不同
Event Worker 的 K8s (Kubernetes) deployment 没有 livenessProbe 检查 HTTP 端口(因为它不暴露 HTTP),而是依赖 Actuator 的健康检查端点。Gateway 的 HPA (Horizontal Pod Autoscaler) 配置和 Domain Service 的不同(流量型 vs 计算型)。生成时就分开,避免以后改模板时改乱。
4. 防止新服务从第一天就偏离 ArchUnit 规则
ArchUnit 的 19 条规则里有"不允许 field injection"、“不允许 RestTemplate”、“Kafka listener 需要具备幂等保护"等约束。每个 Archetype 生成的骨架代码从一开始就尽量贴近这些规则,而不是让开发者在违规后再修正。
Archetype 本身需要测试吗#
在这个项目里,我更倾向把它当成必做项。
模板文件(Velocity .vm 文件 + archetype-metadata.xml)本身是文本,不会被 Maven 编译,IDE 也没有类型检查。一个小的占位符拼写错误、一个遗漏的 #set 指令,或者依赖版本写死了不兼容的坐标,都会导致生成出来的项目无法编译,但在提交时不会有任何报错。
测试策略#
Shop Platform 采用的方案是集成测试:在 tooling/archetype-tests 模块里,每个 Archetype 对应一个 JUnit 5 测试类,继承 AbstractArchetypeTest。
测试的核心逻辑是:
生成项目 → 验证目录结构 → mvn compile → mvn test → 验证 surefire-reports 存在
class DomainServiceArchetypeTest extends AbstractArchetypeTest {
@Test
void shouldGenerateProjectWithExpectedStructure() throws Exception {
Path projectDir = generateProject();
assertStandardJavaStructure(projectDir); // src/main/java, src/test/java
assertK8sStructure(projectDir); // k8s/deployment.yaml, service.yaml, hpa.yaml
assertDirectoryExists(projectDir, "src/main/resources/db/migration");
}
@Test
void shouldGenerateCompilableProject() throws Exception {
Path projectDir = generateProject();
compileProject(projectDir); // 调用 mvn compile,非零退出码即失败
}
@Test
void shouldGenerateWithPassingTests() throws Exception {
Path projectDir = generateProject();
testProject(projectDir); // 调用 mvn test,验证 surefire-reports 生成
}
@Test
void shouldGenerateWithCorrectDependencies() throws Exception {
Path projectDir = generateProject();
String pom = Files.readString(projectDir.resolve("pom.xml"));
assertThat(pom).contains("spring-boot-starter-data-jpa");
assertThat(pom).contains("flyway-core");
assertThat(pom).contains("shop-common-core");
}
}
AbstractArchetypeTest 在第一个测试运行前会用 mvnw install 把 shop-common、shop-contracts、shop-archetypes 全部安装到本地 Maven 仓库,然后在 JUnit 的 @TempDir 里执行 archetype:generate,最后用 maven-invoker (Maven Invoker Plugin) 对生成结果执行编译和测试。整个流程在临时目录里完成,不污染工作区。
为什么不用 maven-archetype-plugin 的 integration-test 机制#
Maven Archetype Plugin 自带一个 integration-test 生命周期,可以通过 src/test/resources/projects/ 目录配置批量生成测试。这个机制有个问题:它只验证生成过程不出错,不验证生成出来的项目是否可以编译和运行。用 JUnit + maven-invoker 的方案可以精确控制验证深度,也更容易在 CI 里查看失败原因。
CI 集成#
Archetype 测试已集成到 GitHub Actions。当 tooling/shop-archetypes/** 或 shared/shop-common/** 下有文件变更时,CI 自动触发 archetype-test job:
make archetype-test
# 等价于:
./platform/scripts/test-archetypes.sh
这个脚本先安装依赖到本地仓库,再运行 mvnw -pl tooling/archetype-tests -am test。
版本管理与维护#
版本策略#
shop-archetypes 和 shop-platform 共用同一个版本号(当前 0.1.0-SNAPSHOT)。这意味着 Archetype 版本和平台版本是强对齐的,不存在"用了旧版 Archetype 生成了新模块"的歧义。
版本语义遵循 SemVer,但针对 scaffold 场景做了具体定义:
| 变更类型 | 版本位 | 举例 |
|---|---|---|
| 默认技术栈或目录结构不兼容变化 | Major | 从 JPA 切换到 jOOQ,K8s 配置格式重构 |
| 新增模板能力、新增默认依赖、新增部署模板 | Minor | 添加 Resilience4j 配置骨架,增加 event-worker-archetype |
| 占位符修复、文档修正、无破坏性默认值调整 | Patch | 修复 application.yml 里的 typo,更新 Java 版本 |
变更同步流程#
每次模板变更,至少同步以下三个位置:
tooling/shop-archetypes/README.md:更新安装 / 生成 / 验证说明。docs/ENGINEERING-STANDARDS-2026.md:如果影响了平台基线或 scaffold 规范。docs/ROADMAP-2026.md:如果影响了平台工程任务状态或交付顺序。
如果变更会破坏兼容性(Major),我们通常会要求 PR 描述包含:
- 影响的 Archetype 名称
- 是否影响已经用 Archetype 生成并落地的模块
- 迁移动作(手动 diff 更新 or 重新生成)
- 验证方式
对存量模块的影响#
Archetype 是生成时工具,不是运行时依赖。一旦用 Archetype 生成了模块并加入仓库,这个模块和 Archetype 就没有绑定关系了。模板变更不会自动同步到已有模块。
这个特性是双刃剑:
- 好处:存量模块不会因为模板升级而意外改变。
- 坏处:模板和存量模块会随着时间出现漂移。
我们用 ArchUnit 作为漂移检测的"兜底”:新规则被 Archetype 模板前置落地后,同时在 ArchUnit 里新增对应的规则,存量模块在下次跑 make arch-test 时就会被检测出偏差。
日常维护建议#
升级平台依赖时(比如 Spring Boot 版本升级):
- 升级根 pom.xml 里的版本号。
- 检查各 Archetype 的
archetype-resources/pom.xml是否有写死的版本号——通常更适合通过父 POM 的<dependencyManagement>继承,不要在模板里写死。 - 运行
make archetype-test,确保所有 Archetype 生成的项目仍然可以编译和测试通过。
新增 Archetype 时:
- 在
tooling/shop-archetypes/下创建新模块,继承shop-archetypes父 POM。 - 编写
archetype-metadata.xml和模板资源。 - 在
tooling/archetype-tests/里新增对应的 JUnit 测试类。 - 在根 pom.xml 的
<modules>和 Makefile 的ARCHETYPE_MODULES变量里注册。 - 更新 README 里的"什么时候用哪个 Archetype"表格。
实操:生成一个新的领域服务#
# 1. 安装模板到本地 Maven 仓库
make archetypes-install
# 2. 在仓库外的空目录里生成
SHOP_REPO=/path/to/shop-platform
mkdir -p /tmp/shop-sandbox && cd /tmp/shop-sandbox
"${SHOP_REPO}/mvnw" archetype:generate \
-DarchetypeCatalog=local \
-DarchetypeGroupId=dev.meirong.shop \
-DarchetypeArtifactId=domain-service-archetype \
-DarchetypeVersion=0.1.0-SNAPSHOT \
-DgroupId=dev.meirong.shop \
-DartifactId=inventory-service \
-Dpackage=dev.meirong.shop.inventory \
-DshopPlatformVersion=0.1.0-SNAPSHOT \
-DinteractiveMode=false
# 3. 验证生成结果(仓库外验证)
perl -0pi -e 's#<relativePath>\.\./pom\.xml</relativePath>#<relativePath/>#g' \
inventory-service/pom.xml
"${SHOP_REPO}/mvnw" -f inventory-service/pom.xml test
# 4. 确认骨架符合预期后,移入仓库
mv /tmp/shop-sandbox/inventory-service "${SHOP_REPO}/services/"
# 5. 在根 pom.xml 的 <modules> 里注册
# <module>services/inventory-service</module>
# 6. 运行完整验证
cd "${SHOP_REPO}" && make verify
生成出来的 inventory-service 目录结构:
inventory-service/
├── pom.xml # 继承 shop-platform,依赖已就绪
├── README.md
├── k8s/
│ ├── deployment.yaml # startupProbe 已配置
│ ├── service.yaml
│ └── hpa.yaml
└── src/
├── main/
│ ├── java/dev/meirong/shop/inventory/
│ │ └── InventoryServiceApplication.java
│ └── resources/
│ ├── application.yml # OTEL, Actuator, 日志格式已配置
│ └── db/migration/ # Flyway 迁移目录
└── test/
└── java/dev/meirong/shop/inventory/
└── AbstractMySqlIntegrationTest.java # Testcontainers 基座
总结#
在 Shop Platform 这个场景下,Maven Archetype 比 Spring Initializr 更贴合当前需求,核心原因是:Archetype 是仓库内的工件,能感知内部依赖体系,版本和平台对齐,也比较容易在 CI 里做端到端测试。
六类 Archetype 对应了平台里六种本质不同的服务职责,分开建模而不是"万能模板+参数",避免了模板逻辑膨胀,也让每种服务从第一行代码开始就符合平台规范。
对我们来说,Archetype 最好也有测试——生成、编译、测试全链路验证,不只是检查“能生成”,而是尽量确认“生成出来的东西还能跑”。
版本策略上,Archetype 和平台共用版本号,SemVer 语义明确到 scaffold 场景,存量模块的漂移由 ArchUnit 兜底检测。