TT Lab
开始
学习 学习路径 课程

构建 EAI 中间层

只在边界处转换,转换不了就拒绝

在 TT Lab 中继续学习

一句话总结

转换适配器只在标准报文(定长、EUC-KR、外部代码)与内部 JSON(UTF-8、内部代码)之间的边界这一个地方转换格式。规则作为布局和代码映射表这样的数据存放,而对于未知的代码、超长的长度、无法表示的字符,不要修改后放行,而是拒绝。

为什么需要它

银行里有两个世界共存。账户系统和外部机构使用几十年前的定长报文,新渠道和内部 API 则使用 JSON。如果让两个世界直接相连,每个构建新渠道的团队都得重新学习 EUC-KR 和字节填充,而且外部代码与内部代码的对照表,每个团队各自带着一套。表有好几套,总有一天一定会有一套过时。

《Enterprise Integration Patterns》在这个位置放置的是 Message Translator。它是在使用不同数据格式的应用程序之间插入一个转换格式的过滤器,书中说明,这是把面向对象的 Adapter 模式移植到消息传递上。如果把转换集中到枢纽,内部系统就只需要认识 JSON 和内部代码,即使外部标准变化,需要修改的地方也只有一个适配器。

工作原理

布局是数据。准备一张表,每个字段占一行,写明名称、长度、格式、JSON 键、映射域和小数位数(scale),转换器只负责解释这张表。定义书变了,就改表。如果为每种交易单独编写转换代码,交易有一百种,填充规则就有一百套。

长度是字节。格式有三种。N(数字)右对齐、前面补 0,AN(字母数字)和 H(混有韩文)左对齐、后面补空格。H 字段的长度是 EUC-KR 的字节数,所以 10 个韩文字恰好填满 20 字节的字段。如果按字符数来数,11 个韩文字的名字会通过“不超过 20 个字符”的检查而变成 22 字节,后面所有字段都会被挤后 2 个字节。

超出时不要截断。为了凑长度而按字节截断,一个韩文字就会被从中间劈开。接收的一方要么无法解码该字段,要么把剩下的第一个字节与下一个字段的第一个字节拼在一起,读成完全不同的字。即使按字符截断,问题仍然存在——收款人姓名被截断的转账,是业务事故。转换器要拒绝,是否缩短由渠道和业务来决定。

代码用表来转换,未知的代码要拒绝。报文中的银行代码 201 在内部是 GRM。对应关系放在映射表这一个地方,双向使用。如果把表中没有的代码原样放行,内部系统会把这个值按自己的代码体系来解释——如果碰巧有相同的值而含义不同,钱就会去到错误的地方。因合并而停用的代码,也要从表中删除,让它在进来的瞬间就被拦下。

往返就是转换器的规格。把按标准生成的正文转成 JSON,再转回正文,与原来的字节相比,一个字节也不能不同。从这个性质中,自然就推出了几条规则。AN 和 H 只能去掉后面的空格——如果连前面的空格也去掉,转回去时就会丢失。N 可以转成整数,是因为前面的 0 只是填充,不是值。每次修改转换器,都用全部样本跑一遍往返,任何会丢失东西的修改,当场就会暴露。

金额和利率不要经过浮点数。报文中的利率 0032500 是隐含小数点后第四位,即 3.2500。Python decimal 文档说明,在二进制浮点数中,1.1 + 2.2 会显示为 3.3000000000000003,0.1 + 0.1 + 0.1 - 0.3 不是 0,所以无法相信相等检查,因此对于需要严格相等的会计,decimal 才是合适的。同一份文档还展示了 Decimal(0.1) 与 Decimal('0.1') 是不同的——一旦成为 float 的值,再转回 decimal 已经晚了。所以在 JSON 中也不是用数字,而是用字符串 "3.2500" 来交换。后面两个 0 是表示位数的信息,而 decimal 会把 1.30 + 1.20 保持为 2.50,从而守住这种写法。实际上 float("0.29") * 100 是 28.999999999999996,转成整数就会少 1。

不要相信 EUC-KR 这个名字。EUC-KR 所容纳的韩文,是 KS X 1001 预组合形的 2,350 个字。Windows 上常见的 CP949(UHC)在此基础上增加了其余的韩文,用 Python 数一数,会用 2 字节容纳全部 11,172 个 Unicode 韩文音节。那 2,350 个字在两种编码中的字节相同,平时没有任何问题,只在像“ddom”这样的字上才会暴露。更棘手的是各个实现并不相同。Python 标准编码表中的 euc_kr 编解码器不会把“ddom”当作错误拒绝。正如 CPython 源码中的注释所说,它会换成 KS X 1001:1998 的组合序列,输出 8 字节。同样的字,glibc 的 iconv -t EUC-KR 会拒绝。只认识 2 字节预组合形的对方,会把这 8 字节读成四个字母。所以转换器要逐字直接判定“是不是 2 字节的预组合形”,不是就拒绝。

在现场相遇的样子

半个韩文字。如果有渠道为了把摘要凑成 30 字节而按字节截断,外部机构就会把整个报文当作格式错误退回。错误虽然出在外部,原因却是我们渠道的截断。混有 CP949 的正文。如果有渠道把文件以 CP949 保存后发送,会好几个月都平安无事,直到第一次来了名字中含有预组合形之外的字的客户那天,才会出问题。如果转换器按 EUC-KR 严格读取并拒绝,当天就能看到原因。用空格填充的数字。有的系统会用空格而不是 0 来填充金额字段。虽然能读取,但往返之后字节会不同。往返测试不仅能发现转换器的 bug,也能发现这种违反标准的对方。

下一项实验要做什么

把定义书转换成布局文件,把代码定义书转换成映射表,然后亲手制作两个转换器(f2j.py·j2f.py)。用全部样本跑往返测试,用 decimal 处理隐含小数点,把预组合形之外的字改为拒绝,然后转换整个收件箱。公共库 lhconv.py 做的是同样的事,但本实验中不使用——从第 4 模块开始使用。评分器每次都会用新的布局和新的值(包括韩文)来运行转换器。