Skip to content

应用架构 · 总览

forge 把一个后端应用拆成若干「模块」。每个模块用一个 @Module 类声明自己的编码、版本、依赖与扫描范围;框架在启动早期把所有模块的 @Model 元数据扫描、解析、校验、注册成 Spring Bean,再按模块依赖拓扑顺序逐个建表、改表、落库。

以宠物商店为例:宠物(Pet)、分类(Category)、主人(Owner)、标签(Tag)这些实体放在业务模块里;权限、系统元数据这些通用能力则由框架内置模块提供。你只需写好模块声明与实体,启动应用即可看到表自动建好、元数据自动落库。

本篇是应用架构的入口,帮你建立全局认知并指明阅读路线。三个核心能力分别是:

  • 模块化组织:用 @Module 声明模块边界、版本与依赖。
  • SPI 扩展:用 META-INF/services 让新模块自洽地贡献扫描包,无需改启动器代码。
  • 启动生命周期:用 app.lifecycle 控制建表/落库/dry-run 三种启动模式。

想先理解「为什么这么设计」?

本篇是使用导向的总览。涉及 BFPP 触发时机、SPI 聚合取舍、生命周期编排链路的深入剖析,见设计篇 应用架构 · 设计总览

三分钟跑通一个模块

下面用宠物商店演示「声明模块 → 写实体 → 启动」的最短路径。

第一步:声明业务模块

模块声明就是一个标注了 @Module 的空类,约定俗成放在业务包的根。

java
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 的常用参数:

参数类型说明缺省
codeString模块唯一编码,必填-
nameString展示名称,用于前端/运维面板空(回退到 code
versionint版本号,单调递增整数,用于程序判断升级1
versionNameString版本展示文本"1.0.0"
priorityint拓扑优先级,数值越小越先处理(同层级生效)1000
dependenciesString[]依赖的模块 code 列表{}
scanPackagesString[]额外的 @Model 扫描包{}

关于扫描范围

不显式写 scanPackages 时,模块的有效扫描包等于 @Module 类所在包(即 sourceClass 的包)。把 PetShopModule 放在 com.demo.petshop 下,子包里的实体就会被一并扫描到。

第二步:写实体

实体用 @Model 标注,归属由「包前缀最长匹配」自动判定到 petshop 模块。这里给出主角 Pet,其余 CategoryOwnerTag 等同理。

java
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,完整安装):

yaml
app:
  lifecycle: INSTALL   # 可选 INSTALL / RELOAD / DDL
取值含义
INSTALL完整安装:建表 + 改表 + 系统表落库(缺省)
RELOAD跳过 DDL,仅把系统表元数据落库
DDLdry-run:仅把 SQL 输出到文件,输出后退出 JVM

直接启动 Spring Boot 应用即可。

验证结果

打开框架启动日志(把 bootstraplifecycle 相关日志放到 DEBUG 即可看到完整轨迹):

yaml
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] 输出里,就说明模块声明、实体归属、建表链路全部打通。

DDL(dry-run)模式跑起来是什么样?

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。

SPI 扩展:让新模块自己贡献扫描包

业务应用包由 Spring Boot 的 AutoConfigurationPackages 自动获取,无需手动声明。但框架内置模块(如系统、权限)的包不在业务包之下,它们通过 SPI 把自身包「贡献」进扫描列表。

机制是 ScanPackageProvider 接口(聚合贡献模式):启动时框架遍历所有实现,把各自返回的包列表汇聚后统一扫描。

java
@SPI
public interface ScanPackageProvider {
    /** 贡献需纳入扫描的包名列表,不可返回 null */
    List<String> contributePackages();
}

若你的模块独立打包、且包路径不在业务应用包之下,按三步即可让它被扫描到:

  1. 实现 ScanPackageProvider,在 contributePackages() 里返回本模块的包,如 List.of("com.demo.petshop")
  2. src/main/resources/META-INF/services/cn.cvking.forge.module.spi.ScanPackageProvider 文件里追加该实现类的全限定名。
  3. 启动应用,BFPP 会自动发现并聚合该贡献。

大多数业务工程不需要这一步

只要你的实体在 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),并核对日志中的拓扑顺序。

阅读路线

按下面顺序逐篇深入,每一步都能在宠物商店里直接验证:

  1. 快速上手(getting-started):从零搭一个可启动的宠物商店工程,跑通建表与落库。
  2. 模块化组织(modules):@Module 各参数实战、依赖与拓扑顺序、模块边界划分。
  3. SPI 扩展(spi):ScanPackageProvider 与其他扩展点的写法、聚合与单选两种模式。

配套的设计篇剖析「为什么这么设计」: