不用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 | 修改,前端改了某个字段的值 |
_o | HashMap,原始值——修改前字段的值被记录到这里 |
这是一个迷你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,清空 _o 和 delete,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",改完一个页面后发现其他几百个页面都依赖这个格式,就算了。一个设计能活下来的标志,不是没人反对,是反对的人改了之后又改回来了。
454

被折叠的 条评论
为什么被折叠?



