搜索 K
Appearance
Appearance
forge 把一组相关的实体(@Model)归拢到一个业务模块(@Module)之下。模块是建表顺序、元数据落库、依赖拓扑的基本单元。本篇用宠物商店的 petshop 模块走通最短路径:声明模块 → 让模块扫描到实体 → 启动后从 debug 日志确认「模块 N 个」。
阅读前提
@Model 声明实体(参见 DDL 篇的快速开始)想了解模块为何这样编排、BFPP 与 SPI 在启动期如何协作,请转 应用架构设计。
新建一个标注了 @Module 的类,放在业务模型包的根位置。code 必填且全局唯一,scanPackages 指向你的模型所在包。
package com.demo.petshop;
import cn.cvking.forge.module.annotation.Module;
import cn.cvking.forge.boot.sys.SystemModule;
@Module(
code = PetShopModule.MODULE_CODE,
name = "宠物商店",
version = 1,
versionName = "1.0.0",
priority = 100,
dependencies = {SystemModule.MODULE_CODE},
scanPackages = {"com.demo.petshop.model"}
)
public final class PetShopModule {
public static final String MODULE_CODE = "petshop";
}各参数含义:
| 参数 | 作用 | 缺省 |
|---|---|---|
code | 模块唯一编码,必填 | 无 |
name | 展示名称,用于前端/运维面板 | 空串时回落到 code |
version | 单调递增整数,程序据此判断升级 | 1 |
versionName | 版本展示文本,如 "1.0.0" | "1.0.0" |
priority | 拓扑同层级时的排序,数值越小越先处理 | 1000 |
dependencies | 依赖的模块 code 列表 | {} |
scanPackages | 显式声明的 @Model 扫描包 | {} |
为什么 dependencies 写 "system"
系统模块 system 承载 sys_module、sys_model 等元数据表。业务模块声明 dependencies = {"system"} 后,建表与元数据落库会排在系统模块之后,避免依赖表尚未就绪。system 模块的 priority 为 0,天然最先处理。
forge 按包归属把每个 @Model 匹配到模块(最长前缀优先)。模块的有效扫描包 = 标注 @Module 的源类所在包 + scanPackages。所以把实体放进 scanPackages 指向的包即可:
com.demo.petshop
├── PetShopModule.java ← @Module(源类所在包也会被纳入扫描)
└── model
├── Category.java ← @Model
├── Owner.java
├── PetProfile.java
├── Tag.java
├── PetTag.java ← extends RelationModel
└── Pet.java ← @Model(name = "pet.Pet"),主角主角 Pet 实体(外键标量列与关系字段成对出现,双向关系字段加 @EqualsAndHashCode.Exclude、@ToString.Exclude 防止递归栈溢出):
package com.demo.petshop.model;
import cn.cvking.forge.metadata.annotation.Field;
import cn.cvking.forge.metadata.annotation.Model;
import cn.cvking.forge.data.IdModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.math.BigDecimal;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@Model(name = "pet.Pet", displayName = "宠物")
public class Pet extends IdModel<Pet> {
@Field(displayName = "名称")
@Field.String(length = 64)
private String name;
@Field(displayName = "状态")
@Field.Enum
private PetStatus status;
@Field(displayName = "售价")
@Field.BigDecimal(precision = 10, scale = 2)
private BigDecimal price;
// 外键标量列:显式声明,与关系字段对称
@Field(displayName = "分类ID")
@Field.Long
private Long categoryId;
@Field(displayName = "主人ID")
@Field.Long
private Long ownerId;
@Field(displayName = "档案ID")
@Field.Long
private Long profileId;
// 关系字段(虚拟,store=false 不落库)
@Field.M2O
@Field.Relation(relationFields = "categoryId")
@EqualsAndHashCode.Exclude @ToString.Exclude
private Category category;
@Field.M2O
@Field.Relation(relationFields = "ownerId")
@EqualsAndHashCode.Exclude @ToString.Exclude
private Owner owner;
@Field.O2O
@Field.Relation(relationFields = "profileId")
@EqualsAndHashCode.Exclude @ToString.Exclude
private PetProfile profile;
@Field.M2M
@Field.Relation(throughModel = PetTag.class,
throughRelationFields = "petId",
throughReferenceFields = "tagId")
@EqualsAndHashCode.Exclude @ToString.Exclude
private List<Tag> tags;
}模块需要被扫描到
框架启动时通过 Spring Boot 的 AutoConfigurationPackages 拿到业务应用包(即启动类所在包及其子包),再聚合各框架内置模块经 SPI 贡献的包,统一扫描 @Module 与 @Model。因此只要 PetShopModule 与 Pet 都落在启动类所在包之下,无需任何额外配置即可被发现。模块发现与包聚合的机制细节见 应用架构设计,包扫描的底层原理见 前置知识 · Spring 类路径扫描。
在 application.yml 打开框架启动日志:
logging:
level:
cn.cvking.forge: debug
# 生命周期默认即 INSTALL(建表 + 改表 + 系统表落库),按需显式指定
app:
lifecycle: INSTALL启动后即可在控制台看到模块/模型注册与生命周期编排的日志。
模块与模型注册阶段(BFPP)会逐条打印归属关系,可看到 petshop 模块连同它的实体被收录:
[bootstrap] registered module: system
[bootstrap] registered module: petshop
[bootstrap] registered model: sys_module -> module=system (pkg=cn.cvking.forge.boot.sys)
[bootstrap] registered model: pet -> module=petshop (pkg=com.demo.petshop.model)随后生命周期编排器按拓扑顺序处理模块,并打印「模块 N 个」的汇总:
[lifecycle] Module topology order: [system, petshop]
[lifecycle] Lifecycle=INSTALL, 模块 2 个, 扫描模型 7 个, 耗时 456ms看到 Module topology order 里出现 petshop、且「模块 N 个」计入了你的新模块,就说明声明已生效。system 排在 petshop 前,正是 dependencies = {"system"} 的拓扑结果。
把 app.lifecycle 设为 DDL,框架会进入 dry-run:只收集并输出 SQL 到目录(默认 target),写完后退出 JVM。
[DDL] Lifecycle=DDL completed, dry-run file: /path/to/target/ddl-2024-06-04.sql, JVM will exit now.若只想刷新系统表元数据而跳过 DDL,用 RELOAD,此时会打印 Lifecycle=RELOAD, DDL 链路已跳过。
现象:启动直接 fail-fast,报告存在无模块覆盖(unowned)的模型,无法启动。
原因:@Model 的所在包没有被任何模块的有效扫描包覆盖。模块的有效扫描包 = 源类所在包 + scanPackages,二者都没命中实体包时,该模型就成了孤儿。例如 PetShopModule 在 com.demo.petshop、scanPackages = {"com.demo.petshop.model"},却把 Pet 放到了 com.demo.petshop.entity。
修复:把实体移回 scanPackages 指向的包,或把实体所在包补进 scanPackages。
现象:debug 日志里只见 registered module: system,看不到 petshop;「模块 N 个」也不含你的模块。即便实体被零散收录,归属也对不上。
原因:PetShopModule 不在启动类所在包及其子包之下,AutoConfigurationPackages 拿不到它,自然扫描不到 @Module。
修复:把 @Module 类放进启动类的根包之下(与实体同一根包最稳妥)。框架如何发现模块包、SPI 如何聚合内置模块的包,见 应用架构设计。
现象:启动期校验失败,或两个模块互相覆盖导致归属混乱。
原因:code 是模块全局唯一标识,必填;两个 @Module 用了相同 code,框架无法区分。
修复:为每个模块取唯一 code(如 petshop),并用常量 MODULE_CODE 暴露给依赖方引用,避免硬编码字符串拼写漂移。
现象:Module topology order 里业务模块排在了系统模块之前,建表/落库时报依赖表不存在。
原因:dependencies 写的是模块 code 而非类名或展示名;写错了字符串等于没声明依赖,拓扑排序就只按 priority 走。
修复:dependencies 填目标模块的 code,推荐直接引用对方常量(如 SystemModule.MODULE_CODE),让编译器帮你兜底。