Erupt 数据源实战:erupt-data-http 将任意 REST 接口绑定为 @Erupt 模型
Erupt 数据源实战:erupt-data-http 将任意 REST 接口绑定为 @Erupt 模型
本篇以 erupt-data-http 模块说明 为主体,讲清如何用一个 @EruptHttp 注解把外部 JSON HTTP 服务直接变成 Erupt 的可视化管理对象——不写 DAO、不写 Spring 代理。读完你可以掌握:预期接口的请求/响应形态约定、value / headers / queryMode / timeout 四个配置项的取值与生效逻辑、LOCAL 与 REMOTE 两种查询模式在 EruptHttpDataService 中的真实执行路径,以及只读接口场景下的降级方案。
一、模块定位:注解驱动的 REST 数据源
erupt-data-http 是 Erupt 多数据源体系(erupt-data 下与 JPA、MongoDB、JDBC、K8s、飞书、Notion 等并列的模块之一,见根 pom.xml 的 module 列表)中的一个轻量实现。它的核心定位,按模块 README 的原话是:
Bind a
@Eruptmodel to a JSON HTTP endpoint — Erupt drives list / add / edit / delete against your service without any DAO or Spring proxy on the client side.
即:只要远端服务暴露了符合约定的 JSON REST 接口,Erupt 就能对它执行列表、新增、编辑、删除全套操作。实现上完全基于 JDK 17 内置的 java.net.http.HttpClient(根 pom.xml 中 java.version 为 17),除了 erupt-core 之外没有任何额外运行时依赖,这一点也可以从模块 pom.xml 确认——依赖列表里只有 erupt-core 一项。
模块通过 Spring Boot 3 的自动装配机制注册:AutoConfiguration.imports 中声明了 EruptHttpAutoConfiguration,该配置类实现 EruptModule 接口,把 erupt-data-http 模块信息注册进 Erupt 的模块清单。
二、预期接口形态:约定优于配置
使用本模块前,目标服务必须满足 README 中定义的接口形态。以 @EruptHttp(value) 指定的资源基础 URL 为 {value}:
GET {value} → [ {...}, ... ] or { "total": n, "list": [ ... ] }
GET {value}/{id} → { ... }
POST {value} add
PUT {value}/{id} edit
DELETE {value}/{id} delete
结合源码可以补充几条实现细节(见 EruptHttpDataService):
- 列表响应两种结构都接受:
parseList方法先对响应体trim(),以<a href="https://link.gitcode.com/i/f45d6d15ebad6b8de06022381381a59c" target="_blank">开头就按 JSON 数组解析;否则按对象解析并取其中的list字段。两种结构都不是时,抛出 i18n 提示“接口返回的数据结构无法识别”([erupt-http.i18n.csv)。 {id}URL 拼接做了尾部斜杠归一化:idUrl方法会先去掉value末尾的/再拼接/id,且 id 值经过URLEncoderUTF-8 编码,因此value写不写尾斜杠都不影响。- 请求失败判定:任何状态码不在 2xx 范围的响应都会抛出带状态码、方法、URL 的运行时异常(i18n key
http.request_failed,中文文案“HTTP 请求失败”);网络 IO 异常、请求被中断同样走该错误通道并恢复中断标志。 - 重定向策略:内部
HttpClient使用Redirect.NORMAL,即 GET/HEAD 自动跟随、POST 不跟随(L48-L49)。
三、注解配置:@EruptHttp 的四个属性
核心注解 EruptHttp 定义在模型类上,作用域 TYPE、保留策略 RUNTIME。完整属性说明(继承自 README 并对照注解源码补充):
| 属性 | 默认值 | 说明 |
|---|---|---|
value |
—(必填) | 资源基础 URL,例如 https://api.example.com/users |
headers |
{} |
额外请求头,格式为 "Name: Value" 字符串数组,如 "Authorization: Bearer xxx" |
queryMode |
LOCAL |
LOCAL 拉全量列表后在内存中过滤/排序/分页;REMOTE 把分页下推给远端 |
timeout |
10 |
请求超时时间,单位秒 |
几个值得注意的源码细节:
- headers 的解析方式:
request方法按第一个:切分每条"Name: Value",前后分别trim(),因此冒号后带空格的标准写法可以正确解析;只有当请求带 body(即 POST/PUT)时才会自动追加Content-Type: application/json头(L145-L154)。 - timeout 的生效点:应用在
HttpRequest.Builder上,是单次请求的读超时,超时后会以 i18n 化的“HTTP 请求失败”异常呈现。 - 注解缺失时的显式报错:若某个声明了
@EruptDataProcessor("HTTP")的模型类忘记加@EruptHttp,eruptHttp私有方法会直接抛出@EruptHttp annotation is missing on <model>的运行时异常,而不是静默回退,便于第一时间定位配置遗漏。
四、完整示例:把 GitHub Users 接口变成 Erupt 管理界面
README 给出的官方示例值得完整保留——它演示了一个只读 GET 接口的典型绑定方式(LOCAL 模式 + 自定义请求头 + 15 秒超时):
@Getter
@Setter
@Erupt(name = "GitHub User", primaryKeyCol = "id")
@EruptHttp(
value = "https://api.github.com/users",
headers = { "Accept: application/vnd.github+json", "Authorization: Bearer ghp_xxx" },
queryMode = EruptHttp.QueryMode.LOCAL,
timeout = 15
)
@EruptDataProcessor(EruptHttpDataService.DATA_PROCESSOR)
public class GhUser {
@EruptField(views = @View(title = "ID"))
private Long id;
@EruptField(
views = @View(title = "Login"),
edit = @Edit(title = "Login", search = @Search)
)
private String login;
@EruptField(views = @View(title = "Type"))
private String type;
@EruptField(views = @View(title = "Avatar"))
private String avatar_url;
}
这个示例体现了接入本模块的完整三要素,也是 EruptDataProcessor 注解注释中声明的三步约定:
@Erupt声明模型:name是 Erupt 中的实体名,primaryKeyCol指定主键列——本模块在构造{value}/{id}URL、解析编辑/删除对象时都依赖它(editData/deleteData通过readValue(model, object, primaryKeyCol)取出 id)。@EruptHttp声明接口绑定:如上的 URL、请求头、查询模式、超时。@EruptDataProcessor(EruptHttpDataService.DATA_PROCESSOR)声明数据处理器:DATA_PROCESSOR常量值为"HTTP"。从源码结构看,注册发生在EruptHttpDataService的静态块里——DataProcessorManager.register("HTTP", EruptHttpDataService.class);运行时 DataProcessorManager 会读取模型类上的@EruptDataProcessor值,从注册表中取类名,再从 Spring 容器拿到对应 Bean。未标注该注解的模型会回落到DEFAULT_DATA_PROCESSOR(JPA 实现),所以这三个注解缺一不可。
字段上的 @EruptField 配置(views / edit / search)与其他数据源完全一致——login 字段声明了 search = @Search 后,前端列表页就能对它做条件查询,这是后面 LOCAL/REMOTE 两种模式差异的关键场景。
五、queryMode 深度解析:LOCAL 与 REMOTE 的执行路径
这是本模块最核心的设计决策。两种模式的语义(README 原文):
LOCAL— 一次GET拉取value全量列表,之后过滤、排序、分页全部在内存中完成。适合远端接口不支持查询参数的场景。REMOTE— 把pageIndex、pageSize、sort以及等值条件以查询参数形式追加到 URL 上,分页由远端完成。端点必须返回{ "total": n, "list": [...] }(纯数组也被接受,此时total取数组长度)。
LOCAL 模式:全量拉取 + 基类内存引擎
LOCAL 模式下 queryList 直接 return super.queryList(...),即委托给基类 EruptBeanDataService。从源码结构看,这条链路的执行过程是:
- 子类
data()方法发起一次GET {value},parseList把响应解析成List<Map<String, Object>>(支持数组或含list字段的对象); - 基类
rows把每行转为Map后,用match方法逐条评估查询条件——支持的表达式非常完整:EQ/NEQ/GT/GTE/LT/LTE/LIKE/NOT_LIKE/RANGE/IN/NOT_IN/NULL/NOT_NULL(L215-L243); - 条件匹配完成后做排序(无用户排序时回落到
@Erupt(orderBy)声明),最后subList切片分页,total取过滤后行数。
所以 LOCAL 模式的代价很明确:每次翻页、筛选、排序都要重新拉全量数据,只适合数据量有限或远端不支持查询参数的接口;换来的是筛选表达式能力完全不受远端限制——LIKE、RANGE、IN 等复杂条件都能用。
REMOTE 模式:分页与条件下推
REMOTE 模式的 queryList 完整实现见 EruptHttpDataService L60-L90,URL 拼装规则如下:
- 基础 URL 上按是否已含
?选择用?或&起始,追加pageIndex=<页码>与pageSize=<每页大小>(均经 URL 编码); - 排序通过 Sort.toSortString 序列化为
field asc/ 多字段时以", "连接的字符串,追加为sort=参数,例如login asc, id desc; - 条件只翻译等值条件:
mergeConditions合并查询条件后,源码注释写明“only equality conditions translate cleanly to key=value query parameters”,因此只有QueryExpression.EQ的条件会被追加为key=value参数,其余表达式(LIKE、GT等)在 REMOTE 模式下不会下推给远端。
响应解析上做了双结构兼容:响应体以 [ 开头按数组处理(total = 数组长度),否则解析对象取 total 与 list 两个字段,total 缺失时按 0 处理。
选型建议(结合上述实现推断):远端接口支持分页且数据量大 → REMOTE,注意远端必须自行支持 pageIndex / pageSize / sort / 等值参数的语义;远端只返回全量数组(如 GitHub 这类公开 API 的分页体系与 Erupt 不同)→ LOCAL,并接受每次交互一次全量请求的开销,此时可通过调大 timeout 缓解慢接口风险。
单条查询与主键的返回类型细节
findDataById 与列表路径有一个容易忽略的差异:列表数据以 Map<String, Object> 流转,而按 id 查询时 findDataById 用 Gson 直接把 JSON 反序列化成模型类本身(eruptModel.getClazz())而不是 Map。源码注释给出了原因——下游的钻取(drill)/ 编辑表单逻辑会对返回对象做模型类的字段反射。这解释了为什么示例中 primaryKeyCol = "id" 必须与远端响应里的真实主键字段名一致。
六、完整 CRUD 链路与只读接口的降级方案
写操作的实现(EruptHttpDataService L101-L119):
| Erupt 操作 | 实际发出的请求 | 请求体 |
|---|---|---|
新增 addData |
POST {value} |
模型对象序列化的 JSON |
编辑 editData |
PUT {value}/{id} |
模型对象序列化的 JSON |
删除 deleteData |
DELETE {value}/{id} |
无 body |
当远端服务是只读的(例如 GitHub Users 接口并不允许通过 POST /users 创建用户),README 给出的两条官方路径:
- 子类重写:继承
EruptHttpDataService,覆写addData/editData/deleteData抛出 i18n 友好的错误(框架侧已有现成的只读错误文案data.read_only“数据源为只读,不支持写入操作”,见 erupt.i18n.csv),再自行DataProcessorManager.register一个独立处理器名并在新子类中注册; - 权限层收敛:利用
@Erupt(power = ...)的权限声明(Erupt 中power属性默认@Power)关闭新增/编辑/删除入口,让界面本身不再暴露写操作。
实践中两者可以组合:权限层挡住正常入口,子类兜底防住直接调用 API 的写入尝试。
七、适用前提与限制小结
- 运行前提:JDK 17(模块编译与
java.net.http.HttpClient均要求),依赖erupt-core与 Spring Boot 自动装配环境; - 接口契约:远端必须是 JSON REST,列表端点接受数组或
{total, list}两种结构;GET {value}/{id}返回单对象;写接口按标准 POST/PUT/DELETE 语义; - REMOTE 模式限制:仅等值条件可下推,排序串格式为
field asc, field desc,total/list结构由端点保证; - LOCAL 模式限制:每次查询全量拉取,数据量大或接口慢时应调大
timeout或改用 REMOTE; - 认证方式:通过
headers静态注入(Bearer Token、API Key 均可),从源码结构看,注解值是编译期常量,不支持运行时动态生成 token(如 OAuth 刷新令牌场景需要从源码结构看自行扩展,例如重写request逻辑)。
对“把已有 HTTP 服务快速纳管”这一场景,erupt-data-http 用三个注解 + 一个纯 JDK 实现就补齐了 Erupt 低代码框架最后一块拼图:无需建表、无需代理,接口即数据源。