搜索 K
Appearance
Appearance
forge 把一个后端应用拆成若干「模块」。每个模块用一个 @Module 类声明自己的编码、版本、依赖与扫描范围;框架在启动早期把所有模块的 @Model 元数据扫描、解析、校验、注册成 Spring Bean,再按模块依赖拓扑顺序逐个建表、改表、落库。
以宠物商店为例:宠物(Pet)、分类(Category)、主人(Owner)、标签(Tag)这些实体放在业务模块里;权限、系统元数据这些通用能力则由框架内置模块提供。你只需写好模块声明与实体,启动应用即可看到表自动建好、元数据自动落库。
本篇是应用架构的入口,帮你建立全局认知并指明阅读路线。三个核心能力分别是:
@Module 声明模块边界、版本与依赖。META-INF/services 让新模块自洽地贡献扫描包,无需改启动器代码。app.lifecycle 控制建表/落库/dry-run 三种启动模式。想先理解「为什么这么设计」?
本篇是使用导向的总览。涉及 BFPP 触发时机、SPI 聚合取舍、生命周期编排链路的深入剖析,见设计篇 应用架构 · 设计总览。
下面用宠物商店演示「声明模块 → 写实体 → 启动」的最短路径。
模块声明就是一个标注了 @Module 的空类,约定俗成放在业务包的根。
package com.demo.petshop;
import cn.cvking.forge.boot.sys.SystemModule;
import cn.cvking.forge.module.annotation.Module;
@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";
}@Module 的常用参数:
| 参数 | 类型 | 说明 | 缺省 |
|---|---|---|---|
code | String | 模块唯一编码,必填 | - |
name | String | 展示名称,用于前端/运维面板 | 空(回退到 code) |
version | int | 版本号,单调递增整数,用于程序判断升级 | 1 |
versionName | String | 版本展示文本 | "1.0.0" |
priority | int | 拓扑优先级,数值越小越先处理(同层级生效) | 1000 |
dependencies | String[] | 依赖的模块 code 列表 | {} |
scanPackages | String[] | 额外的 @Model 扫描包 | {} |
关于扫描范围
不显式写 scanPackages 时,模块的有效扫描包等于 @Module 类所在包(即 sourceClass 的包)。把 PetShopModule 放在 com.demo.petshop 下,子包里的实体就会被一并扫描到。
实体用 @Model 标注,归属由「包前缀最长匹配」自动判定到 petshop 模块。这里给出主角 Pet,其余 Category、Owner、Tag 等同理。
package com.demo.petshop.model;
import cn.cvking.forge.data.model.IdModel;
import cn.cvking.forge.metadata.annotation.Field;
import cn.cvking.forge.metadata.annotation.Model;
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;
@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;
}实体本身的字段与关系映射不是本篇重点,详见对应专题文档。这里只需知道:实体被扫描后会归到某个模块名下,并据此参与建表与落库。
application.yml 里用 app.lifecycle 选择启动模式(缺省即 INSTALL,完整安装):
app:
lifecycle: INSTALL # 可选 INSTALL / RELOAD / DDL| 取值 | 含义 |
|---|---|
INSTALL | 完整安装:建表 + 改表 + 系统表落库(缺省) |
RELOAD | 跳过 DDL,仅把系统表元数据落库 |
DDL | dry-run:仅把 SQL 输出到文件,输出后退出 JVM |
直接启动 Spring Boot 应用即可。
打开框架启动日志(把 bootstrap 与 lifecycle 相关日志放到 DEBUG 即可看到完整轨迹):
logging:
level:
cn.cvking.forge: debug启动后控制台会先打印模块/模型注册轨迹(BFPP 阶段):
[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: user -> module=auth (pkg=cn.cvking.forge.auth.entity)
[bootstrap] registered model: pet -> module=petshop (pkg=com.demo.petshop.model)随后是生命周期编排阶段,先打印模块拓扑顺序(被依赖者优先,同层按 priority 升序),再执行建表与落库:
[lifecycle] Module topology order: [system, auth, petshop]
[lifecycle] Lifecycle=INSTALL, 模块 3 个, 扫描模型 12 个, 耗时 456ms
[bootstrap] RepositoryRegistry initialized with 5 repository(ies)
[bootstrap] TableInfo patched for 10/12 STORE model(s)
[bootstrap] Registered 8 dynamic MapperFactoryBean(s)
[DDL] -- Table: sys_module
CREATE TABLE `sys_module` (...)看到 Module topology order 中包含你的 petshop、且对应的表出现在 [DDL] 输出里,就说明模块声明、实体归属、建表链路全部打通。
把 app.lifecycle 设为 DDL,应用不会真正连库建表,而是把 SQL 收集后写入文件并退出:
[lifecycle] Module topology order: [system, auth, petshop]
[DDL] Lifecycle=DDL completed, dry-run file: /path/to/target/ddl-2024-06-04.sql, JVM will exit now.输出目录由 app.dryRunOutputDir 控制,缺省为 target。这适合在 CI 里审阅将要执行的 DDL。
业务应用包由 Spring Boot 的 AutoConfigurationPackages 自动获取,无需手动声明。但框架内置模块(如系统、权限)的包不在业务包之下,它们通过 SPI 把自身包「贡献」进扫描列表。
机制是 ScanPackageProvider 接口(聚合贡献模式):启动时框架遍历所有实现,把各自返回的包列表汇聚后统一扫描。
@SPI
public interface ScanPackageProvider {
/** 贡献需纳入扫描的包名列表,不可返回 null */
List<String> contributePackages();
}若你的模块独立打包、且包路径不在业务应用包之下,按三步即可让它被扫描到:
ScanPackageProvider,在 contributePackages() 里返回本模块的包,如 List.of("com.demo.petshop")。src/main/resources/META-INF/services/cn.cvking.forge.module.spi.ScanPackageProvider 文件里追加该实现类的全限定名。大多数业务工程不需要这一步
只要你的实体在 Spring Boot 主类所在包之下,AutoConfigurationPackages 已经覆盖了它们。ScanPackageProvider 主要给「独立发布、包路径游离于业务包之外」的框架级模块使用。SPI 的整体取舍与单选/聚合两种模式的区别,见设计篇 应用架构 · SPI 与扩展。
反例一:实体没被任何模块覆盖(unowned)
现象:启动直接 fail-fast 报错,提示存在无模块归属的模型。
原因:实体所在包既不在业务应用包(AutoConfigurationPackages)之下,也不在任何 @Module 的有效扫描包之内,导致它扫得到却找不到归属模块。
修复:把实体移到某个模块的包之下,或给该模块补 scanPackages;框架级模块则补 ScanPackageProvider 贡献。
反例二:一个实体被多个模块等长匹配(ambiguous)
现象:启动 fail-fast,提示某模型被多个模块以同等长度的包前缀匹配,无法判定归属。
原因:模块归属按「最长包前缀优先」判定,当两个模块的扫描包对该实体而言前缀长度相同时产生歧义。
修复:调整实体所在包,或收窄某个模块的 scanPackages,让目标模块的前缀更长、更精确。
反例三:表名为空或重复
现象:启动 fail-fast,提示 STORE 模型表名为空或与其他模型重复。
原因:两个 @Model 最终固化出了同一张表名,或某存储模型没能解析出表名。
修复:检查 @Model(name=...) 命名,确保不同实体落到不同表;命名规范参见数据建模相关文档。
反例四:模块依赖写错导致拓扑顺序不符预期
现象:日志里的 Module topology order 顺序与预期不符,被依赖模块没有先于依赖方处理。
原因:dependencies 写的是模块 code 而非类名/展示名,写错或漏写会让拓扑排序退化为仅按 priority 排序。
修复:确认 dependencies 填的是目标模块的 code 常量(如 SystemModule.MODULE_CODE),并核对日志中的拓扑顺序。
按下面顺序逐篇深入,每一步都能在宠物商店里直接验证:
@Module 各参数实战、依赖与拓扑顺序、模块边界划分。ScanPackageProvider 与其他扩展点的写法、聚合与单选两种模式。配套的设计篇剖析「为什么这么设计」: