目录
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 指向对应 XDef --> <orm x:schema="/nop/schema/orm/orm.xdef" xmlns:x="/nop/schema/xdsl.xdef" x:extends="super"> <!-- 只写需要变动的部分,其余继承上游 --> </orm>
各文件类型对应的 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” 即可——框架自动找到上一层同名文件:
<orm x:schema="/nop/schema/orm/orm.xdef" xmlns:x="/nop/schema/xdsl.xdef" x:extends="super"> <!-- 只写变动的部分 --> </orm>
⚠️ 忘了写 x:extends,框架会把你的文件当作独立替换文件,上游内容全部丢失。典型症状:登录后菜单全空,或自定义字段生效了但上游字段也没了。
x:override
控制合并行为:
| 取值 | 含义 |
|---|---|
remove | 删除上游同名节点 |
merge | 合并(默认) |
replace | 完全替换 |
最常见用法是删除上游不需要的 Bean:
<bean id="nopNamingService" x:override="remove"/>
x:gen-extends
在 Delta 层动态生成扩展内容,通常在 ORM 模型文件中使用:
<orm x:schema="/nop/schema/orm/orm.xdef" ...> <x:gen-extends> <orm-gen:GenFromOrm modelPath="/nop/auth/orm/app.orm.xml" xpl:lib="/nop/orm/xlib/orm-gen.xlib"/> </x:gen-extends> <entities> <!-- 自定义实体或字段 --> </entities> </orm>
四、实战场景
以下场景全部来自物业管理系统项目。
场景一:ORM 模型加字段
上游 NopAuthUser 没有 phone、avatar 字段。在 Delta 层写 ORM 文件:
Delta 文件路径: _vfs/_delta/default/nop/auth/orm/app.orm.xml
<orm x:schema="/nop/schema/orm/orm.xdef" xmlns:x="/nop/schema/xdsl.xdef" x:extends="super"> <entities> <entity name="io.nop.auth.dao.entity.NopAuthUser"> <columns> <column name="phone" code="PHONE" propId="100" stdDomain="string" precision="20"/> <column name="avatar" code="AVATAR" propId="101" stdDomain="string" precision="500"/> </columns> <relations> <to-many name="orgUnits" refEntityName="io.nop.auth.dao.entity.NopAuthOrgUser"> <join> <col ref="USER_ID"/> </join> </to-many> </relations> </entity> </entities> </orm>
新增字段的 propId 从 100 开始,避开上游 1-99 的范围。
场景二:去掉不需要的 Bean
物业系统只用 Nacos 做服务发现,不需要 sys-dao 的 NamingService。在 Delta 层删掉它:
Delta 文件路径: _vfs/_delta/default/nop/sys/beans/app-dao.beans.xml
<beans x:schema="/nop/schema/beans.xdef" xmlns:x="/nop/schema/xdsl.xdef" x:extends="super"> <bean id="nopNamingService" x:override="remove"/> </beans>
场景三:覆盖 Bean 实现(Gateway 无 ORM)
网关不依赖 ORM,但上游 filter 引用了 nopSingleSessionFunctionInvoker(在 nop-orm 中)。在 Delta 层注入透传实现:
Delta 文件路径: _vfs/_delta/default/nop/gateway/beans/gateway-defaults.beans.xml
<beans x:schema="/nop/schema/beans.xdef" xmlns:x="/nop/schema/xdsl.xdef" x:extends="super"> <bean id="nopSingleSessionFunctionInvoker" class="com.estate.gateway.SimpleFunctionInvoker" ioc:default="true"/> </beans>
Java 类需实现 IFunctionInvoker 和 IAsyncFunctionInvoker 的所有方法。
场景四:自定义菜单结构
去掉 nop-auth-web 默认的“测试 nop-auth”菜单项:
Delta 文件路径: _vfs/_delta/default/nop/auth/auth/app.action-auth.xml
<auth x:extends="/nop/auth/auth/nop-auth.action-auth.xml" x:schema="/nop/schema/action-auth.xdef" xmlns:x="/nop/schema/xdsl.xdef"> <site id="main"> <resource id="test-orm-nop-auth" x:override="remove"/> </site> </auth>
注意这里 x:extends 必须指向具体路径(不能写 “super”),因为文件本身在 Delta 层,“super” 有歧义。
场景五:Biz 层方法扩展
通过 xbiz 为上游实体新增 GraphQL 查询,无需写 Java:
Delta 文件路径: _vfs/_delta/default/nop/auth/model/auth/NopAuthUser.xbiz
<biz x:schema="/nop/schema/biz/xbiz.xdef" xmlns:x="/nop/schema/xdsl.xdef" x:extends="super"> <actions> <query name="findByPhone"> <arg name="phone" type="String" mandatory="true"/> <source> <c:if test="${phone == null}"> <return/> </c:if> <orm:DoFindFirst query="select o from NopAuthUser o where o.phone = ${phone}"/> </source> </query> </actions> </biz>
五、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 定制,上游升级时只需改版本号,低成本享受新功能和安全补丁。