Skip to content

整体架构

forge 的应用骨架由两个职责清晰的模块共同支撑:forge-module 负责「在容器启动早期把元数据收齐、校验、注册」,forge-boot 负责「在应用启动后把元数据落地为表结构与系统记录」。一个管「知道有什么」,一个管「让它真的存在」。本篇解释这条链路为什么这样切分,以及模块/模型注册中心与生命周期阶段背后的设计取舍。

如果你想直接动手把一个模块跑起来,请先看使用指南:应用架构使用指南。涉及包扫描、反射与启动钩子的基础知识,可参阅 前置知识

为什么把启动拆成「注册期」与「落地期」

框架要做的事情,本质上分两类。第一类是纯粹的元数据计算:扫描 @Module@Model、解析注解、校验约束、确定模型归属与表名。这类工作没有任何副作用,越早做越好——做得越早,越能在 Bean 实例化之前 fail-fast,把配置错误挡在容器启动之前。第二类是有副作用的落地:建表、改表、把元数据写进系统表。这类工作必须等数据源、方言、事务等基础设施就绪后才能进行。

把这两类强行揉在一起,会让「计算」被「落地」的时机绑架,失去 fail-fast 的窗口。因此 forge 沿启动时间轴把它们切成两段,分别交给两个模块。

一句话记住分工

forge-module 在 Bean 工厂阶段产出两份注册中心;forge-boot 在应用启动事件里消费它们完成 DDL 与落库。前者是「编译期级别的真理」,后者是「运行期的副作用」。

forge-module:注册期的元数据中枢

forge-module 的核心是一个 Bean 工厂后处理器 ModelMetaBootstrapPostProcessor,它同时实现 BeanDefinitionRegistryPostProcessorPriorityOrdered,并把 getOrder() 钉死在 PriorityOrdered.HIGHEST_PRECEDENCE。这意味着它在所有普通 Bean 实例化之前运行,是元数据计算最早能介入的合法时机。

它的入口方法 postProcessBeanDefinitionRegistry 按四个有序步骤推进:

模块注册中心 ModuleRegistry

@Module 是模块的声明,被 ModuleAnnoScanHelper.parse 解析成 ModuleDefinition。模块定义携带 codenameversionprioritydependenciesscanPackages 等属性,以及标注源类 sourceClass。其中 effectivePackages() 把源类所在包与显式 scanPackages 合并,作为该模块的有效扫描范围。

模块之间通过 dependencies 表达依赖,例如 AuthModule 声明 dependencies = {"system"}。这份依赖关系会在落地期被 ModuleSortHelper 拓扑排序,保证被依赖者先建表。

模型注册中心 ModelRegistry

每个 @Model 类经 ModelParser.parse 得到 ModelDefinition,随后经历几道关键加工:resolveLogicDelete() 结合全局配置固化逻辑删除的生效值,getModuleByPackage() 按「最长前缀优先」把模型挂到所属模块,finalizeNaming() 固化出形如 module.name 的模型名、表名与索引名。注册中心同时维护 definitionsownership(模型归属模块)、storeTables(落库表)三份索引。

为什么校验集中在这一步

模型解析完成后,MetadataValidator 会跨模型做一轮整体校验:无模块覆盖的模型、被多模块等长匹配的歧义模型、STORE 模型表名为空或重复、索引字段不存在、逻辑删除列与字段冲突、枚举字段非法、关系字段非法。所有违规收集后由 throwIfAny() 一次性抛出,而不是遇到第一个错误就崩。

这与 forge 的整体偏好一致:把隐式约束变成启动期主动抛异常,并把多处违规汇总成一份报告,让使用者一次看清所有问题。

SPI 聚合:让扫描包不再硬编码

注册期要扫描的包,既包含业务应用包,也包含框架内置模块(如系统模块、权限模块)的包。业务包通过 Spring Boot 标准的 AutoConfigurationPackages 获取;框架模块的包则通过 SPI 聚合而来。

ScanPackageProvider 是一个 @SPI 接口,每个想被扫描的框架模块都实现它并在 META-INF/services/cn.cvking.forge.module.spi.ScanPackageProvider 中登记。BFPP 启动时通过 Spider.getAllExtensions(ScanPackageProvider.class) 遍历所有实现,把各家 contributePackages() 返回的包汇聚到一起。

