Skip to content

快速开始:声明一个模块

forge 把一组相关的实体(@Model)归拢到一个业务模块(@Module)之下。模块是建表顺序、元数据落库、依赖拓扑的基本单元。本篇用宠物商店的 petshop 模块走通最短路径:声明模块 → 让模块扫描到实体 → 启动后从 debug 日志确认「模块 N 个」。

阅读前提

  • JDK 17+
  • Spring Boot 3.x
  • 已能用 @Model 声明实体(参见 DDL 篇的快速开始)

想了解模块为何这样编排、BFPP 与 SPI 在启动期如何协作,请转 应用架构设计

三步上手

1. 声明一个 @Module

新建一个标注了 @Module 的类,放在业务模型包的根位置。code 必填且全局唯一,scanPackages 指向你的模型所在包。

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},
        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_modulesys_model 等元数据表。业务模块声明 dependencies = {"system"} 后,建表与元数据落库会排在系统模块之后,避免依赖表尚未就绪。system 模块的 priority0,天然最先处理。

2. 让模块扫描到实体

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 防止递归栈溢出):

java
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。因此只要 PetShopModulePet 都落在启动类所在包之下,无需任何额外配置即可被发现。模块发现与包聚合的机制细节见 应用架构设计,包扫描的底层原理见 前置知识 · Spring 类路径扫描

3. 开启 debug 日志并启动

application.yml 打开框架启动日志:

yaml
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"} 的拓扑结果。

只想看建表 SQL、不真正建表?

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,二者都没命中实体包时,该模型就成了孤儿。例如 PetShopModulecom.demo.petshopscanPackages = {"com.demo.petshop.model"},却把 Pet 放到了 com.demo.petshop.entity

修复:把实体移回 scanPackages 指向的包,或把实体所在包补进 scanPackages

反例二:模块类压根没被扫描到

现象:debug 日志里只见 registered module: system,看不到 petshop;「模块 N 个」也不含你的模块。即便实体被零散收录,归属也对不上。

原因:PetShopModule 不在启动类所在包及其子包之下,AutoConfigurationPackages 拿不到它,自然扫描不到 @Module

修复:把 @Module 类放进启动类的根包之下(与实体同一根包最稳妥)。框架如何发现模块包、SPI 如何聚合内置模块的包,见 应用架构设计

反例三:code 重复或漏填

现象:启动期校验失败,或两个模块互相覆盖导致归属混乱。

原因:code 是模块全局唯一标识,必填;两个 @Module 用了相同 code,框架无法区分。

修复:为每个模块取唯一 code(如 petshop),并用常量 MODULE_CODE 暴露给依赖方引用,避免硬编码字符串拼写漂移。

反例四:依赖名写错,拓扑顺序不对

现象Module topology order 里业务模块排在了系统模块之前,建表/落库时报依赖表不存在。

原因:dependencies 写的是模块 code 而非类名或展示名;写错了字符串等于没声明依赖,拓扑排序就只按 priority 走。

修复:dependencies 填目标模块的 code,推荐直接引用对方常量(如 SystemModule.MODULE_CODE),让编译器帮你兜底。