字数 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动画如下:
liquid xml建模

注意:后来发现有一种更好的办法,不要在下方添加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