SPI 在 forge 里有两种用法:聚合贡献与单选互斥。ScanPackageProvider 属于聚合贡献——所有实现的产出会被汇总;而像方言、鉴权实现这类需要唯一答案的场景,则用 getDefaultExtension()getExtension(name) 做单选。SPI 框架本身的细节见 SPI 扩展机制设计

新增一个框架模块要做什么

实现 ScanPackageProvider,在该模块的 META-INF/services/cn.cvking.forge.module.spi.ScanPackageProvider 里登记实现类全限定名即可。无需改动任何核心启动代码——这正是开闭原则的体现。

forge-boot:落地期的生命周期编排

应用启动后,forge-bootLifecycleOrchestrator 监听 ApplicationStartedEvent,以 @Order(0) 抢在其他监听器之前执行。它从容器里取出注册期产出的 moduleRegistrymodelRegistry,按 ModuleSortHelper.sort 的拓扑顺序处理每个模块,被依赖模块优先、同层按 priority 升序。

生命周期阶段

落地期的行为由 Lifecycle 枚举切换,配置键 app.lifecycle,缺省 INSTALL

阶段含义DDL系统表落库
INSTALL完整安装建表 + 改表 + 删表写入
RELOAD重载元数据跳过写入
DDLdry-run仅生成 SQL 输出到文件不写入,输出后退出 JVM

INSTALL 是默认形态,适合首次部署与日常迭代。RELOAD 在表结构已稳定、只想刷新系统表里那份元数据快照时使用,跳过 DDL 既省时间也避免误触表结构。DDL 是 dry-run:只把将要执行的 SQL 写到 app.dryRunOutputDir 指定目录(缺省 target),写完即 SpringApplication.exit() 退出,让你在真正动数据库前先审阅一遍。各阶段的操作步骤见 应用架构使用指南

落地期会打印形如下面的日志,可据此确认拓扑顺序与统计结果:

text
[lifecycle] Module topology order: [system, auth, biz]
[lifecycle] Lifecycle=INSTALL, 模块 3 个, 扫描模型 12 个, 耗时 456ms

设计取舍

注册期为何用 BFPP,而非 @AutoConfigurationPackage

选项选择理由
@AutoConfigurationPackage 标记包否(仅借它取业务包)它只是包标记,拿不到容器引用,无法注册元数据 Bean
用 BFPP(BeanDefinitionRegistryPostProcessorPriorityOrdered.HIGHEST_PRECEDENCE 在 Bean 实例化前运行,能拿到 BeanDefinitionRegistry 直接注册单例,且时序由 Spring 显式保证,支持 fail-fast 校验

BFPP 是唯一能在启动早期同时控制「包扫描、元数据解析、容器注册」三件事的时机;@AutoConfigurationPackage 只承担「标记业务包」这一辅助职责。

扫描包为何用 SPI 聚合,而非硬编码列表

选项选择理由
在启动器里硬编码框架模块包每加一个框架模块都要改核心启动代码,启动器沦为中央枢纽,耦合不断累积
ScanPackageProvider 的 SPI 聚合各模块自洽声明自身包,新增模块只需添加 META-INF/services 贡献;模块可独立分发,SPI 自动发现,符合开闭原则

落地期为何独立成阶段,而非在注册期顺手建表

选项选择理由
在 BFPP 里直接建表Bean 工厂阶段数据源、方言、事务等尚未就绪,且会让无副作用的元数据计算丧失 fail-fast 窗口
ApplicationStartedEvent 后的 LifecycleOrchestrator基础设施已就绪,可安全执行 DDL;并能按 Lifecycle 阶段在安装、重载、dry-run 之间切换

校验为何一次性抛出,而非遇错即停

选项选择理由
遇到第一处违规即抛异常使用者需反复启动逐个修,体验差
MetadataValidator 收集后 throwIfAny() 统一抛一次启动暴露全部违规,符合启动期 fail-fast 偏好,修复一轮即可

小结

forge 的应用架构是一条沿启动时间轴铺开的链路:forge-module 在 Bean 工厂阶段以最高优先级的 BFPP 完成扫描、解析、校验与注册,产出 ModuleRegistryModelRegistry 两份注册中心;forge-bootApplicationStartedEvent 之后由 LifecycleOrchestrator 消费它们,按拓扑顺序执行 DDL 并落库,并通过 Lifecycle 阶段在 INSTALLRELOADDDL(dry-run)之间切换。把「计算」与「落地」分离、用 SPI 聚合扩展点、把校验一次性抛出,是这套架构反复出现的设计主线。

动手实践请转 应用架构使用指南