不用JSON-RPC和GraphQL:自研DataCenter统一数据协议,一套格式管全部
不用JSON-RPC和GraphQL:自研DataCenter统一数据协议,一套格式管全部
文章目录
- 不用JSON-RPC和GraphQL:自研DataCenter统一数据协议,一套格式管全部
- 一、问题:前后端数据交互的格式碎片化
- 二、答案:DataCenter统一数据协议
- 三、为什么这么做:三个核心设计决策
- 决策一:多数据块放一个响应里
- 决策二:Row 自带状态追踪
- 决策三:查询参数原样带回
- 四、RowSet 的增删改模型
- 五、VtoH:一个意外的设计
- 六、Header 的设计:简单但有底线
- 七、这套协议运行了多少年
一、问题:前后端数据交互的格式碎片化
做政务系统,前后端数据交互有个特点:一个页面经常需要同时组装多块数据。
比如社保个人信息页面,要同时展示三个面板:上方是基本信息(姓名、身份证、参保状态),中间是缴费记录列表,下面是待遇发放记录列表。每块数据都有自己的查询条件、分页参数、行数据。
如果每次请求只返回一个数据块,这个页面至少要发三次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。前端代码变成统一的模式:
vardc=JSON.parse(response);varpersonInfo=dc.body.dataStores["personInfo"];varpayList=dc.body.dataStores["payList"];关键是——后端的 Java 代码也统一了:
DataCenterdc=newDataCenter();dc.setCode(1);DataStoredsPerson=newDataStore("personInfo");dsPerson.getRowset().getPrimary().add(row);dc.addStore(dsPerson);dc.addStore(dsPay);// 另一个DataStoreStringjson=dc.toJson();每个业务方法只管往自己的 DataStore 里塞数据,最后统一序列化。
决策二:Row 自带状态追踪
Row 不是简单的 HashMap,它有三个关键字段:
| 字段 | 含义 |
|---|---|
_t = 0 | 未修改,从数据库查出来的原始数据 |
_t = 1 | 新增,前端新增的行 |
_t = 3 | 修改,前端改了某个字段的值 |
_o | HashMap,原始值——修改前字段的值被记录到这里 |
这是一个迷你ActiveRecord,核心方法是setItemValue:
publicvoidsetItemValue(Objectkey,Objectvalue){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 的设计:简单但有底线
publicclassHeader{privateintcode;privateHashMapmessage=newHashMap();// 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",改完一个页面后发现其他几百个页面都依赖这个格式,就算了。一个设计能活下来的标志,不是没人反对,是反对的人改了之后又改回来了。
