字数 13070
1 引文
本文主要介绍内容如下:
-
liquid在xml建模上的利用,以及liquid生成java代码,方便对模型的操作
-
liquid 2011在使用时的一些问题及解决办法
2 起因
在开展同招行接口互通时,与前置程序的通信报文都是XML格式的,接口虽然不算多,但是数据项比较多,比如详情查询的接口返回的数据可能有48项之多,按照招行提供的报文工具得搞死人:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.ByteArrayInputStream;
import org.xml.sax.SAXException;
/**
*
* 招行XML通讯报文类
*
*/
public class XmlPacket{
protected String FUNNAM;
protected final String DATTYP="2";//报文类型固定为2
protected String LGNNAM;
protected String RETCOD;
protected String ERRMSG;
protected Map data; //<String,Vector>
public XmlPacket(){
data = new Properties();
}
public XmlPacket(String sFUNNAM){
FUNNAM = sFUNNAM;
data = new Properties();
}
public XmlPacket(String sFUNNAM, String sLGNNAM){
FUNNAM = sFUNNAM;
LGNNAM = sLGNNAM;
data = new Properties();
}
public String getFUNNAM() {
return FUNNAM;
}
public void setFUNNAM(String fUNNAM) {
FUNNAM = fUNNAM;
}
public String getLGNNAM() {
return LGNNAM;
}
public void setLGNNAM(String lGNNAM) {
LGNNAM = lGNNAM;
}
public String getRETCOD() {
return RETCOD;
}
public void setRETCOD(String rETCOD) {
RETCOD = rETCOD;
}
public String getERRMSG() {
return ERRMSG;
}
public void setERRMSG(String eRRMSG) {
ERRMSG = eRRMSG;
}
/**
* XML报文返回头中内容是否表示成功
* @return
*/
public boolean isError(){
if(RETCOD.equals("0")){
return false;
}else{
return true;
}
}
/**
* 插入数据记录
* @param sSectionName
* @param mpData <String, String>
*/
public void putProperty(String sSectionName, Map mpData){
if(data.containsKey(sSectionName)){
Vector vt = (Vector)data.get(sSectionName);
vt.add(mpData);
}else{
Vector vt = new Vector();
vt.add(mpData);
data.put(sSectionName, vt);
}
}
/**
* 取得指定接口的数据记录
* @param sSectionName
* @param index 索引,从0开始
* @return Map<String,String>
*/
public Map getProperty(String sSectionName, int index){
if(data.containsKey(sSectionName)){
return (Map)((Vector)data.get(sSectionName)).get(index);
}else{
return null;
}
}
/**
* 取得制定接口数据记录数
* @param sSectionName
* @return
*/
public int getSectionSize(String sSectionName){
if(data.containsKey(sSectionName)){
Vector sec = (Vector)data.get(sSectionName);
return sec.size();
}
return 0;
}
/**
* 把报文转换成XML字符串
* @return
*/
public String toXmlString(){
StringBuffer sfData = new StringBuffer(
"<?xml version='1.0' encoding = 'GBK'?>");
sfData.append("<CMBSDKPGK>");
sfData
.append("<INFO><FUNNAM>"+FUNNAM+"</FUNNAM><DATTYP>"+DATTYP+"</DATTYP><LGNNAM>"+LGNNAM+"</LGNNAM></INFO>");
int secSize = data.size();
Iterator itr = data.keySet().iterator();
while(itr.hasNext()){
String secName = (String)itr.next();
Vector vt = (Vector)data.get(secName);
for(int i=0; i<vt.size(); i++){
Map record = (Map)vt.get(i);
Iterator itr2 = record.keySet().iterator();
sfData.append("<"+secName+">");
while(itr2.hasNext()){
String datakey = (String)itr2.next();
String dataValue = (String)record.get(datakey);
sfData.append("<"+datakey+">");
sfData.append(dataValue);
sfData.append("</"+datakey+">");
}
sfData.append("</"+secName+">");
}
}
sfData.append("</CMBSDKPGK>");
return sfData.toString();
}
/**
* 解析xml字符串,并转换为报文对象
* @param message
*/
public static XmlPacket valueOf(String message) {
SAXParserFactory saxfac = SAXParserFactory.newInstance();
try {
SAXParser saxparser = saxfac.newSAXParser();
ByteArrayInputStream is = new ByteArrayInputStream(message.getBytes());
XmlPacket xmlPkt= new XmlPacket();
saxparser.parse(is, new SaxHandler(xmlPkt));
is.close();
return xmlPkt;
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.*;
/**
*
* 招行XML报文解析类
*
*/
public class SaxHandler extends DefaultHandler {
int layer=0;
String curSectionName;
String curKey;
String curValue;
XmlPacket pktData;
Map mpRecord;
public SaxHandler(XmlPacket data){
curSectionName = "";
curKey = "";
curValue = "";
pktData = data;
mpRecord = new Properties();
}
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
layer++;
if(layer==2){
curSectionName = qName;
}else if(layer==3){
curKey = qName;
}
}
public void endElement(String uri, String localName, String qName)
throws SAXException {
if(layer==2){
pktData.putProperty(curSectionName, mpRecord);
mpRecord = new Properties();
}else if(layer==3){
mpRecord.put(curKey, curValue);
if(curSectionName.equals("INFO")){
if(curKey.equals("FUNNAM")){
pktData.setFUNNAM(curValue);
}else if(curKey.equals("LGNNAM")){
pktData.setLGNNAM(curValue);
}else if(curKey.equals("RETCOD")){
pktData.setRETCOD(curValue);
}else if(curKey.equals("ERRMSG")){
pktData.setERRMSG(curValue);
}
}
}
curValue = "";
layer--;
}
public void characters(char[] ch, int start, int length)
throws SAXException {
if(layer==3){
String value = new String(ch, start, length);
if(ch.equals("\n")){
curValue += "\r\n";
}else{
curValue += value;
}
}
}
}
import java.io.*;
import java.net.*;
import java.util.Map;
import java.util.Properties;
/**
*
* HTTP通讯范例: 直接支付
*
*/
public class HttpRequest {
/**
* 生成请求报文
*
* @return
*/
private String getRequestStr() {
// 构造支付的请求报文
XmlPacket xmlPkt = new XmlPacket("Payment", "USRA01");
Map mpPodInfo = new Properties();
mpPodInfo.put("BUSCOD", "N02031");
xmlPkt.putProperty("SDKPAYRQX", mpPodInfo);
Map mpPayInfo = new Properties();
mpPayInfo.put("YURREF", "201009270001");
mpPayInfo.put("DBTACC", "571905400910411");
mpPayInfo.put("DBTBBK", "57");
mpPayInfo.put("DBTBNK", "招商银行杭州分行营业部");
mpPayInfo.put("DBTNAM", "NEXT TEST");
mpPayInfo.put("DBTREL", "0000007715");
mpPayInfo.put("TRSAMT", "1.01");
mpPayInfo.put("CCYNBR", "10");
mpPayInfo.put("STLCHN", "N");
mpPayInfo.put("NUSAGE", "费用报销款");
mpPayInfo.put("CRTACC", "571905400810812");
mpPayInfo.put("CRTNAM", "测试收款户");
mpPayInfo.put("CRTBNK", "招商银行");
mpPayInfo.put("CTYCOD", "ZJHZ");
mpPayInfo.put("CRTSQN", "摘要信息:[1.01]");
xmlPkt.putProperty("SDKPAYDTX", mpPayInfo);
return xmlPkt.toXmlString();
}
/**
* 连接前置机,发送请求报文,获得返回报文
*
* @param data
* @return
* @throws MalformedURLException
*/
private String sendRequest(String data) {
String result = "";
try {
URL url;
url = new URL("http://localhost:8080");
HttpURLConnection conn;
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoInput(true);
conn.setDoOutput(true);
OutputStream os;
os = conn.getOutputStream();
os.write(data.toString().getBytes("gbk"));
os.close();
BufferedReader br = new BufferedReader(new InputStreamReader(conn
.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
result += line;
}
System.out.println(result);
br.close();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (ProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
/**
* 处理返回的结果
*
* @param result
*/
private void processResult(String result) {
if (result != null && result.length() > 0) {
XmlPacket pktRsp = XmlPacket.valueOf(result);
if (pktRsp != null) {
String sRetCod = pktRsp.getRETCOD();
if (sRetCod.equals("0")) {
Map propPayResult = pktRsp.getProperty("NTQPAYRQZ", 0);
String sREQSTS = (String) propPayResult.get("REQSTS");
String sRTNFLG = (String) propPayResult.get("RTNFLG");
if (sREQSTS.equals("FIN") && sRTNFLG.equals("F")) {
System.out.println("支付失败:"
+ propPayResult.get("ERRTXT"));
} else {
System.out.println("支付已被银行受理(支付状态:" + sREQSTS + ")");
}
} else if (sRetCod.equals("-9")) {
System.out.println("支付未知异常,请查询支付结果确认支付状态,错误信息:"
+ pktRsp.getERRMSG());
} else {
System.out.println("支付失败:" + pktRsp.getERRMSG());
}
} else {
System.out.println("响应报文解析失败");
}
}
}
public static void main(String[] args) {
try {
HttpRequest request = new HttpRequest();
// 生成请求报文
String data = request.getRequestStr();
// 连接前置机,发送请求报文,获得返回报文
String result = request.sendRequest(data);
// 处理返回的结果
request.processResult(result);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
招行的工具类明显有几个特点:
-
(1)抽象的数据包,用基于SAX的解析方式,用MAP和properties构造数据结构,其中层次属性上有很多硬编码,基础的ELEMENT本身就有一些硬编码的属性;
-
(2)具体的数据包,就有更多的硬编码了,看打包请求报文和解释响应报文处,就知道是非常地容易出差错
硬编码是一种表象的硬伤,结合用键值对这种建数据的思路,最终的性质就是:ELEMENTS间的关系非常弱化;ELEMENT的数据项没有约束,用这一套工具去开发接口互通非常致命,开发的重点不是放在业务上,将会浪费大量的时间在梳理和规范ELEMENT以及它们之间的关系上。
3 怎么办?
想到XML模板的做法,只要定义模板,然后从模板里通过变量拿数据(或者往模板里塞数据),模板语言有FREEMARKER/VELOCITY,但都不尽如人意。
4 利器来了
接触XML处理并不多,在谷歌关键字检索结果闲庭散步之间,了解一款XML工作室:LIQUID XML STUDIO
,用了它的一些基本功能就足以解决我的诉求了:
-
(1)建立报文的ELEMENT间的关系,必须存在和可存在的ELEMENT约束,XSD层面的规范在第一层面就检验了数据包的合法性,这种岂是招商的工具能搞定的事?
-
(2)ELEMENT的值的类型、长度、范围、枚举等约束,这个看起来锦上添花的功能,恰恰大大减少出错的机率,比如之前在解析银行账号时,定义的LONG型解析不了一些超长的古老的比如工行19位长度的账号,如果类型搞不定直接告诉你错误之处了,这在建立请求报文时同样非常有用;另外一点,合适的类型解析XML效率会更高;类型到最终数据的类型不需要再转换。
-
(3)能生成JAVA代码,直接用对象来建立ELEMENT间的联系了,活生生地;
-
(4)对象的初始化可以从XML模板文件来,而且XML模板文件是根据XSD生成的规范的,这省掉了很多事。
LIQUID使用示例放一个GIF动画如下:
注意:后来发现有一种更好的办法,不要在下方添加ELEMENT,直接在父对象的关系中添加,类似一个树的编辑,这样不会错漏。 但是好像添加是直接的对象,而不是引用,没办法重复利用,还有待更一步挖掘更好的办法
再放一些代码,XML模板文件如:
<?xml version="1.0" encoding="utf-8"?>
<!-- Created with Liquid XML Studio Developer Edition 9.0.11.3078 (http://www.liquid-technologies.com) -->
<CMBSDKPGK xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="NTDMTQRD.xsd">
<INFO>
<FUNNAM>NTDMTQRD</FUNNAM>
<DATTYP>2</DATTYP>
<LGNNAM>string</LGNNAM>
</INFO>
<NTDMTQRDY1>
<INBACC>755937973210000</INBACC>
<BBKNBR>75</BBKNBR>
</NTDMTQRDY1>
</CMBSDKPGK>
//查询虚拟户当天交易
public List<BankPayment> getPaymemtsToday(int id_member) {
com.test.xml.NTDMTQRD.CMBSDKPGK elm = new com.test.xml.NTDMTQRD.CMBSDKPGK();
//文档中需要指定为UTF-8编码(或者string中含中文字符,转成UTF-16:elm.fromXmlStream(str.getBytes("UTF-16"));)
try {
elm.fromXmlFile(TransferService.class.getResource("/xml_template/").getPath() + "NTDMTQRD.xml");
elm.getINFO().setLGNNAM(CoreCommConst.LGNNAME);
elm.getNTDMTQRDY1().setDYANBR(String.format("%010d", id_member));
String xml = elm.toXml();
xml = xml.replace("UTF-8", "GBK");
log.info("发送[查当天交易]请求:\n{}", xml);
//发送给招商了
String retNet = NetUtil.httpPost(xml);
if (retNet == null) return null;
com.test.xml.NTDMTQRD_RET.CMBSDKPGK elm_ret = new com.test.xml.NTDMTQRD_RET.CMBSDKPGK ();
elm_ret.fromXmlStream(retNet.getBytes("UTF-16"));
log.info("接收[查当天交易]反馈:\n{}", elm_ret.toXml());
if (com.test.xml.NTDMTQRD_RET.INFO_RETCOD.n0
!= elm_ret.getINFO().getRETCOD()) { //调用接口失败
return null;
} else {//成功,进一步解释子结果
int account = elm_ret.getNTDMTQRYZ1().count();
if (account > 0) {
List<BankPayment> bps = new ArrayList<BankPayment>();
//仅获取平台入账
for (int i = 0; i < account; i++) {
com.test.xml.NTDMTQRD_RET.NTDMTQRYZ1 p = elm_ret.getNTDMTQRYZ1().getItem(i);
//log.info(p.toXml());
if (p.getTRXAMT() > 0) { //表示入账
BankPayment bp = new BankPayment();
bp.setComment_state(pack.getCommentPayment());
bp.setId_bank_member(Integer.parseInt(p.getDYANBR()));
bp.setMoney(p.getTRXAMT());
bp.setPay_acc_name(p.getRPYNAM());
bp.setPay_acc_no(p.getRPYACC());
bp.setPay_bank_addr(p.isValidRPYADR() ? p.getRPYADR() : "") ;
bp.setPay_bank_flag(p.getRPYBKN().contains("招商") ? "Y" : "N");
bp.setPay_bank_name(p.getRPYBKN());
bp.setSetnbr(p.getTRXSET());
bp.setTrxnbr(p.getTRXNBR());
bps.add(bp);
}
}
return bps;
}//account > 0
}
} catch (LtException | IOException e) {
e.printStackTrace();
return null;
}
return null;
}
就以上几行代码,就搞定了发请求
到招行获得
查某账户下当天的交易流水,非常地清爽。
5 在使用liquid 2011时的问题
在使用过程中有一些问题:
-
liquid 2011在win10上安装成功,安装界面像xp系统安装,其依赖.Net framework 3.0,在[这里](https://www.microsoft.com/en-us/download/details .aspx?id=3005)下载
-
2011版的基本jar包是:LtXmlLib9.jar,依赖xerces-1_4_3.jar,后者版本太老,报错:
org.springframework.beans.factory.BeanDefinitionStoreException: Parser configuration exception parsing XML from
class path resource [spring-context.xml]; nested exception is javax.xml.parsers.ParserConfigurationException:
Unable to validate using XSD: Your JAXP provider [org.apache.xerces.jaxp.DocumentBuilderFactoryImpl@57795907]
does not support XML Schema. Are you running on Java 1.4 with Apache Crimson?
Upgrade to Apache Xerces (or Java 1.5) for full XSD support.
解决办法,启动时加上配置,参考自[这里](http://forum.spring.io/forum/spring-projects/container/27870-unable-to-validate-using-xsd-your-jaxp-provider),和[这里](http://forum.spring.io/forum/spring-projects/container/12025-using-the-container-in-a-standalone-app-with-xml-definitions):
```
-Djavax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl -Djavax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl
```
如果部署的话,需要修改tomcat的配置,修改../bin/catalina.sh,在首行增加,参考自[这里](https://blog.csdn.net/chtnj/article/details/47320251):
```
JAVA_OPTS="-Djavax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl -Djavax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl"
```
-
liquid解析不了GBK编码,这样xml模板文件肯定不能有汉字了,不然加载出错,代码中这样处理才能发送GBK报文给对方
String xml = elm.toXml(); xml = xml.replace("UTF-8", "GBK");
-
回复报文中含GBK编码如何处理?将GBK的内容以UTF-16的编码加载,
elm_ret.fromXmlStream(retNet.getBytes("UTF-16"));
-
UTF-16内容如何保存到mysql数据库中,如果为乱码怎么办?同样加tomcat运行参数,-Dfile.encoding=GBK,其默认的字符集与操作系统有关 ,ubuntu的服务器默认可能是UTF-8编码,那么如果定义的XML是GBK编码,则不能正确转换,包括在mysql连接属性,或mysql库、表、字段中设置编码 ,都不能解决问题。
liquid官网对字符编码的详细介绍: https://www.liquid-technologies.com/XML/Encoding.aspx