搜索 K
Appearance
Appearance
这一组设计文档回答的核心问题不是「forge 怎么用」,而是「forge 为什么这样组织一个应用」。如果你只想把宠物商店跑起来,请先读对应的使用指南;如果你想知道一行 @Model 注解从被扫描到落地成表,中间究竟经过了哪些环节、为什么是这个顺序,那么本篇就是入口。
forge 的应用架构可以浓缩成一句话:以模块为编排单元,在启动早期一次性完成「扫描 — 解析 — 校验 — 注册」,再以拓扑顺序驱动建表与元数据落库。围绕这句话,本篇先给出整体分层,再把设计篇的三条主线串起来。
| 主线 | 解决的问题 | 关键载体 |
|---|---|---|
| 架构总览 | 模块如何声明、模型如何归属、注册表如何共享 | @Module、ModuleDefinition、ModelRegistry |
| 启动生命周期编排(★) | 扫描与建表为何分两段、谁先谁后、dry-run 如何退出 | ModelMetaBootstrapPostProcessor、LifecycleOrchestrator |
| SPI 聚合机制 | 框架模块如何在不改启动器代码的前提下贡献扫描包 | @SPI、Spider、ScanPackageProvider |
阅读顺序建议
先看本页的「分层与启动两段论」建立全局心智模型,再深入「启动生命周期编排」理解时序,最后用「SPI 聚合机制」理解可扩展性。三篇互为补充,不必一次读完。
forge 把「描述应用」与「驱动应用」拆成两类构件:
@Module 描述模块边界,用 @Model/@Field 描述实体。这些注解是纯粹的元数据,不含运行逻辑。ModuleRegistry 与 ModelRegistry,再据此生成 DDL、落库系统表。声明层只表达「是什么」,编排层独占「怎么落地」。这条分界线让业务实体类保持极简——以宠物商店的 Pet 为例,它只声明字段与关系,不感知扫描、校验、建表的任何一步。
forge 的启动被刻意切成两个互不重叠的阶段,这是整个架构最重要的取舍。
| 设计取舍 | 选项 | 选择 | 理由 |
|---|---|---|---|
| 元数据何时构建 | A. Bean 实例化后用监听器构建;B. BFPP 在实例化前构建 | B | 元数据是后续所有 Bean(Mapper、Repository)的前提,必须最早就绪 |
| 校验时机 | A. 用到时报错;B. 启动期全量校验 | B | 隐式约束改启动期 fail-fast,多违规一次性汇总报告,避免上线后才暴露 |
| 建表与扫描是否合并 | A. 扫描完立即建表;B. 扫描归 BFPP、建表归 StartedEvent | B | 建表依赖数据源连接,而数据源 Bean 在 BFPP 阶段尚未就绪 |
两段不可颠倒
阶段一在 BeanDefinitionRegistryPostProcessor 中执行,此时数据源尚未初始化,因此不能建表;阶段二在 ApplicationStartedEvent 中执行,此时元数据注册表早已就绪,可以放心驱动 DDL。把建表塞进阶段一会导致拿不到数据源连接。
阶段一由 ModelMetaBootstrapPostProcessor 承担,它实现 BeanDefinitionRegistryPostProcessor 与 PriorityOrdered,getOrder() 返回 PriorityOrdered.HIGHEST_PRECEDENCE,确保自己在所有 Bean 定义后处理器中最先运行。阶段二由 LifecycleOrchestrator 承担,监听 ApplicationStartedEvent 且 @Order(0)。两者的详细时序在启动生命周期编排中展开。
@Module 不是装饰性标记,它是拓扑排序与元数据归属的基准。宠物商店的业务模块声明形如:
@Module(
code = BizModule.MODEL_CODE,
name = "测试业务工程",
version = 1,
versionName = "1.0.0",
priority = 100,
dependencies = {SystemModule.MODULE_CODE}
)
public final class BizModule {
public static final String MODEL_CODE = "biz";
}几个字段直接决定编排行为:
dependencies 描述模块间依赖,priority 描述同层先后(数值越小越先处理),二者共同喂给 ModuleSortHelper 得到拓扑序。scanPackages 与源类所在包共同构成有效扫描范围,由 ModuleDefinition 的 effectivePackages() 返回。框架自带的 SystemModule(priority = 0)与 AuthModule(priority = 10, dependencies = {"system"})是这套规则的内置示范:系统模块永远最先建表,权限模块紧随其后,业务模块最后。这正是日志里那行拓扑序的来源:
[lifecycle] Module topology order: [system, auth, biz]模块定义模型、注册表与排序器的细节,见架构总览。
阶段一的第一步是「收集扫描包」。业务包由 Spring Boot 的 AutoConfigurationPackages 提供,而框架内置模块(forge-boot、forge-auth 等)的包则通过 SPI 聚合贡献:
这里用的是聚合贡献模式:Spider.getAllExtensions(ScanPackageProvider.class) 遍历所有实现,把每个 contributePackages() 的返回值汇总进同一个包集合。它与「单选互斥」模式(如方言、鉴权实现用 getDefaultExtension() 取唯一实现)形成对照。
| 设计取舍 | 选项 | 选择 | 理由 |
|---|---|---|---|
| 框架模块如何被扫描 | A. 启动器硬编码包列表;B. SPI 聚合贡献 | B | 新框架模块只需添加 META-INF/services 贡献,无需修改启动器代码,践行开闭原则 |
| 包声明职责归属 | A. 集中在启动器;B. 各模块自洽声明 | B | 每个模块声明自身包,符合单一职责,利于独立发布 |
这意味着新增一个框架模块时,既不需要改 BFPP,也不需要改任何中央清单——只要实现 ScanPackageProvider 并在 META-INF/services/cn.cvking.forge.module.spi.ScanPackageProvider 里登记即可。完整机制与新增步骤见 SPI 聚合机制,其底层依赖的反射与包扫描能力见前置知识。
把三条主线合到一张时序图上,就是 forge 应用从进程拉起到对外服务的全过程。
任何一篇深入文档,最终都能落回这张图上的某个环节。建议带着「这一步发生在 BFPP 还是 StartedEvent」「这是聚合还是单选」两个问题去读后续各篇。
@SPI、SpiLoader 排序规则、聚合与单选两种模式。