Erupt 数据源实战:erupt-data-http 将任意 REST 接口绑定为 @Erupt 模型

原创2026-10-14 16:23:151,873 阅读
文章标签:后端低代码认证鉴权人工智能大模型AI AgentRAG

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 @Erupt model 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 值经过 URLEncoder UTF-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 注解注释中声明的三步约定:

  1. @Erupt 声明模型:name 是 Erupt 中的实体名,primaryKeyCol 指定主键列——本模块在构造 {value}/{id} URL、解析编辑/删除对象时都依赖它(editData / deleteData 通过 readValue(model, object, primaryKeyCol) 取出 id)。
  2. @EruptHttp 声明接口绑定:如上的 URL、请求头、查询模式、超时。
  3. @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。从源码结构看,这条链路的执行过程是:

  1. 子类 data() 方法发起一次 GET {value},parseList 把响应解析成 List<Map<String, Object>>(支持数组或含 list 字段的对象);
  2. 基类 rows 把每行转为 Map 后,用 match 方法逐条评估查询条件——支持的表达式非常完整:EQ / NEQ / GT / GTE / LT / LTE / LIKE / NOT_LIKE / RANGE / IN / NOT_IN / NULL / NOT_NULL(L215-L243);
  3. 条件匹配完成后做排序(无用户排序时回落到 @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 给出的两条官方路径:

  1. 子类重写:继承 EruptHttpDataService,覆写 addData / editData / deleteData 抛出 i18n 友好的错误(框架侧已有现成的只读错误文案 data.read_only“数据源为只读,不支持写入操作”,见 erupt.i18n.csv),再自行 DataProcessorManager.register 一个独立处理器名并在新子类中注册;
  2. 权限层收敛:利用 @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 低代码框架最后一块拼图:无需建表、无需代理,接口即数据源。

登录后查看全文
erupt