====== 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 定制,上游升级时只需改版本号,低成本享受新功能和安全补丁。