数据通信是两个信息系统之间进行信息交换的过程,比如业务系统A需要登记业务系统B的数据信息,比如监控心跳之类的等等。基于系统之间的数据通信方式有很多。比如我们常见的基于http协议的rest风格的数据通信,基于Soap协议的webservice的wsdl文档的数据通信,基于socket的xml数据交换方式以及对象序列华数据交换方式等等的,当然还有很多比如rmi等等的远程调用方式,那么我们怎么来规划一个系统的数据通信呢。
以下是我在工作中对这个通信框架的设计的一个思路与实现,希望广大的程序爱好者,提出宝贵意见,或者帮我修正,完善这个框架,让这个数据通信框架更简洁,更强大,我已经把代码上传到github,希望有各类人才,提出宝贵意见。
设计思路:
程序代码中只负责封装需要发送的数据,到一个list或者map集合中,剩下的所有事情,统一交给框架去处理,通过配置文件处理,对框架进行配置,使框架满足各种各样的功能。
实现思路 Service.xml
<services>
<service
serviceId="serviceAccount"
serviceName="获取增量账户信息"
serviceTitle="获取增量账户信息"
validClass="com.yzj.seal.ibank.service.concreteService.AcconutConcreateValid"
>
<serviceAddress host="128.160.22.6" port="9136">
<destinationSys>ESB</destinationSys>
</serviceAddress>
<msgNote>
<!-- sys_head -->
<num name="ServiceCode" realkey="0"><value>服务代码1200300003503</value></num>
<num name="ServiceScene" realkey="1"><value>服务场景03 </value></num>
<num name="ConsumerId" realkey="2"><value>消费系统编号105004</value></num>
<num name="OrgConsumerId" realkey="3"><value>发起方系统编号105004</value></num>
<num name="ConsumerSeqNo" realkey="4"><value>消费系统流水号</value></num>
<num name="OrgConsumerSeqNo" realkey="5"><value>发起方系统流水号</value></num>
<num name="TranDate" realkey="6"><value>交易日期</value></num>
<num name="TranTime" realkey="7"><value>交易时间</value></num>
<!-- app_head -->
<!-- body -->
<num name="SysCd" realkey="8"><value>系统代码105004</value></num>
<num name="TfrIntfCtfPwd" realkey="9"><value>调用接口认证密码UUMS</value></num>
<num name="StrtTm" realkey="10"><value>开始时间</value></num>
<num name="EndTm" realkey="11"><value>结束时间</value></num>
<num name="AcsCstTmlIp" realkey="12"><value>访问者IP(128.160.15.14)</value></num>
</msgNote>
<resParser msgType="xml" parseMethod="parResMsg" />
<msgModel>
<modelName>accountModel</modelName>
<modelValue>
<![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
<service>
<SYS_HEAD>
<ServiceCode>{0}</ServiceCode>
<ServiceScene>{1}</ServiceScene>
<ConsumerId>{2}</ConsumerId>
<OrgConsumerId>{3}</OrgConsumerId>
<ConsumerSeqNo>{4}</ConsumerSeqNo>
<OrgConsumerSeqNo>{5}</OrgConsumerSeqNo>
<TranDate>{6}</TranDate>
<TranTime>{7}</TranTime>
</SYS_HEAD>
<APP_HEAD></APP_HEAD>
<BODY>
<SysCd>{8}</SysCd>
<TfrIntfCtfPwd>{9}</TfrIntfCtfPwd>
<StrtTm>{10}</StrtTm>
<EndTm>{11}</EndTm>
<AcsCstTmlIp>{12}</AcsCstTmlIp>
</BODY>
</service>
]]>
</modelValue>
</msgModel>
</service>
如上图所示,通过一个xml文件对远程系统的服务器做了定义,定义了接口的唯一id,定义了接口的发送的数据校验类,定义了远程服务器的ip地址,以及端口号,定义了报文需要的参数,报文模板,报文的解析方式与解析方法。
通过如上所示的xml配置我定义了我要对接的远程通信端的所有的信息,通过对配置文件进行变更,我可以实现传输不同的数据模板,包括json数据模板,xml数据模板,通过字符串占位符的方式来替换模板中对应报文的位置。
具体步骤
1 系统启动的时候配置文件解析并加载到内存。
2 客户端通过map或者list集合准备好数据之后从内存中获取数据模板对制定位置的报文进行替换
3 通过如上的配置,从代码中获取要发送信息到指定系统的ip端口号
4 指定发送的系统,通过系统的名称来区分通过什么方式来发送,比如socket,httpclient等等的或者wsdl等等
5 最后获取服务器端返回的数据
6 曾今有位老员工提过,获取过来的数据,通过配置进行过滤(未实现)后期完善
7 你的意见....
下面是一些实现的核心代码,希望大家参与完善
public class AbstractEsbHandler {
@SuppressWarnings("unchecked")
public static final Map<String, Object> esbSerConfigs = new ConcurrentHashMap<String, Object>();
Logger logger = Logger.getLogger(AbstractEsbHandler.class);
public static Services esbServices = null;
@SuppressWarnings("restriction")
public AbstractEsbHandler() {
try {
@SuppressWarnings("restriction")
JAXBContext jaxbContext = JAXBContext.newInstance(Services.class);
@SuppressWarnings("restriction")
Unmarshaller unmash = jaxbContext.createUnmarshaller();
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("Service.xml");
logger.info("resourceAsStream " + resourceAsStream);
if (null == resourceAsStream) {
logger.error(this.getClass().getSimpleName() + "Service.xml 文件加载失败 ");
}
esbServices = (Services) unmash.unmarshal(resourceAsStream);
List<Service> service = esbServices.getService();
if (service.size() > 0) {
logger.info("ESBService " + service.size());
for (Service esbs : service) {
// 把各个ESB的接口准备好
esbSerConfigs.put(esbs.getServiceId(), esbs);
}
logger.info(this.getClass() + " " + esbSerConfigs.keySet().size());
}
} catch (Exception e) {
logger.error(this.getClass() + " " + e.getMessage());
}
}
}
如上所示的代码块主要是用来加载配置文件的,并把xml解析成对象放置到缓存中。
public class EsbServiceImp implements IEsbService {
Logger logger = Logger.getLogger(EsbServiceImp.class);
@SuppressWarnings("unchecked")
public Object sendMsg(String serviceId, Object msgParam) throws Exception {
Map<String, Object> result=null;
Service serviceConfig = (Service) AbstractEsbHandler.esbSerConfigs.get(serviceId);
String classname = serviceConfig.getValidClass();
Class<?> clsObj = Class.forName(classname);
logger.info("参与校验的类是 " + clsObj);
AbstractEsbSendMethod esbSendMethod = (AbstractEsbSendMethod) clsObj.newInstance();
Object rebuild = esbSendMethod.buildMsg(serviceConfig, msgParam);
if(null==rebuild){
logger.error("请根据日志检查 Service.xml文件的配置 serviceId 为"+serviceId);
throw new RuntimeException("请根据日志检查 Service.xml文件的配置 serviceId 为"+serviceId);
}
ServiceAddress serviceAddress = serviceConfig.getServiceAddress();
String sysName = serviceAddress.getDestinationSys();
String clapath = (String) Worn_xiaoConstant.senderMap.get(sysName);
if(StringUtils.isEmpty(clapath)){
logger.error("不存在目标系统 "+sysName+"对象的报文发送类"+ clapath);
throw new RuntimeException("不存在目标系统 "+sysName+"对象的报文发送类"+ clapath);
}
logger.error("目标系统 "+sysName+"对象的报文发送类"+ clapath);
Class<?> cla=Class.forName(clapath);
logger.info("目标发送类 " + cla);
ISendToSystem sendToSystem=(ISendToSystem) cla.newInstance();
Object send = sendToSystem.send(serviceConfig, rebuild);
logger.info("真实响应的报文呢:"+send );
ResponseParser resParser = serviceConfig.getResParser();
String msgType = resParser.getMsgType();
String methodName = resParser.getParseMethod();
if(StringUtils.isEmpty(msgType)){
logger.error("请配置解析报文类型:"+msgType);
throw new RuntimeException("不存在目标系统 "+sysName+"对象的报文发送类"+ clapath);
}
logger.info("msgType="+msgType+" methodName="+methodName);
String object = (String) Worn_xiaoConstant.recivMap.get(msgType);
Class<?> classd = Class.forName(object);
Object newInstance = classd.newInstance();
Method method = classd.getMethod(methodName,new Class[]{Object.class});
method.invoke(newInstance, send);
logger.info("解析好的报文:"+ result);
return result;
}
如上所示是通过serviceId获取到service以后,通过把客户端封装的list或者map的数据,进行模板处理,通过调用指定的类,来进行发送模板信息。包括制定的端口号以及IP地址。
* 对接核心的接口
*/
public class SendToCoreSendHandler implements ISendToSystem{
Logger logger = Logger.getLogger(SendToESBSendHandler.class);
private Socket socket;
public Object send(Service service, Object msg) {
StringBuffer buffer = new StringBuffer();
try {
ServiceAddress serviceAddress = service.getServiceAddress();
String host = serviceAddress.getHost();
logger.info("host " + host);
int port = serviceAddress.getPort();
logger.info("port " + port);
socket = new Socket(host, port);
OutputStream outs = socket.getOutputStream();
BufferedOutputStream bufed = new BufferedOutputStream(outs);
byte[] bytes = msg.toString().getBytes();
bufed.write(bytes);
bufed.flush();
InputStream inputStream = socket.getInputStream();
BufferedInputStream stream = new BufferedInputStream(inputStream);
byte[] buf = new byte[1024];
int len = 0;
while ((len = stream.read(buf)) != -1) {
buffer.append(new String(buf, 0, len));
}
bufed.close();
logger.info("buffer.toString() " + buffer.toString());
} catch (IOException exception) {
logger.error("对接ESB的socket通信接口异常 " + exception.getMessage());
}
return buffer.toString();
}
}
如上代码所示是发送类,通过实现发送接口的发送方法进行不同的实现
public class XmlResMsgPaser implements IResonpsePaser {
Logger logger=Logger.getLogger(XmlResMsgPaser.class);
public Map<String, Object> parResMsg(Object args) {
if(null==args) return null;
Map<String, Object> result=new HashMap<String, Object>();
try {
logger.info("报文解析方法 开始解析:parResMsg");
String xmltext=args.toString().substring(
Integer.valueOf(Worn_xiaoConstant.MsgHeader.getCode()));
Document read = DocumentHelper.parseText(xmltext);
Element rootElement = read.getRootElement();
List<?> elements = rootElement.elements();
for(Object element:elements){
Element ele=(Element) element;
String name = ele.getName();
if(Worn_xiaoConstant.msgBoy.getCode().equalsIgnoreCase(name)){
List<?> elements2 = ele.elements();
for(Object obj:elements2){
Element elec=(Element) obj;
result.put(elec.getName(), elec.getText());
}
}
}
} catch (DocumentException e) {
logger.info("文档解析异常 "+ e.getMessage());
throw new RuntimeException("文档解析异常 "+ e.getMessage());
}
return result;
}
}
如上代码是返回的数据进行数据解析的类,通过把返回的数据进行解析封装成map返回给客户端。
这个是xml对应的基本数据结构的类文件。大致的数据结构定义如下所示吧
Struct Serviceaddress{
String host,
String port,
String destinationSys
}
Strutct MsgModle {
String modelName,
String modelValue
}
Struct resParser {
String msgType,
String parseMethod
}
Struct Num{
String name,
String realkey
}
Struct msgNote{
Num[] num;
}
struct Service{
Serviceaddress address,
msgNote msgnote,
resParser respar,
MsgModle model
}
struct services{
Service[] services,
}
辅助工具的实现
public Worn_xiaoConstant() {
senderMap = MsgTranslateUtils.getInstance().getCacheMap("senderMap");
recivMap = MsgTranslateUtils.getInstance().getCacheMap("recivMap");
redisMap = MsgTranslateUtils.getInstance().getCacheMap("redisMap");
logger.info("senderMap "+senderMap.toString());
logger.info("recivMap "+recivMap.toString());
logger.info("redisMap "+redisMap.toString());
}
如上图所示系统启动时加载工具类
public Map<String, Object> getCacheMap(String mapName) {
Map<String, Object> result = new HashMap<String, Object>();
try {
DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance();
DocumentBuilder newDocumentBuilder = builder.newDocumentBuilder();
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(Worn_xiaoConstant.StaticMap.getCode());
Document doc = newDocumentBuilder.parse(resourceAsStream);
NodeList senderMaps = doc.getElementsByTagName(mapName);
for (int i = 0; i < senderMaps.getLength(); i++) {
Node item = senderMaps.item(i);
NodeList childNodes = item.getChildNodes();
for(int j=0;j<childNodes.getLength();j++){
Node item2 = childNodes.item(j);
if(item2.getNodeName().equalsIgnoreCase("kev-val")){
NamedNodeMap attributes2 = item2.getAttributes();
Node namedItem = attributes2.getNamedItem("key");
Node namedItem2 = attributes2.getNamedItem("val");
String nodeValue = namedItem.getNodeValue();
String nodeValue2 = namedItem2.getNodeValue();
result.put(nodeValue, nodeValue2);
}
}
}
} catch (ParserConfigurationException e) {
logger.error("解析配置错误" + e.getMessage());
throw new RuntimeException("解析配置错误" + e.getMessage());
} catch (IOException e) {
logger.error("IO错误" + e.getMessage());
throw new RuntimeException("IO错误" + e.getMessage());
} catch (SAXException e) {
logger.error("文件解析错误" + e.getMessage());
throw new RuntimeException("文件解析错误" + e.getMessage());
}
return result;
}
如上图所示是获取工具类,通过jdom的方式解析xml文件,把工具类放到缓存。
Cachemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<Maps>
<senderMap>
<!-- 对接其他系统的通信类 -->
<kev-val key="ESB" val="com.yzj.seal.ibank.common.destination.SendToESBSendHandler" note="对接ESB的发送类"/>
<kev-val key="CORE" val="com.yzj.seal.ibank.common.destination.SendToCoreSendHandler" note="对接核心的发送类"/>
</senderMap>
<recivMap>
<!-- 对接其他系统的报文解析类 -->
<kev-val key="json" val="com.yjz.seal.ibank.resPaser.JsonResMsgPaser" note="json响应解析类" />
<kev-val key="xml" val="com.yjz.seal.ibank.resPaser.XmlResMsgPaser" note="xml响应解析类" />
</recivMap>
<redisMap>
<kev-val key="isredis" val="false" note="是否开启redis的缓存" />
</redisMap>
</Maps>
通过如上所示的xml文件来制定用哪个类来发送数据,用哪个类来对数据进行解析,以上就是整个通信客户端的设计思路。希望广大程序爱好者多多指点,改善我的编码与设计能力。
Github 地址git@github.com:wornxiao/ms-exchager.git