模拟金融中间件系统
本文通过提供金融中间件系统的模拟层,有助于减少离岸开发工作。
目录
引言
获得金融应用程序的经验绝非寻常。您必须考虑许多开发人员通常会忽略或不知道的额外事项。例如,满足监管要求,如审计日志、制作者-检查者规则、在执行金融交易及其撤销时采取极端安全措施、安全性、数据归档、反洗钱、KYC 等。
集成主要是任何金融应用程序的核心部分,因为无论如何您都必须与银行主机或中间件集成,这绝非易事。请记住,主机系统通常指核心银行系统,而中间件实际上是与 ATM、SMS、电话银行、IVR、WAP 等主机系统通信的渠道集成器。
在开发环境中,最严峻的挑战是在异地编写集成代码,因为中间件或主机系统在开发中心不可用。这一关键限制迫使公司在现场完成所有集成工作,这显然会增加开发和生产后支持的成本,因为在大多数情况下,需要现场调查修复问题。
本文通过在开发中心创建一个模拟主机系统/中间件系统的虚拟环境,来创建模拟集成层,从而帮助缓解这一问题。
通过创建模拟层,我们可以在开发中心轻松模拟中间件,这极大地有助于开发/支持活动。
模拟集成层
此集成层应遵循以下目标进行设计。
- 前端应用程序在此模拟层方面不得以任何方式更改,并将其视为与生产环境完全相同的真实集成层。
- 该层应该是易于更改的,这意味着添加新交易应该非常容易,并且根本不需要任何开发工作。
异地开发中心对前端应用程序进行此模拟集成层的定制将破坏目标,因为由于应用程序中的定制,我们永远无法确定其质量。
如果此模拟层不易于更改,则意味着我们通过维护此层而增加了项目的开发成本,最终将导致开发团队在现场进行活动并放弃此模拟层。
设计
主机系统通常通过 TCP/IP 上的消息驱动通信协议进行通信。这可以是 ISO 8583 消息格式、简单的字符分隔文本格式或其他供应商驱动的格式。每条消息都标记为一个请求/响应格式的事务。每个请求/响应都有标头和数据部分。典型的消息标头可能如下所示:
Content |
字节长度 |
描述 |
消息大小(字节) |
4 |
0600(如果消息长度为 600) |
消息类型 |
2 |
01-请求 02-响应 |
交易代码 |
4 |
0001-余额查询 |
日期时间 |
14 |
YYYYMMDDHHmmss [20150901112910] |
STAN |
6 |
123456 |
样本消息标头如下详细说明,以便于理解。
现在我们理解了典型的主机系统是如何工作的,我们需要设计模拟层,使其包含以下模块:
- 用于前端和模拟层之间通信的 TCP/IP 模块
- 组装请求/响应消息的机制
- 包含虚拟消费者信息的数��部分
TCP/IP 模块
TCP/IP 模块是最简单的,可以使用网上随处可见的示例来构建。我们需要了解这是一个正在构建的模拟层,用于异地开发,因此不需要生产级别的性能要求。一个简单的 TCP/IP 模块足以满足此目的。
组装请求/响应消息
这是模拟层的核心部分,必须以一种可以即时进行添加/修改的方式进行设计。XML 在数据驱动的应用程序方面具有巨大的灵活性和强大的功能,非常适合此用途。让我们将这个关键的 XML 文件命名为 TransactonMetaInfo.xml 并开始创建它。
一个基本版本可以像这样:
<Root Client="ABC Bank" Product="ACME Product">
<Header Type="Request"
<Field Name="MessageType" EvalFunction="" DefaultValue="" />
<Field Name="Transactioncode" EvalFunction="" DefaultValue="" />
<Field Name="TranDateTime" EvalFunction="" DefaultValue="" />
<Field Name="STAN" EvalFunction="" DefaultValue="" />
</Header>
<Header Type="Response">
<Field Name="MessageType" EvalFunction="" DefaultValue="" />
<Field Name="Transactioncode" EvalFunction="" DefaultValue="" />
<Field Name="TranDateTime" EvalFunction="" DefaultValue="" />
<Field Name="STAN" EvalFunction="" DefaultValue="" />
</Header>
<Transaction DispName="Balance Inquiry" Code="9999">
<Request Code="11" Example="">
<Header Type="Request"/>
<Field Name="CustomerPan" EvalFunction="" DefaultValue="" />
</Request>
<Response Code="22" Example="">
<Header Type="Response"/>
<Field Name="ResponseCode" EvalFunction="" DefaultValue=""/>
<Field Name="Balance" EvalFunction="" DefaultValue=""/>
</Response>
</Transaction>
</Root>
XML 非常直接。根标记定义了客户端和此 XML 所绑定产品的名称。这消除了风险,以防模拟层被用于多个客户,例如美国银行、花旗银行等。
我们稍后将讨论 Header 标签;现在,最重要的设计元素是交易。每笔交易都服务于特定的业务需求。例如,余额查询是一笔服务于特定需求的交易,其交易代码可能是 9999。同样,我们也可以进行另一笔交易,如账单查询或账单支付,以此类推。每笔交易都由请求和响应消息组成。让我们通过创建一个场景来理解这一点。假设互联网银行是源渠道,创建一个余额查询交易的请求消息并发送到中间件,中间件将此请求转发给实际处理请求的银行主机,主机反过来创建一个响应消息发回中间件,中间件重新格式化响应并将其发送回源渠道(互联网银行)。这个完整的循环称为一次交易。
请求包含一个标头和其他参数。类似地,响应也包含一个标头和其他信息。由于特定客户端实现的后续所有交易的标头都是常量,那么自然的做法是在 XML 的开头包含两种类型的标头 {请求/响应}。
XML 提供了极大的灵活性,可以即时添加/更新/删除字段,甚至可以实时创建整个交易集。您所需要做的就是修改字段。
字段可以有一个默认值。如果提供了默认值,则默认值的优先级很高,这意味着模拟层会忽略消息中该字段对应的数据,并强制该字段具有默认值。字段还可以有一个求值函数。Eval 函数是内置的系统函数,在收到请求消息时可以在运行时触发。
引入 Header 标签是为了识别所有交易集共有的字段。由于其字段中的值不同,Headers 还可以进一步分为请求和响应类型。独立的 Headers 也提供了修改单个 Headers 的灵活性,也就是说,如果响应 Headers 包含额外的内容,也可以实现这一点。
此时,尽管我们已经构建了一个基本设计,但它存在局限性。当模拟层收到消息时,每个请求都需要被适当地自动拆分。这需要额外的求值函数,以便模拟引擎能够轻松地完成这项工作。我们提供以下函数:
命令函数 |
描述 |
GetMessageFieldByIndex |
顾名思义,此 eval 函数按索引在请求中搜索字段并返回位于该索引位置的值。例如:对于请求消息:0064~01~0001~20151009132331~123456~010000001,GetMessageFieldByIndex(2) 将返回 **0001**。 |
Date.Now |
此函数以 yyyymmddhhmmss 格式返回当前日期和时间。 |
ResolveXPath |
此函数对提供的 XPath 进行求值,并必须返回单个值。 |
让我们根据新获得的知识修改我们的 XML。
<Root Client="ABC Bank" Product="ACME Product">
<Header Type="Request"
<Field Name="MessageType" EvalFunction="" DefaultValue="1" />
<Field Name="Transactioncode" EvalFunction="GetMessageFieldByIndex(1)" DefaultValue="" />
<Field Name="TranDateTime" EvalFunction="GetMessageFieldByIndex(3)" DefaultValue="" />
<Field Name="STAN" EvalFunction="GetMessageFieldByIndex(4)" DefaultValue="" />
</Header>
<Header Type="Response">
<Field Name="MessageType" EvalFunction="" DefaultValue="2" />
<Field Name="Transactioncode" EvalFunction="" DefaultValue="GetMessageFieldByIndex(1)" />
<Field Name="TranDateTime" EvalFunction="Date.Now" DefaultValue="" />
<Field Name="STAN" EvalFunction="GetMessageFieldByIndex(4)" DefaultValue="" />
</Header>
<Transaction DispName="Balance Inquiry" Code="9999">
<Request Code="11" Example="">
<Header Type="Request"/>
<Field Name="CustomerPan" EvalFunction="GetMessageFieldByIndex(5)" DefaultValue="" />
</Request>
<Response Code="22" Example="">
<Header Type="Response"/>
<Field Name="ResponseCode" EvalFunction="" DefaultValue="0"/>
<Field Name="Balance" EvalFunction="" DefaultValue=""/>
</Response>
</Transaction>
</Root>
上述方法现在带来了一种自动创建请求和响应列表的方法,该方法基于原始请求消息。唯一剩下的就是客户数据,即余额。填补这个空白很容易,我们只需要使用客户 PAN 筛选客户数据并获取可用余额,而实现这一点并使其易于修改的最佳方法是 XPATH。
现在唯一缺失的部分是客户数据。如果返回硬编码的消息,我们的模拟层将非常有限。该层应该了解客户,并对不同客户有不同的行为。这也有助于进行全面的测试,因为不同的客户可以配置不同的值来产生不同的输出。一个明显的测试是用零余额配置一个客户来测试余额不足的情况。
如果我们引入一个客户标签到我们的 XML 中,如下简要概述,就可以轻松实现这一点。
Customer 标签非常灵活,因为它允许添加新客户及其关联信息。它还支持创建新的数据层次结构。最后一个缺失的元素是创建消息字段和客户之间的链接机制。我们也可以这样做,然后完成。
<Root Client="ABC Bank" Product="ACME Product">
<Header Type="Request"
<Field Name="MessageType" EvalFunction="" DefaultValue="1" />
<Field Name="Transactioncode" EvalFunction="GetMessageFieldByIndex(1)" DefaultValue="" />
<Field Name="TranDateTime" EvalFunction="GetMessageFieldByIndex(3)" DefaultValue="" />
<Field Name="STAN" EvalFunction="GetMessageFieldByIndex(4)" DefaultValue="" />
</Header>
<Header Type="Response">
<Field Name="MessageType" EvalFunction="" DefaultValue="2" />
<Field Name="Transactioncode" EvalFunction="" DefaultValue="GetMessageFieldByIndex(1)" />
<Field Name="TranDateTime" EvalFunction="Date.Now" DefaultValue="" />
<Field Name="STAN" EvalFunction="GetMessageFieldByIndex(4)" DefaultValue="" />
</Header>
<Transaction DispName="Balance Inquiry" Code="9999">
<Request Code="11" Example="">
<Header Type="Request"/>
<Field Name="CustomerPan" EvalFunction="GetMessageFieldByIndex(5)" DefaultValue="" />
</Request>
<Response Code="22" Example="">
<Header Type="Response"/>
<Field Name="ResponseCode" EvalFunction="" DefaultValue="0"/>
<Field Name="Balance"
EvalFunction="ResolveXPath('//Customers/Customer/Account[@PAN={0}]/@Balance, GetMessageFieldByIndex(5));" DefaultValue=""/>
</Response>
</Transaction>
<Customers>
<Customer UserID="testUser1" Name="Test User 1" DateOfBirth="01/01/1995"
Gender="MA" PhoneMobile="03339999999" AddressOffice=""
CNIC="0123456789123" Email="testUser1@test.com"
Fax="" TotalAccounts="1">
<Account BranchCode="0101" Number="010000001" IBAN="" PAN=”010000001”
Currency="PKR" Balance="100" AccountTitle="Test User 1"
AccountType="SAVING" BranchName="Head Office Branch"/>
</Customer>
<Customer UserID=" testUser2" Name="Test User 2" DateOfBirth="10/01/1996"
Gender="MA" PhoneMobile="03349999999" AddressOffice=""
CNIC="0123456789124" Email="testUser2@test.com"
Fax="" TotalAccounts="1">
<Account BranchCode="0101" Number="010000002" IBAN="" PAN=”010000002”
Currency="PKR" Balance="100" AccountTitle="Test User 1"
AccountType="SAVING" BranchName="Head Office Branch"/>
</Customer>
<Customers>
</Root>
如果我们仔细查看下面的字段标签,您就可以轻松理解字段和数据之间的链接是如何创建的。
<Field Name="Balance"
EvalFunction="ResolveXPath('//Customers/Customer/Account[@PAN={0}]/@Balance, GetMessageFieldByIndex(4));"
DefaultValue=""/>
是的,正是 XPath 再次派上了用场,用于获取特定于数据的结果。我们已经创建了一个命令函数 ResolveXPath,它将 XPath 作为输入参数并返回结果。XPath 之美在于它易于管理变更。如果您创建了一个复杂的数据层次结构,XPath 也不会让您失望。现在我们已经具备了创建模拟层的所有必要元素,这不再是什么难事了。
块 - 增加复杂性
到目前为止,我们已经处理了非常简单的交易集,它们返回简单值,例如余额查询,它返回可用余额。在现实世界中,存在更复杂的交易,它们返回不同集合的列表,例如客户账户查询,其中一个客户可以有多个账户,并且每个账户都有不同的值。例如,一种账户类型可以是储蓄账户,另一种账户类型可以是贷款账户,等等。管理这些交易集需要一种不同的方法。
Block 字段在这些情况下对我们有帮助。Block 字段本身也是一个字段,它对 XPath 表达式进行求值,并根据 XPath 返回的对象数量重复所有子字段标签。让我们深入了解一下。
<Field Name="Block"
EvalFunction="ResolveBlock('//Customers/Customer[@PAN={0}]/Account',GetMessageFieldByIndex(5));"
DefaultValue="">
<Field Name="AccountNumber"
EvalFunction="ResolveXPath('//Customers/Customer[@PAN={0}]/Account[@Id={1}]/@Number',GetMessageFieldByIndex(5), ContextId);"
DefaultValue=""/>
<Field Name="AccountTitle"
EvalFunction="ResolveXPath('//Customers/Customer[@PAN={0}]/Account[@Id={1}]/@AccountTitle',GetMessageFieldByIndex(5), ContextId);"
DefaultValue=""/>
</Field>
在上面的示例中,Block 字段的 evalFunction 是特殊的,因为它包含 ResolveBlock 命令函数。此函数对 XPath 表达式进行求值,并将检索到的对象存储在 ArrayList 中。在上面的情况下,将为与提供的 CNIC 匹配的特定客户获取所有账户数据。此账户信息现在存储在 ArrayList 中,因为它将进一步用于后续子字段的求值,例如对于 AccountNumber 字段,eval 函数现在接受两个参数 [CNIC 和 Account Id]。第二个参数 [Account Id] 是唯一区分特定客户账户的参数,一旦识别出账户,获取账户号码就不是难事了,正如 XPath 中所述。
我们上面看到的是一个名为 Get Account Inquiry 的新交易。此交易返回多个账户信息以及各个账户的详细信息,例如账户号码和账户名称,使用 Block 字段。
Using the Code
模拟层应用程序设计非常简单,甚至足够简单地编写伪代码。
Function Process(string rawMessage, XmlDocument metaXML)
{
string[] splittedMessageArray = split rawMessage using ‘~’ as delimeter;
Dictionary<string, string> request = new Dictionary<string, string>();
For each field f in Header and Request of MetaXml Document
If f.DefaultValue is not empty Then
Value = f.DefaultValue
Else
EvaluateFunction = f.EvalFunction;
Value = Evaluate(EvaluateFunction, splittedMessageArray, metaXML)
End IF
Request[f.Name] = Value;
End For
Arraylist responseFieldList = new ArrayList();
For each field f in Header and Response of MetaXml Document
If f.DefaultValue is not empty Then
Value = f.DefaultValue
Else
EvaluateFunction = f.EvalFunction;
Value = Evaluate(EvaluateFunction, splittedMessageArray, metaXML)
End IF
responseFieldList.Add(Value);
End For
String outMessage = string.join(‘~’, responseFieldList);
Byte[] outByte = Encoding.ASCII.GetBytes(fourByteSize);
return outByte;
}
这是一个三步过程:拆分接收到的消息,通过读取交易的标头和字段来构建请求映射,然后读取响应标头和字段,并使用源消息数组和 xpath 填充值来创建响应数组。就是这样,没有什么高深莫测的。
在代码中,Message Complexity 由 MessageManager 类管理。类似地,Transaction Meta Info 由 MetaInfoManager 管理,它是一个单例类,读取文件并将 XML 内容存储在内存中。
TCP 通信由下载的开源类 TCPServer3 管理,我已对其进行了一些调整以满足我的特定需求。
关注点
这里有趣的方法是,XPath 驱动的灵活性非常强大。可以在数小时内构建一个完整的模拟层,并且它非常易于更改。通过添加/删除字段或更改 XPath,可以在几分钟内修改一个交易。
历史
版本 1.0