搜索 K
Appearance
Appearance
在 forge 里,「模块」是一组业务实体的归属单元。你用一个标注了 @Module 的类声明它,框架在启动时扫描包、把每个 @Model 归到某个模块名下,并按模块依赖关系决定建表顺序。本篇用宠物商店演示如何声明模块、组织多模块、让模型正确归属,文末列出最容易踩的归属错配与依赖坑。
想知道这一切在启动期是怎么被串起来的,见设计篇 应用架构与启动编排。
模块声明类通常是一个空的 final 类,只承载 @Module 注解与一个模块编码常量。把宠物商店声明成一个名为 petshop 的业务模块:
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}
)
public final class PetShopModule {
public static final String MODULE_CODE = "petshop";
}把宠物商店的所有实体放进与模块声明类同包或子包下,例如 com.demo.petshop.model:
package com.demo.petshop.model;
@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;
// ……其余字段见关系字段篇
}不写 @Module 也能跑
如果业务工程一个模块都没声明,框架仍会以 Spring Boot 的应用主包作为隐式模块兜底。但一旦你想表达依赖、控制建表顺序、给运维面板一个可读的模块名,就应当显式声明 @Module。
| 参数 | 类型 | 含义 | 缺省 |
|---|---|---|---|
code() | String | 模块唯一编码,必填,作为依赖引用的键 | 无 |
name() | String | 模块展示名称,用于前端、运维面板 | 空串(回落为 code) |
version() | int | 模块版本号,单调递增整数,供程序判断升级 | 1 |
versionName() | String | 版本展示文本,如 "1.0.0" | "1.0.0" |
priority() | int | 拓扑优先级,数值越小越先处理(同层级生效) | 1000 |
dependencies() | String[] | 依赖的模块 code 列表 | {} |
scanPackages() | String[] | 额外纳入扫描的包(@Model 扫描范围补充) | {} |
几点要点:
code 是依赖关系的连接键。dependencies = {"system"} 里写的就是被依赖模块的 code,推荐引用对方暴露的常量(如 SystemModule.MODULE_CODE)而非硬编码字符串。priority 不直接等于建表顺序。框架先按依赖做拓扑排序(被依赖者优先),再在同一拓扑层级里按 priority 升序排列。系统模块声明为 priority = 0、权限模块 priority = 10、业务模块 priority = 100,从小到大体现「越基础越靠前」的约定。scanPackages 是可选补充。模块默认就会扫描声明类所在的包及其子包,只有当实体散落在声明类包之外时才需要追加。模型归属哪个模块由「包」决定,不由 dependencies 决定
dependencies 只影响建表与处理顺序,与「某个 @Model 属于哪个模块」无关。后者完全由包前缀匹配决定,见下一节。
每个 @Model 类落在哪个模块名下,由它所在的包与各模块的「有效扫描包」做最长前缀匹配得出:
scanPackages() 列出的包(对应 ModuleDefinition.effectivePackages())。@Model 后,拿它的包名去和所有模块的有效扫描包比对,命中前缀最长的那个模块即为归属。例如宠物商店:PetShopModule 位于 com.demo.petshop,其有效扫描包即 com.demo.petshop;Pet、Category、Owner 等实体都在 com.demo.petshop.model 下,前缀命中 petshop,于是全部归属 petshop 模块。
在 application.yml 打开 forge 启动期日志:
logging:
level:
cn.cvking.forge: debug启动后控制台会先打印模块注册,再打印每个模型的归属(pkg= 即用于匹配的包名):
[bootstrap] registered module: system
[bootstrap] registered module: auth
[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)
[bootstrap] registered model: category -> module=petshop (pkg=com.demo.petshop.model)
[bootstrap] registered model: owner -> module=petshop (pkg=com.demo.petshop.model)看到每个宠物商店实体后面跟着 module=petshop,归属就对了。
当工程变大,可以把宠物商店拆成多个模块。模块之间用 dependencies 表达先后,框架据此做拓扑排序,确保被依赖模块的表先建好。
@Module(
code = "petshop-catalog",
name = "商品目录",
version = 1,
versionName = "1.0.0",
priority = 100
)
public final class CatalogModule {
public static final String MODULE_CODE = "petshop-catalog";
}@Module(
code = "petshop-order",
name = "宠物订单",
version = 1,
versionName = "1.0.0",
priority = 110,
dependencies = {"petshop-catalog"}
)
public final class OrderModule {
public static final String MODULE_CODE = "petshop-order";
}把 Category、Pet、Tag 等放进 com.demo.petshop.catalog 包,把订单相关实体放进 com.demo.petshop.order 包,两个模块各管各的包,互不串味。
启动期生命周期编排会打印拓扑顺序,被依赖者排在前:
[lifecycle] Module topology order: [system, auth, petshop-catalog, petshop-order]
[lifecycle] Lifecycle=INSTALL, 模块 4 个, 扫描模型 12 个, 耗时 456mspetshop-catalog 排在 petshop-order 之前,说明依赖被正确识别——目录表会先于订单表建立。
同层按 priority 升序,但不能替代 dependencies
若两个模块之间存在数据依赖(比如订单引用目录),必须用 dependencies 显式声明,不要指望仅靠 priority 数值小就「碰巧」先建表。priority 只在拓扑同层内做二级排序。
业务应用包与框架内置模块包,是从两条不同途径汇入扫描列表的:
AutoConfigurationPackages 机制获取,通常就是你 @SpringBootApplication 主类所在的包及其子包。宠物商店的 com.demo.petshop.* 就走这条路被纳入扫描。ScanPackageProvider 聚合贡献。例如系统模块贡献 cn.cvking.forge.boot.sys、权限模块贡献 cn.cvking.forge.auth,框架启动时遍历所有实现并汇聚。两条途径汇成同一份包列表后,统一扫描 @Module 与 @Model。这意味着:只要你的实体在业务主包之下,无需任何额外配置就会被扫到;框架自带模块的包也无需你手动列出。scanPackages() 仅用于补充那些既不在业务主包、又不属于框架内置模块的「游离」包。
聚合扫描背后的 SPI 机制与启动钩子细节,见 应用架构与启动编排 以及 启动期前置机制。
现象:日志里某实体 module= 后面不是预期的模块名,或启动直接报 unowned 模型违规。
原因:模型类的包不在任何模块的有效扫描包前缀之下。模块归属只看包前缀,与 dependencies 无关。例如把 Pet 错放进 com.demo.common.model,而 PetShopModule 在 com.demo.petshop,前缀对不上。
修复:把实体移回模块声明类所在包的子包(推荐),或在该模块的 @Module(scanPackages = {"com.demo.common.model"}) 里显式追加这个游离包。
现象:启动按拓扑排序后,依赖模块没有排在前面,或建表顺序不符合预期。
原因:dependencies 里写的字符串与目标模块的 code 不一致(拼写、大小写、连字符差异),框架找不到被依赖模块,依赖边形同虚设。
修复:依赖项引用对方暴露的编码常量,例如 dependencies = {SystemModule.MODULE_CODE},让编译器替你校验,而非裸写 "sytem" 这类易错字面量。
现象:启动报 ambiguous 模型违规,或实体归到了「更短前缀」的那个模块。
原因:模块归属用最长前缀匹配。若模块 A 声明包 com.demo.petshop、模块 B 声明 com.demo.petshop.order,落在 com.demo.petshop.order 下的实体会优先归 B;而把两个模块都指向完全相同的包则会出现等长歧义。
修复:让各模块的扫描包互不重叠、层级清晰,一个实体只落在一个模块的包子树里。
现象:所有实体都归到隐式应用模块,[lifecycle] Module topology order 里只有兜底的一个业务模块,依赖与建表顺序无从表达。
原因:未显式声明 @Module,框架以应用主包作隐式模块兜底,自然没有 dependencies 可言。
修复:为需要表达依赖、可读名称或独立版本的业务域显式声明 @Module,再用 dependencies 串联,参见上文「多模块组织」。