====== Nop 实战:新建项目与已有模块扩展 ======
程序员在开发中有两个常见的情景。一个是从零开始构建一个项目,这个时候我们先根据项目需求,创建 model,产出 Excel 文件,再用 nop-cli 生成代码;另一种情形是使用已有的代码,就像前面我们做的一样——基于 nop-auth 扩展出我们自己的认证微服务。
本文分别介绍这两种情形。
===== 情景一:从零开始构建一个新项目 =====
配置好环境后,如何快速写一个 API?Nop 平台的流程是**模型驱动**的:设计 Excel 数据模型 → 代码生成 → 增量定制。
=== 第一步:设计 Excel 数据模型 ===
在项目根目录下创建 ''model'' 文件夹。Nop 使用特定格式的 Excel 文件(.orm.xlsx)作为模型定义源,一个文件包含多个 Sheet。
以 ''my-app.orm.xlsx'' 为例,说明各 Sheet 的格式。
**1. 目录 Sheet**——列出所有实体和对应的表名:
^ 对象名 ^ 表名 ^ 中文名 ^ 说明 ^
| Article | my_article | 文章 | 文章管理 |
**2. 配置 Sheet**——应用名、包名、数据库方言等:
^ 键 ^ 值 ^ 说明 ^
| appName | my-app | |
| maven.groupId | com.example | |
| maven.artifactId | my-app | |
| entityPackageName | com.example.myapp.entity | |
| dialect | mysql | |
**3. 域定义 Sheet**——自定义数据类型:
^ 名称 ^ 类型 ^ 长度 ^ 小数位数 ^ 标准域 ^
| userName | VARCHAR | 50 | | |
| email | VARCHAR | 100 | | |
| phone | VARCHAR | 50 | | |
| image | VARCHAR | 100 | file | |
**4. 实体 Sheet**——每个实体一张,以 ''Article'' 为例,列头为 Nop ORM 标准 14 列:
^ 编号 ^ 主键 ^ 标签 ^ 字段名 ^ 显示 ^ 英文名 ^ 中文名 ^ 数据域 ^ 非空 ^ 类型 ^ 长度 ^ 小数位数 ^ 缺省值 ^ 字典 ^
| 1 | PK | seq | articleId | | articleId | 文章 ID | | TRUE | VARCHAR | 50 | | | |
| 2 | | | title | | title | 文章标题 | | TRUE | VARCHAR | 200 | | | |
| 3 | | | content | | content | 文章内容 | | TRUE | VARCHAR | 4000 | | | |
| 4 | | | author | | author | 作者 | | | VARCHAR | 100 | | | |
| 5 | | clock | createTime | | createTime | 创建时间 | | | DATE | | | | |
各列含义:
* **编号** — 字段序号,从 1 开始
* **主键** — 标记 PK 的字段为主键
* **标签** — 特殊标记,如 seq(自动编号)、clock(时间戳)、disp(显示用)
* **字段名** — Java 属性名,自动转为下划线数据库列名
* **显示** — 表单显示控制(X=隐藏、C=创建、R=只读等)
* **英文名 / 中文名** — 字段标签
* **数据域** — 引用域定义中的域名
* **非空** — TRUE 表示 NOT NULL
* **类型** — VARCHAR、INTEGER、DATE、TIMESTAMP、TEXT 等
* **长度** — 字符串长度
* **小数位数** — 数字精度
* **缺省值** — 数据库默认值
* **字典** — 关联的字典编码
=== 第二步:运行代码生成器 ===
Nop 平台提供 nop-cli 命令行工具,可以直接通过 CLI 调用代码生成。nop-cli 是一个可独立运行的 JAR,不需要 Maven 插件配置。
# 使用 nop-cli 从 Excel 模型生成代码
java -jar nop-cli.jar gen \
-o=./my-app \
-t=/nop/templates/orm \
./model/my-app.orm.xlsx
生成的目录结构:
my-app/
_gen/ ← 自动生成的代码
model/
_App.orm.xml ← ORM 模型定义
_Article.java ← 实体类
service/
_ArticleBizModel.java ← CRUD 服务
api/
_ArticleApi.java ← API 接口
src/
main/
resources/
my-app.orm.xml ← 可定制的 ORM 模型(继承 _gen 版本)
''_gen'' 目录下的文件都以 ''_'' 开头,标记为"只读生成"——不要手动修改。定制化在 ''src/'' 中完成。
=== 第三步:编写 Delta 定制层 ===
自动生成的服务已经可以运行,提供了完整的 CRUD 操作。如果需要定制,不要修改 _gen 下的代码,而是添加 Delta 层。
例如添加一个"获取文章统计"的接口:
在 ''src/main/resources/_my-app/service/'' 下创建文件,覆盖自动生成的服务定义:
在 Java 中编写扩展类:
public class ArticleBizModelEx extends _ArticleBizModel {
@BizQuery
public List getRecentPosts(@Name("days") int days) {
return dao().findRecent(days);
}
}
Delta 层的 Java 文件放在 ''src/main/java/'' 下,与自动生成的代码隔离。
=== 第四步:启动服务 ===
在 my-app 模块中配置好 quarkus-maven-plugin 后,运行:
cd my-app
./mvnw compile quarkus:dev
启动后,自动注册的 API 端点:
* GraphQL:http://localhost:8080/graphql
* REST:http://localhost:8080/r/ArticleBizModel__save
* REST:http://localhost:8080/r/ArticleBizModel__get?id=xxx
* REST:http://localhost:8080/r/ArticleBizModel__findPage
=== 第五步:测试 API ===
**保存一篇文章:**
curl -X POST http://localhost:8080/r/ArticleBizModel__save \
-H "Content-Type: application/json" \
-H "nop-tenant: 0" \
-d '{"data":{"title":"Hello Nop","content":"我的第一篇文章","author":"test"}}'
**查询文章列表:**
curl -X POST http://localhost:8080/r/ArticleBizModel__findPage \
-H "Content-Type: application/json" \
-H "nop-tenant: 0" \
-d '{"query":{"title__contains":"Nop"}}'
===== 情景二:基于已有项目进行扩展(Delta 定制) =====
在实际项目中,更多时候不是从零开始,而是**对已有的模块进行扩展**。Nop 平台的可逆计算(Delta)机制使得我们可以在**不修改上游源码**的前提下,为已有的模块增加新功能。
下面以我们实际的工程为例——基于 nop-auth 扩展出物业系统的认证微服务(tom-auth-app)。整个模块的目录结构如下(标注了新增和生成的文件):
tom-auth-app/
├── pom.xml ← 新建:Maven 配置,依赖 nop-auth-*
├── model/
│ └── tom-auth.orm.xml ← 新建:源模型,定义新增实体
├── postcompile/
│ └── gen-orm.xgen ← 新建:代码生成脚本
└── src/main/
├── java/com/estate/auth/
│ ├── TomAuthApplication.java ← 新建:Quarkus 启动入口
│ ├── TomAuthCodeGen.java ← 新建:代码生成启动器
│ ├── entity/
│ │ ├── OrgUnit.java ← 生成:实体包装类
│ │ ├── UserOrgRel.java ← 生成:实体包装类
│ │ └── _gen/
│ │ ├── _OrgUnit.java ← 生成:实体基类
│ │ └── _UserOrgRel.java ← 生成:实体基类
│ ├── biz/
│ │ ├── IOrgUnitBiz.java ← 生成:业务接口
│ │ └── IUserOrgRelBiz.java ← 生成:业务接口
│ └── dao/
│ ├── NopAuthDaoConstants.java ← 生成:DAO 常量
│ └── _NopAuthDaoConstants.java ← 生成:DAO 常量基类
└── resources/
├── application.yaml ← 新建:应用配置
└── _vfs/_delta/default/nop/auth/
├── _module ← 生成:模块声明
├── orm/
│ ├── app.orm.xml ← 生成:Delta ORM 入口
│ └── default/_app.orm.xml ← 生成:Delta ORM 定义
└── beans/
└── _dao.beans.xml ← 生成:DAO Bean 注册
说明:
* **新建**(手工编写):pom.xml、model/ 源模型、postcompile/ 脚本、TomAuthApplication.java、TomAuthCodeGen.java、application.yaml
* **生成**(从模型自动产出):实体类、业务接口、DAO 常量、Delta VFS 资源
=== 第一步:引入上游模块为依赖 ===
在 ''pom.xml'' 中引入上游 nop-auth 模块为依赖,不把源码复制到项目里:
io.github.entropy-cloud
nop-auth-dao
io.github.entropy-cloud
nop-auth-service
io.github.entropy-cloud
nop-auth-web
上游模块的 VFS(虚拟文件系统)资源会随 JAR 一起加载,包含 ORM 模型、xbiz 业务逻辑、AMIS 页面等。
=== 第二步:创建 ORM 源模型 ===
在 ''model/'' 目录下编写 ''tom-auth.orm.xml'',定义新增的实体和字段。这些实体是对上游的补充,而非替代。
=== 第三步:配置代码生成器 ===
在 ''postcompile/gen-orm.xgen'' 中编写代码生成脚本,指定模型路径和模板:
codeGenerator.withTargetDir("../")
.renderModel('../model/tom-auth.orm.xml',
'/nop/templates/orm-delta', '/', $scope);
在 ''pom.xml'' 中配置 exec-maven-plugin,绑定到 ''process-classes'' 阶段,指向代码生成启动器 ''TomAuthCodeGen'':
org.codehaus.mojo
exec-maven-plugin
3.1.0
codegen
process-classes
java
com.estate.auth.codegen.TomAuthCodeGen
运行 ''mvn package'' 时,编译完成后自动触发代码生成:TomAuthCodeGen 启动 → 加载 ORM 模型 → 根据 ''orm-delta'' 模板生成实体类和 Delta VFS 资源。
代码生成器产出的内容(对应目录结构中标为"生成"的文件):
**Java 实体类:**
* ''entity/OrgUnit.java'' / ''entity/UserOrgRel.java'' — 可定制的实体包装类
* ''entity/_gen/_OrgUnit.java'' / ''entity/_gen/_UserOrgRel.java'' — 生成的基类(含 getter/setter/ORM 方法)
* ''biz/IOrgUnitBiz.java'' / ''biz/IUserOrgRelBiz.java'' — 业务接口
* ''dao/NopAuthDaoConstants.java'' / ''dao/_NopAuthDaoConstants.java'' — DAO 常量
**Delta VFS 资源(放在 ''_vfs/_delta/default/nop/auth/'' 下):**
* ''orm/app.orm.xml'' — Delta ORM 入口,标记为 ''x:extends="super,default/_app.orm.xml"''
* ''orm/default/_app.orm.xml'' — 实际的 Delta ORM 定义
* ''beans/_dao.beans.xml'' — DAO Bean 注册
* ''_module'' — 模块声明
启动时,上游 nop-auth 的 ORM 和我们的 Delta ORM 自动合并,新增的 OrgUnit 和 UserOrgRel 实体就生效了。
=== 第四步:编写应用启动入口 ===
在 ''src/main/java/com/estate/auth/'' 下创建两个 Java 文件。
**TomAuthApplication.java**——Quarkus 应用入口,负责启动 HTTP 服务和 Nop 平台:
@QuarkusMain
public class TomAuthApplication {
static String[] globalArgs;
public void start(@Observes StartupEvent event) {
QuarkusIntegration.start();
new NopApplication().run(globalArgs);
}
public void stop(@Observes ShutdownEvent event) {
CoreInitialization.destroy();
}
public static void main(String... args) {
globalArgs = args;
Quarkus.run(args);
}
}
**TomAuthCodeGen.java**——代码生成启动器,放在同一个包下,负责初始化 Nop、执行 ''postcompile/'' 下的 ''.xgen'' 脚本:
public class TomAuthCodeGen {
public static void main(String[] args) {
AppConfig.getConfigProvider().updateConfigValue(
CoreConfigs.CFG_CORE_MAX_INITIALIZE_LEVEL,
CoreConstants.INITIALIZER_PRIORITY_ANALYZE);
CoreInitialization.initialize();
try {
File projectDir = MavenDirHelper.projectDir(TomAuthCodeGen.class);
XCodeGenerator.runPostcompile(projectDir, "/", false);
} finally {
CoreInitialization.destroy();
}
}
}
然后配置 ''application.yaml'',放到 ''src/main/resources/'' 下:
nop:
auth:
login:
allow-create-default-user: true
jwt:
enc-key: your-jwt-secret-key
site-map:
static-config-path: /nop/auth/auth/app.action-auth.xml
orm:
init-database-schema: true
datasource:
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:mem:test
=== 第五步:编译打包为独立微服务 ===
mvn clean package -DskipTests -Dquarkus.package.type=uber-jar
产物是一个可独立运行的 uber-jar,包含了上游 nop-auth 的全部功能和我们的 Delta 定制:
java -jar tom-auth-app/target/tom-auth-app-1.0.0-SNAPSHOT-runner.jar
启动后:
* 自动加载上游 nop-auth 所有功能(用户/角色/权限/部门管理)
* 自动合并 Delta 定制(OrgUnit 组织架构、UserOrgRel 用户组织关系)
* GraphQL API 和 AMIS 管理页面全部可用
===== REST API 路由说明 =====
Nop 平台所有业务对象的 REST API 统一使用以下 URL 格式:
/r/{BizObjName}__{method}
例如:
* ''/r/LoginApi__login'' → 调用 LoginApi 的 login 方法
* ''/r/SiteMapApi__getSiteMap'' → 调用 SiteMapApi 的 getSiteMap 方法
* ''/r/NopAuthUser__findPage'' → 调用 NopAuthUser 的 findPage 方法
其中 ''/r/'' 是 Nop 框架的固定路由前缀,**不是转义符**。完整的路由体系:
^ 前缀 ^ 用途 ^ 说明 ^
| ''/r/'' | REST API | 调用任意业务对象的 CRUD 或自定义方法 |
| ''/graphql'' | GraphQL | GraphQL 查询端点 |
| ''/p/'' | 页面资源 | AMIS 页面 JSON 配置 |
| ''/f/'' | 文件资源 | 文件上传/下载 |
| ''/q/'' | 查询 | 简化查询接口 |
网关和 Vite 代理需要正确转发这些路径,确保 ''/r/'' 请求到达对应的微服务。
===== 两种情景的对比 =====
^ 环节 ^ 从零开始 ^ 扩展已有项目 ^
| 模型来源 | 新建 Excel 定义 | 编写 ORM XML,引用上游实体 |
| 代码生成 | nop-cli gen | exec-maven-plugin 绑定到生命周期 |
| 模板 | /nop/templates/orm | /nop/templates/orm-delta |
| 定制方式 | 在 src/ 中覆盖 _gen/ | 在 _vfs/_delta/ 中覆盖上游 VFS |
| 升级影响 | 重新运行 codegen | 只改上游版本号,Delta 层不受影响 |
===== 总结 =====
无论从零开始还是扩展已有项目,Nop 的核心模式不变:**模型驱动 → 代码生成 → Delta 定制**。区别在于:
* **从零开始**:模型是 Excel,模板是 orm,生成全套 CRUD
* **扩展已有**:模型是 ORM XML,模板是 orm-delta,生成 Delta 叠加层
Delta 机制的好处是升级友好——上游 nop-entropy 发新版本时,只需改版本号,定制代码完全不受影响。
===== 进一步阅读 =====
* [[.:setup_guide|配置开发环境指南]]
* [[../reversible_computing_implementation/xdef_meta_model|XDef 元模型体系]]
* [[https://gitee.com/canonical-entropy/nop-entropy/tree/master/docs/tutorial/tutorial.md|官方开发示例文档]]