Skip to content

模块声明与扫描包

在 forge 里,「模块」是一组业务实体的归属单元。你用一个标注了 @Module 的类声明它,框架在启动时扫描包、把每个 @Model 归到某个模块名下,并按模块依赖关系决定建表顺序。本篇用宠物商店演示如何声明模块、组织多模块、让模型正确归属,文末列出最容易踩的归属错配与依赖坑。

想知道这一切在启动期是怎么被串起来的,见设计篇 应用架构与启动编排

一分钟上手:声明业务模块

模块声明类通常是一个空的 final 类,只承载 @Module 注解与一个模块编码常量。把宠物商店声明成一个名为 petshop 的业务模块:

java
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

java
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

@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.petshopPetCategoryOwner 等实体都在 com.demo.petshop.model 下,前缀命中 petshop,于是全部归属 petshop 模块。

验证结果

application.yml 打开 forge 启动期日志:

yaml
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 表达先后,框架据此做拓扑排序,确保被依赖模块的表先建好。

java
@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";
}
java
@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";
}

CategoryPetTag 等放进 com.demo.petshop.catalog 包,把订单相关实体放进 com.demo.petshop.order 包,两个模块各管各的包,互不串味。

验证结果

启动期生命周期编排会打印拓扑顺序,被依赖者排在前:

[lifecycle] Module topology order: [system, auth, petshop-catalog, petshop-order]
[lifecycle] Lifecycle=INSTALL, 模块 4 个, 扫描模型 12 个, 耗时 456ms

petshop-catalog 排在 petshop-order 之前,说明依赖被正确识别——目录表会先于订单表建立。

同层按 priority 升序,但不能替代 dependencies

若两个模块之间存在数据依赖(比如订单引用目录),必须用 dependencies 显式声明,不要指望仅靠 priority 数值小就「碰巧」先建表。priority 只在拓扑同层内做二级排序。

与 AutoConfigurationPackages 的关系

业务应用包与框架内置模块包,是从两条不同途径汇入扫描列表的:

  • 业务应用包:通过 Spring Boot 标准的 AutoConfigurationPackages 机制获取,通常就是你 @SpringBootApplication 主类所在的包及其子包。宠物商店的 com.demo.petshop.* 就走这条路被纳入扫描。
  • 框架内置模块包:通过 SPI 接口 ScanPackageProvider 聚合贡献。例如系统模块贡献 cn.cvking.forge.boot.sys、权限模块贡献 cn.cvking.forge.auth,框架启动时遍历所有实现并汇聚。

两条途径汇成同一份包列表后,统一扫描 @Module@Model。这意味着:只要你的实体在业务主包之下,无需任何额外配置就会被扫到;框架自带模块的包也无需你手动列出。scanPackages() 仅用于补充那些既不在业务主包、又不属于框架内置模块的「游离」包。

聚合扫描背后的 SPI 机制与启动钩子细节,见 应用架构与启动编排 以及 启动期前置机制

常见反例与排查

反例一:实体放错包,被归到隐式模块或报「无模块覆盖」

现象:日志里某实体 module= 后面不是预期的模块名,或启动直接报 unowned 模型违规。

原因:模型类的包不在任何模块的有效扫描包前缀之下。模块归属只看包前缀,与 dependencies 无关。例如把 Pet 错放进 com.demo.common.model,而 PetShopModulecom.demo.petshop,前缀对不上。

修复:把实体移回模块声明类所在包的子包(推荐),或在该模块的 @Module(scanPackages = {"com.demo.common.model"}) 里显式追加这个游离包。

反例二:硬编码 dependencies 字符串拼错

现象:启动按拓扑排序后,依赖模块没有排在前面,或建表顺序不符合预期。

原因:dependencies 里写的字符串与目标模块的 code 不一致(拼写、大小写、连字符差异),框架找不到被依赖模块,依赖边形同虚设。

修复:依赖项引用对方暴露的编码常量,例如 dependencies = {SystemModule.MODULE_CODE},让编译器替你校验,而非裸写 "sytem" 这类易错字面量。

反例三:两个模块的包互相包含,归属变模糊

现象:启动报 ambiguous 模型违规,或实体归到了「更短前缀」的那个模块。

原因:模块归属用最长前缀匹配。若模块 A 声明包 com.demo.petshop、模块 B 声明 com.demo.petshop.order,落在 com.demo.petshop.order 下的实体会优先归 B;而把两个模块都指向完全相同的包则会出现等长歧义。

修复:让各模块的扫描包互不重叠、层级清晰,一个实体只落在一个模块的包子树里。

反例四:完全不声明 @Module,却又写了跨模块依赖期望

现象:所有实体都归到隐式应用模块,[lifecycle] Module topology order 里只有兜底的一个业务模块,依赖与建表顺序无从表达。

原因:未显式声明 @Module,框架以应用主包作隐式模块兜底,自然没有 dependencies 可言。

修复:为需要表达依赖、可读名称或独立版本的业务域显式声明 @Module,再用 dependencies 串联,参见上文「多模块组织」。