====== Nop Platform 的 Delta 定制机制 ====== Nop Platform 的 Delta 定制(Delta 层 / VFS 叠加)是其最核心的扩展能力。它让你能不修改上游框架源码,只通过「写差异文件」的方式完成业务定制。这种机制在 Java 生态中相对罕见——熟悉 Spring Boot 的开发者可能会更习惯用「继承 + 覆盖 Bean」或「fork 源码改」,但 Delta 提供了第三种、也是更优雅的选择。 本文以物业管理系统项目中的真实案例为素材,从原理到实战逐步拆解。 ===== 一、Delta 是什么 ===== ==== 传统方案的问题 ==== 在进入 Delta 之前,先看看传统方案有什么痛点: ^ **方案** ^ **问题** ^ | fork 上游源码直接改 | 上游一升级, merge 冲突地狱,长期陷入技术债 | | 继承 + Override | 可覆盖的范围有限,XML 配置、ORM 模型、页面模板无法继承 | | 插件 / SPI 机制 | 框架必须预设扩展点,漏一个就改不了 | Delta 的思路完全不同:**不改源头,只写差异。** 在 ''_vfs/_delta/default/'' 下放一个与上游同路径的文件,用 ''x:extends'' 引用上游,只写你需要变动的部分,框架启动时自动合并。这就是 Delta 定制的全部。 ===== 二、目录结构与层叠规则 ===== 了解了 Delta 的思路之后,下一步自然就是:**入口在哪?** 答案在 ''_vfs/_delta/default/'' 目录。上游 JAR 把所有可供定制的资源——ORM 模型、Bean 配置、业务逻辑——都放在各自 ''_vfs/'' 中,你要在 ''_vfs/_delta/default/'' 下建一个同路径的文件,框架会自动合并。 下面先看目录结构怎么组织的。 ==== VFS 分层 ==== Nop 的 VFS(Virtual File System)是一个多层文件系统。每个模块 JAR 中的 ''_vfs/'' 资源构成基础层,你的项目通过 ''_vfs/_delta/{deltaDir}/'' 构成定制层: 你的模块 src/main/resources/ └── _vfs/ └── _delta/ └── default/ ← Delta 层名 └── nop/auth/ ← 与上游 _vfs/ 下的路径一致 ├── orm/app.orm.xml ├── beans/ └── model/ **关键规则**:Delta 文件的完整路径(目录 + 文件名)必须与上游 JAR 中 ''_vfs/'' 下的路径一字不差。上游是 ''nop/auth/orm/app.orm.xml'',Delta 就是 ''_vfs/_delta/default/nop/auth/orm/app.orm.xml''。路径或文件名错了不会报错,只是不生效。 ==== Delta 层文件命名规则 ==== 上游 ''_vfs/'' 下的文件名不是随便起的,目的是按「类型 + 用途」拆分文件,让 Delta 覆盖的粒度更精准: ^ **文件** ^ **用途** ^ **覆盖粒度** ^ | ''app.orm.xml'' | ORM 模型(实体、字段、关联),代码生成器输入 | 模块级 | | ''app.action-auth.xml'' | 站点地图/菜单树/操作权限 | 模块级 | | ''app.data-auth.xml'' | 行级/列级数据过滤规则 | 模块级 | | ''*beans.xml'' | IoC Bean 配置,按功能拆分 | 按功能 | | ''{实体名}.xbiz'' | 实体业务逻辑扩展 | 按实体 | | ''{实体名}.xmeta'' | 实体元数据(显示标签、校验规则) | 按实体 | | ''_module'' | 模块标记文件 | 一个文件 | 拆得越细,Delta 覆盖就越精准——只想改 NopAuthUser 的业务逻辑,只覆盖它的 ''NopAuthUser.xbiz'' 就够了。 ==== 层序优先级 ==== 如果有多个 Delta 层(如 ''default'' + ''customer-a''),后加载的优先级更高。在 ''application.yaml'' 中指定: nop: vfs: delta-layer-ids: default,customer-a ==== Delta XML 文件的通用规则 ==== 所有 Delta 层的 XML 文件都遵循同一套格式约定: ^ **要素** ^ **说明** ^ | 文件编码 | UTF-8 | | 根标签 | 与上游文件一致(''orm''、''beans''、''auth''、''biz'' 等)| | ''x:schema'' | 必须指定对应的 XDef 模式定义,如 ''/nop/schema/orm/orm.xdef'' | | ''xmlns:x'' | 固定为 ''/nop/schema/xdsl.xdef'',提供 ''x:extends''、''x:override'' 等能力 | | ''x:extends'' | 覆盖上游时写 ''"super"'',新增独立文件时不写 | 一个典型的 Delta XML 文件模板: 各文件类型对应的 ''x:schema'': ^ **文件类型** ^ **x:schema** ^ | ''app.orm.xml'' | ''/nop/schema/orm/orm.xdef'' | | ''*beans.xml'' | ''/nop/schema/beans.xdef'' | | ''app.action-auth.xml'' | ''/nop/schema/action-auth.xdef'' | | ''app.data-auth.xml'' | ''/nop/schema/data-auth.xdef'' | | ''{实体名}.xbiz'' | ''/nop/schema/biz/xbiz.xdef'' | ==== `_` 前缀文件与手写文件的关系 ==== 在 `_vfs/` 中经常看到带 `_` 前缀的文件,比如 `default/_app.orm.xml`。这是 codegen(nop-cli)自动生成的产物,**不要手动修改它**。 它不会独立加载,必须被上层的入口文件通过 `x:gen-extends` 引用才能生效: nop/auth/orm/ ├── app.orm.xml ← 手写入口,x:gen-extends 引用下行 └── default/ └── _app.orm.xml ← codegen 自动生成,不直接使用 如果你在 Delta 层想定制 ORM 模型,覆盖的是上层的 `app.orm.xml`,而不是 `_app.orm.xml`: _delta/default/nop/auth/orm/ └── app.orm.xml ← 你手写,x:extends="super" | 文件 | 角色 | 覆盖入口? | |---|---|---| | `app.orm.xml` | 入口文件,聚合所有 ORM 定义 | ✅ 在这里覆盖 | | `default/_app.orm.xml` | codegen 生成的实体细节 | ❌ 不直接覆盖,通过上层引入 | `_` 前缀的含义就是「自动生成,不要手动改」。 ===== 三、核心注解三件套 ===== Delta 文件通过以下三个注解控制与上游文件的合并行为。 ==== x:extends ==== 告诉框架「我要继承哪个上游文件」。大部分情况填 ''"super"'' 即可——框架自动找到上一层同名文件: **⚠️** 忘了写 ''x:extends'',框架会把你的文件当作独立替换文件,上游内容全部丢失。典型症状:登录后菜单全空,或自定义字段生效了但上游字段也没了。 ==== x:override ==== 控制合并行为: ^ **取值** ^ **含义** ^ | ''remove'' | 删除上游同名节点 | | ''merge'' | 合并(默认) | | ''replace'' | 完全替换 | 最常见用法是删除上游不需要的 Bean: ==== x:gen-extends ==== 在 Delta 层动态生成扩展内容,通常在 ORM 模型文件中使用: ===== 四、实战场景 ===== 以下场景全部来自物业管理系统项目。 ==== 场景一:ORM 模型加字段 ==== 上游 ''NopAuthUser'' 没有 ''phone''、''avatar'' 字段。在 Delta 层写 ORM 文件: //Delta 文件路径:// ''_vfs/_delta/default/nop/auth/orm/app.orm.xml'' 新增字段的 ''propId'' 从 100 开始,避开上游 1-99 的范围。 ==== 场景二:去掉不需要的 Bean ==== 物业系统只用 Nacos 做服务发现,不需要 ''sys-dao'' 的 NamingService。在 Delta 层删掉它: //Delta 文件路径:// ''_vfs/_delta/default/nop/sys/beans/app-dao.beans.xml'' ==== 场景三:覆盖 Bean 实现(Gateway 无 ORM)===== 网关不依赖 ORM,但上游 filter 引用了 ''nopSingleSessionFunctionInvoker''(在 ''nop-orm'' 中)。在 Delta 层注入透传实现: //Delta 文件路径:// ''_vfs/_delta/default/nop/gateway/beans/gateway-defaults.beans.xml'' Java 类需实现 ''IFunctionInvoker'' 和 ''IAsyncFunctionInvoker'' 的所有方法。 ==== 场景四:自定义菜单结构 ==== 去掉 ''nop-auth-web'' 默认的"测试 nop-auth"菜单项: //Delta 文件路径:// ''_vfs/_delta/default/nop/auth/auth/app.action-auth.xml'' 注意这里 ''x:extends'' 必须指向具体路径(不能写 ''"super"''),因为文件本身在 Delta 层,''"super"'' 有歧义。 ==== 场景五:Biz 层方法扩展 ==== 通过 ''xbiz'' 为上游实体新增 GraphQL 查询,无需写 Java: //Delta 文件路径:// ''_vfs/_delta/default/nop/auth/model/auth/NopAuthUser.xbiz'' ===== 五、Delta 的边界 ===== ^ **能做的** ^ **不能做的** ^ | ORM 模型新增字段 / 实体 | 修改 Java 类的方法实现 → 用继承 | | Bean 配置覆盖 / 新增 | 删除 Java 类的字段或方法 → 改模型重生成 | | xbiz 业务逻辑扩展 | 覆盖 codegen 产物(''_gen/''、''_*.xml'')→ 改模型 | | GraphQL 扩展字段(@BizLoader)| 修改上游 XDef 元模型 | | 页面 / 菜单调整 | 修改上游 API 接口签名 | **通用原则**:Delta 文件作用于 VFS 资源层(XML、页面、配置),不作用于 Java 类层。需要改 Java 行为时,用 OO 继承——继承上游 BizModel 或 Service 类,用 ''@Named'' 注册到 IoC 容器。 ===== 六、常见陷阱 ===== ==== 陷阱 1:Delta 路径或文件名配错了 ==== 上游文件在 ''_vfs/nop/auth/orm/app.orm.xml'',Delta 必须是: _vfs/_delta/default/nop/auth/orm/app.orm.xml 而不是: _vfs/_delta/default/app.orm.xml ← 目录不对 _vfs/_delta/default/nop/auth/orm/my_app.orm.xml ← 文件名不对 _vfs/_delta/default/nop/orm/app.orm.xml ← 目录 auth 掉了 **排查**:启动加 ''nop.debug=true'',看控制台 VFS 加载日志,检查 Delta 文件是否被匹配到。 ==== 陷阱 2:忘了写 x:extends ==== 写了 Delta 文件但没写 ''x:extends="super"'',上游全部丢失。症状:菜单全空、ORM 字段不全。 ==== 陷阱 3:propId 冲突 ==== 新增字段的 ''propId'' 从 **100 开始**,避开上游 1-99 的范围,否则上游升级后冲突。 ==== 陷阱 4:忘了加依赖 ==== Delta 文件引用了类或模型,但 pom.xml 没有对应依赖。Maven 编译能过(资源文件不参与编译),运行时才失败。 ===== 七、总结 ===== Delta 是 Nop Platform 最具特色的机制。它从根本上解决了框架定制的老大难——既不像 fork 源码那样难升级,也不像 SPI 扩展那样受限于预设扩展点。 **最佳实践**: * 优先用 Delta,不要一上来就 Copy 源码 * Delta + 代码生成器配合效果最佳 * 代码生成产物(''_gen/'')不手动修改,改模型重新生成 * 框架团队维护上游,业务团队写 Delta,互不干扰 掌握 Delta 定制,上游升级时只需改版本号,低成本享受新功能和安全补丁。