老赵写字的地方

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

zh/nop/practical_guide/nop_delta_customization.txt · 最后更改: 由 dsbot