不用JSON-RPC和GraphQL:自研DataCenter统一数据协议,一套格式管全部

不用JSON-RPC和GraphQL:自研DataCenter统一数据协议,一套格式管全部

一、问题:前后端数据交互的格式碎片化

做政务系统,前后端数据交互有个特点:一个页面经常需要同时组装多块数据

比如社保个人信息页面,要同时展示三个面板:上方是基本信息(姓名、身份证、参保状态),中间是缴费记录列表,下面是待遇发放记录列表。每块数据都有自己的查询条件、分页参数、行数据。

如果每次请求只返回一个数据块,这个页面至少要发三次HTTP请求。更麻烦的是——三个请求是独立的,前端要自己管理三个异步回调,等全部返回了再渲染。

我们当时没有Spring Boot,没有页面前后端分离,JSP页面在服务端渲染,但Ajax交互也很多。需要一个办法:一个请求,返回多块数据,前端只解析一种格式

二、答案:DataCenter统一数据协议

这个协议的核心结构只有三层:

DataCenter
├── Header    状态码 + 消息
└── Body
    ├── dataStores    多个数据块,按名字索引
    │   └── DataStore
    │       ├── RowSet  { primary[], delete[] }
    │       │   └── Row  { _t状态, map字段值, _o旧值 }
    │       ├── name      数据块名称
    │       ├── pageSize  每页条数
    │       ├── pageNumber 当前页
    │       └── parameters 查询条件(原样带回)
    └── parameters    全局参数(如当前登录用户信息)

一个完整的JSON响应长这样:

{
  "header": {
    "code": 1,
    "message": {
      "title": "查询成功",
      "detail": ""
    }
  },
  "body": {
    "dataStores": {
      "personInfo": {
        "rowSet": {
          "primary": [
            {"aac001": "10001", "aac003": "张三", "_t": 0}
          ],
          "delete": []
        },
        "name": "personInfo",
        "pageSize": 50,
        "pageNumber": 1,
        "recordCount": 1
      },
      "payList": {
        "rowSet": {
          "primary": [
            {"aae002": "202601", "aae019": "1234.56", "_t": 0},
            {"aae002": "202602", "aae019": "1234.56", "_t": 0}
          ],
          "delete": []
        },
        "name": "payList",
        "pageSize": 20,
        "pageNumber": 1,
        "recordCount": 48,
        "parameters": {"aac001": "10001"}
      }
    },
    "parameters": {"loginUser": "admin"}
  }
}

一个请求,返回了两块数据:personInfo 是一次请求的数据,payList 是分页列表。前端拿到这个JSON,按 dataStores 的名字取对应的数据块渲染到各自的面板。

三、为什么这么做:三个核心设计决策

决策一:多数据块放一个响应里

当时业界有几种方案:

  • 发多个HTTP请求——并发管理复杂
  • 返回值拍成一个大XML——XML前端解析重
  • 自定义分隔符拼字符串——太脆弱

我们的方案:一个JSON,多DataStore。前端代码变成统一的模式:

var dc = JSON.parse(response);
var personInfo = dc.body.dataStores["personInfo"];
var payList = dc.body.dataStores["payList"];

关键是——后端的 Java 代码也统一了:

DataCenter dc = new DataCenter();
dc.setCode(1);
DataStore dsPerson = new DataStore("personInfo");
dsPerson.getRowset().getPrimary().add(row);
dc.addStore(dsPerson);
dc.addStore(dsPay); // 另一个DataStore
String json = dc.toJson();

每个业务方法只管往自己的 DataStore 里塞数据,最后统一序列化。

决策二:Row 自带状态追踪

Row 不是简单的 HashMap,它有三个关键字段:

字段含义
_t = 0未修改,从数据库查出来的原始数据
_t = 1新增,前端新增的行
_t = 3修改,前端改了某个字段的值
_oHashMap,原始值——修改前字段的值被记录到这里

这是一个迷你ActiveRecord,核心方法是 setItemValue

