搜索 K
Appearance
Appearance
forge 的应用骨架由两个职责清晰的模块共同支撑:forge-module 负责「在容器启动早期把元数据收齐、校验、注册」,forge-boot 负责「在应用启动后把元数据落地为表结构与系统记录」。一个管「知道有什么」,一个管「让它真的存在」。本篇解释这条链路为什么这样切分,以及模块/模型注册中心与生命周期阶段背后的设计取舍。
如果你想直接动手把一个模块跑起来,请先看使用指南:应用架构使用指南。涉及包扫描、反射与启动钩子的基础知识,可参阅 前置知识。
框架要做的事情,本质上分两类。第一类是纯粹的元数据计算:扫描 @Module 与 @Model、解析注解、校验约束、确定模型归属与表名。这类工作没有任何副作用,越早做越好——做得越早,越能在 Bean 实例化之前 fail-fast,把配置错误挡在容器启动之前。第二类是有副作用的落地:建表、改表、把元数据写进系统表。这类工作必须等数据源、方言、事务等基础设施就绪后才能进行。
把这两类强行揉在一起,会让「计算」被「落地」的时机绑架,失去 fail-fast 的窗口。因此 forge 沿启动时间轴把它们切成两段,分别交给两个模块。
一句话记住分工
forge-module 在 Bean 工厂阶段产出两份注册中心;forge-boot 在应用启动事件里消费它们完成 DDL 与落库。前者是「编译期级别的真理」,后者是「运行期的副作用」。
forge-module 的核心是一个 Bean 工厂后处理器 ModelMetaBootstrapPostProcessor,它同时实现 BeanDefinitionRegistryPostProcessor 与 PriorityOrdered,并把 getOrder() 钉死在 PriorityOrdered.HIGHEST_PRECEDENCE。这意味着它在所有普通 Bean 实例化之前运行,是元数据计算最早能介入的合法时机。
它的入口方法 postProcessBeanDefinitionRegistry 按四个有序步骤推进:
@Module 是模块的声明,被 ModuleAnnoScanHelper.parse 解析成 ModuleDefinition。模块定义携带 code、name、version、priority、dependencies、scanPackages 等属性,以及标注源类 sourceClass。其中 effectivePackages() 把源类所在包与显式 scanPackages 合并,作为该模块的有效扫描范围。
模块之间通过 dependencies 表达依赖,例如 AuthModule 声明 dependencies = {"system"}。这份依赖关系会在落地期被 ModuleSortHelper 拓扑排序,保证被依赖者先建表。
每个 @Model 类经 ModelParser.parse 得到 ModelDefinition,随后经历几道关键加工:resolveLogicDelete() 结合全局配置固化逻辑删除的生效值,getModuleByPackage() 按「最长前缀优先」把模型挂到所属模块,finalizeNaming() 固化出形如 module.name 的模型名、表名与索引名。注册中心同时维护 definitions、ownership(模型归属模块)、storeTables(落库表)三份索引。
模型解析完成后,MetadataValidator 会跨模型做一轮整体校验:无模块覆盖的模型、被多模块等长匹配的歧义模型、STORE 模型表名为空或重复、索引字段不存在、逻辑删除列与字段冲突、枚举字段非法、关系字段非法。所有违规收集后由 throwIfAny() 一次性抛出,而不是遇到第一个错误就崩。
这与 forge 的整体偏好一致:把隐式约束变成启动期主动抛异常,并把多处违规汇总成一份报告,让使用者一次看清所有问题。
注册期要扫描的包,既包含业务应用包,也包含框架内置模块(如系统模块、权限模块)的包。业务包通过 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 的 LifecycleOrchestrator 监听 ApplicationStartedEvent,以 @Order(0) 抢在其他监听器之前执行。它从容器里取出注册期产出的 moduleRegistry 与 modelRegistry,按 ModuleSortHelper.sort 的拓扑顺序处理每个模块,被依赖模块优先、同层按 priority 升序。
落地期的行为由 Lifecycle 枚举切换,配置键 app.lifecycle,缺省 INSTALL:
| 阶段 | 含义 | DDL | 系统表落库 |
|---|---|---|---|
INSTALL | 完整安装 | 建表 + 改表 + 删表 | 写入 |
RELOAD | 重载元数据 | 跳过 | 写入 |
DDL | dry-run | 仅生成 SQL 输出到文件 | 不写入,输出后退出 JVM |
INSTALL 是默认形态,适合首次部署与日常迭代。RELOAD 在表结构已稳定、只想刷新系统表里那份元数据快照时使用,跳过 DDL 既省时间也避免误触表结构。DDL 是 dry-run:只把将要执行的 SQL 写到 app.dryRunOutputDir 指定目录(缺省 target),写完即 SpringApplication.exit() 退出,让你在真正动数据库前先审阅一遍。各阶段的操作步骤见 应用架构使用指南。
落地期会打印形如下面的日志,可据此确认拓扑顺序与统计结果:
[lifecycle] Module topology order: [system, auth, biz]
[lifecycle] Lifecycle=INSTALL, 模块 3 个, 扫描模型 12 个, 耗时 456ms| 选项 | 选择 | 理由 |
|---|---|---|
用 @AutoConfigurationPackage 标记包 | 否(仅借它取业务包) | 它只是包标记,拿不到容器引用,无法注册元数据 Bean |
用 BFPP(BeanDefinitionRegistryPostProcessor) | 是 | 以 PriorityOrdered.HIGHEST_PRECEDENCE 在 Bean 实例化前运行,能拿到 BeanDefinitionRegistry 直接注册单例,且时序由 Spring 显式保证,支持 fail-fast 校验 |
BFPP 是唯一能在启动早期同时控制「包扫描、元数据解析、容器注册」三件事的时机;@AutoConfigurationPackage 只承担「标记业务包」这一辅助职责。
| 选项 | 选择 | 理由 |
|---|---|---|
| 在启动器里硬编码框架模块包 | 否 | 每加一个框架模块都要改核心启动代码,启动器沦为中央枢纽,耦合不断累积 |
用 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 完成扫描、解析、校验与注册,产出 ModuleRegistry 与 ModelRegistry 两份注册中心;forge-boot 在 ApplicationStartedEvent 之后由 LifecycleOrchestrator 消费它们,按拓扑顺序执行 DDL 并落库,并通过 Lifecycle 阶段在 INSTALL、RELOAD、DDL(dry-run)之间切换。把「计算」与「落地」分离、用 SPI 聚合扩展点、把校验一次性抛出,是这套架构反复出现的设计主线。
动手实践请转 应用架构使用指南。