关注公众号【1024个为什么】,及时接收最新推送文章!
最近公司要新接入一个支付渠道,涉及到公司和三方的对账。由于三方对账内容格式太特殊,需要对文件解析引擎改造适配,实际改造耗时比预估的要少很多,回过头总结才发现,原来解析引擎经过前几次的迭代变得越来越易扩展,如果不是这次文件内容格式太特殊,连兼容改造都省了。
接下来就回顾一下它是如何一步步拥有今天这种能力的。
《对账中心设计与实现》这篇文章里提到,对账中心里有一个重要的组成模块就是文件解析引擎,它是驱动各对账项目的关键模块。
然而最开始,它简陋到就只有一个方法。
最开始的出发点很简单,对账所需的数据,不想直连业务的库获取,原因有三:
1、把自己的库直接暴露给其他服务,本身就是不合理设计;
2、对账属于任务性质的,瞬间请求流量上来了,搞不好会影响业务;
3、业务表结构变更后,还要跟随改动、上线。
于是就约定,参与对账的业务方,把各自每天要对账的数据放在 .txt 文件中,上传到 FTP 指定的目录。对账中心(当时还没有对账中心的概念,只是为了两个经常数据不一致的场景单独做了一个功能)从 FTP 拉取文件,每条数据解析到一个 java 对象中,再进行对象内容的比较。
这种设计也不太好,还是和业务系统间接耦合。
为了减少工作量,约定参与对账的 A、B 双方采用相同的表头,相同的分隔符,这样 A、B 就可以共用一套解析逻辑、一个 java 类暂存解析处理的内容。
解析引擎这就有了最初的雏形,然而它却差点被扼杀在摇篮之中。
这个功能上线后就暴露出好几个业务系统的 bug,给公司及时止损。对账中心也得到了重视,就推动核心的业务场景都要接进来。接了几个业务场景后发现,接入成本太高了。
业务方要开发生成对账文件,对账中心要针对每个接入的业务场景单独开发解析逻辑(因为每个业务场景文件中的字段都不一样,前面设计的是用 java 对象承载每条数据去完成比对,这样一来 java 类和对账文件一一绑定)。
于是对账中心就做了一次很大的改造,降低接入成本,适应更长远的规划。
改造后废弃了通过文件获取对账数据的模式,利用数仓直接从业务库同步所需的对账数据,数据载体也从原来的 java 对象变为由 SQL 查询结果得到的字符串,更具通用性。
与此同时,又来了新的需求,要实现公司数据和三方支付公司的对账,三方的数据都是通过三方提供的对账单下载接口下载下来的对账文件,这下数仓就不好使了吧,还是要先解析文件,这下文件解析引擎又起死回生了。
这次需求只涉及到解析微信、支付宝的对账单,而且从可预见的相当长一段时间内,不会再有其他渠道的对账单了,于是解析引擎的设计就紧紧围绕这两个渠道的文件内容展开,而且尽可能做得通用。
首先对两个渠道的文件进行全方位的对比解读:
有了这个对比,就可以抽象出变化的维度,划分出不同的功能模块,内容做成可配置的。虽然文件类型不同,但都可以直接使用 readLine() 读取每一行内容。
1、文件编码默认 UTF-8,支持配置
2、分隔符默认英文逗号,支持配置,内容中特殊字符需要额外处理,要有特殊字符处理模块
3、表头处理模块,同一类文件表头内容不同要兼容
4、遍历模块需要知道起止行号,支持配置,有无内容判断模块、无内容特殊处理
所以就有了下图的结构设计:
重点说一下转换、落库功能,解析落库的数据会在管理后台展示,而且为了简化非String 类型、空数据的落库处理,所以复用了 Dao 和 Dto 的能力,但带来的问题就是扩展性差。
前面也说过今后再接新的三方支付渠道可能性不大,方案设计就仅局限于当前需求(其实也没办法向后兼容,接不接新渠道、新渠道的对账单是什么样子,一切都是未知)。
然而没过多久,种种原因,导致软件界的墨菲定律发生了,解析新渠道对账单的需求就砸过来了,而且还是接连不断的砸过来!
然后上面的对比表格就变成了下面这个样子:
这里强烈吐槽一下易宝,对账单五花八门,恶心至极!
在 农行、开联通接入时,转换、落库这两部分的扩展就显得很 low 了,拷贝复制一套 Dto、Dao,但鉴于排期和回归测试的工作量,就没有改动。
到易宝接入时,由于原有的解析引擎不支持 .xlsx 文件的解析,必须要大改了,借此机会动个大手术,使其更易扩展。
结构设计也变成了下面的样子:
红框的位置是主要下刀点。
excel 解析没啥可说的,我重点说一下转换、落库这两部分的改动。
之前 某类对账单、Dto、Dao、表 这四者是 1:1:1:1 的关系,想要做到零开发的扩展性,必须打破这个关系,变成 n:1:1:n,对账单和表只能是1:1,同类型的对账单数据必须放在一张表内,便于后续数据使用。
但干掉 Dto、Dao 后,对账单内容各不相同,如何设计一个通用的 java 类来承载不同对账单的数据呢?
总不能定义一个 Dto,包含属性 field1......field100(100个应该够用了)吧,况且哪个属性对应的类型是什么也是未知的,更不能都定义成 String 类型吧。(如果单纯解析文件,可以直接按列序号用数组接收内容就行,但要实现最终动态落库,就力不从心了)
一个通用的 Dao 能用这个通用的 Dto 作为入参吗?
插入哪些字段、哪个字段是什么类型的值、插入到哪张表,Dao 怎么控制?
想到这些,差点想放弃这个方案,首先能不能实现先放一边、关键是排期允不允许,开发和回归的工作量都不小。
但如果能实现肯定会很有成就感,毕竟程序员仅存的快乐就在攻克这些难题之上了。
先说干掉 Dto 的方案,通过各种查阅资料,发现 cglib 动态生成对象的能力很适合我这个场景,也就是 net.sf.cglib.beans.BeanGenerator,它可以根据你提供的 属性名、属性类型动态创建一个 object。
而属性名、数据类型,可以从待落库的表信息中得到(查询、解析表结构),哪类对账单落到哪张表也可以通过对账中心的配置模块动态拿到。
这样就可以动态的获得一个和对账单适配的 java 对象,随后就可以把每行的内容解析出来放到这个对象中,一行对应一个对象,第一个对象创建出来后,该文件剩余行所需要的对象克隆出来就行,既简单又高效。
拿到这些承载着数据的对象后,就得想办法存到数据库了,传统的 mybatis 写法肯定满足不了了,只能另辟蹊径。干脆就自己动态的拼装 SQL,字段和内容的顺序、数据类型都能保证,CommonDao 提供一个和数据库交互的能力就行。
有了这些能力的加持后,再有新的渠道接进来,只要文件内容不是很另类,只需要建好一张表、简单配置就行。
即便这次的 XX付,格式这么奇葩,只需要修改某个小模块 [行内容解析模块],就能快速适配。
这是不是就是大家常说的发现变化、封装变化,各维度变化互不影响。
扯两句
好的系统不是一次设计出来的,而是一点点演变出来的
能够精准的发现变化并封装变化,就是好的设计
原创不易,如有收获,一键三连,感谢支持!