public void setItemValue(Object key, Object value) {
    if (map.get(key) == null) {
        if (_t == 0) { _t = 1; }  // 之前是空,填了一个值——新增
    } else {
        if (_t == 0) {             // 之前有值,先记下来,再改
            _t = 3;                // 标记为修改
            _o.put(key, map.get(key)); // 备份旧值
        }
    }
    map.put(key.toString(), value);
}

前端不需要知道自己是在"新增行"还是在"编辑行"。修改一个单元格,Row 自动把 _t 从0变成3,并把旧值存入 _o。提交时,后端遍历 RowSet 的 primary 列表,根据 _t 判断:_t==1 调 insert,_t==3 调 update。一条 save() 搞定增删改。

决策三:查询参数原样带回

看 payList 这个 DataStore,它在查询请求时传入了 parameters: {"aac001": "10001"}。返回时,这些参数原封不动地带着。

这不是冗余。前端翻页时,不需要重新组装查询条件——直接从 DataStore 里取 parameters 再发出去就行:

// 翻到第2页
pageData.body.dataStores["payList"].pageNumber = 2;
// parameters不用重新填,round-trip保证了它还在
ajax.post("/query", pageData);

参数的"round-trip"设计让前端彻底解耦了查询条件的管理。查询条件是谁填的、从哪里来的、中间有没有被用户改过——前端不需要知道,后端给什么前端就用什么。

四、RowSet 的增删改模型

RowSet 用三个 Vector 管理行数据:

向量用途
primary当前数据行,含新增、修改和未改动行
delete被删除的行,从primary移入这里
filter预留的过滤结果集

为什么 delete 不是标记删除,而是物理移动到另一个集合?

因为前端渲染时,delete 集合里的行是不显示的(它们已经被移出了 primary)。提交时,后端同时处理 primary(新增+修改)和 delete(物理删除)——一个 RowSet 就包含了本次操作的完整变更集。

resetUpdate() 方法更体现了这个思想——提交成功后,把所有行的 _t 重置为0,清空 _odelete,RowSet 恢复为"干净的查询结果"状态。

五、VtoH:一个意外的设计

RowSet 还有一个神奇的方法 VtoH——纵向转横向。把一个列式存储的数据集变成行式:

输入(纵向):                    输出(横向):
col_name  col_vale               aac001  aac002  aac003
aac001    10001        →         10001   xxxx    张三
aac002    xxxx                   101     yyyy    李四
aac003    张三
aac001    101
aac002    yyyy
aac003    李四

数据审计时,变更记录通常以"字段名+新旧值"的列格式存储。VtoH 一键转成用户能看懂的表格。这个方法只有十几行,但解决了一个在政务系统中反复出现的问题——审计数据的横向展示。

六、Header 的设计:简单但有底线

public class Header {
    private int code;
    private HashMap message = new HashMap();  // title + detail + 自定义
}

code 是状态码(1成功,负数是具体错误码),message 里的 title 是对用户的标题(“保存成功”、“参保人不存在”),detail 是给技术人员的详细信息。

这个设计在今天看来普通,但在当时有一个细节:code 永远不是 HTTP 状态码。即使业务逻辑报错(“该参保人已存在”),HTTP Status 依然是200,错误信息通过 Header.code 传递。因为我们的前端只认 Header.code,不认 HTTP 状态——换了一种错误传递方式前端就崩了。

七、这套协议运行了多少年

从2010年左右设计出来,到系统2023年下线,这套 DataCenter 协议跑了十多年。

它不是什么高深的技术——没有 schema 校验,没有类型系统,没有缓存策略。但它在政务系统的实际约束下解决了一个反复出现的问题:前后端数据交互的统一格式

今天回头看,这套协议有点像简化版的 GraphQL——一个请求返回多个命名的数据集,前端按需取用。区别在于 GraphQL 有完整的类型系统和查询语言,而 DataCenter 只有一个 JSON 结构和一套 Java 类。前者是工业标准,后者是在约束条件下的实用解

最后说一句——这套协议存在了十多年,不是因为没有人想过要换。而是每次有人提"要不要改成 RESTful",改完一个页面后发现其他几百个页面都依赖这个格式,就算了。一个设计能活下来的标志,不是没人反对,是反对的人改了之后又改回来了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值