概述
也许是人员紧张,一个跨部门调用的新接口开发任务就交给我了。然后在只了解简单业务和接口数据属性名的情况下,开始了第一次的数据接口开发。我甚至没有一个工程模版。
准备
项目组前段时间开始使用maven进行项目构建和依赖管理了,我顺势就自己create一个,把公司私服的封装工具引用一下。要保存数据,添个数据库相关的依赖;用webservice接口,添上axis2的依赖;数据是xml的,dom4j已经添加进去了,日志系统slf4j也有了。创建一个入口类,copy一个META-INF/services.xml文件放上去再改改,大约能跑起来了。
构建
xml格式解析
输入数据是xml格式的,一种简单而“古老”的格式。看着那一半像技术一半像运维写的接口文档,就觉得没这么简单。为了以后好改,决定把解析功能剥离出来。毕竟改个三四次的觉悟还是有的,事实也是如此。
设想格式为<root> <id>1</id> <name>xiaoming</name> <age>18</age> <address>18</address> </root>
在简单百度之后发现java标准库里有个注解@XmlElement不错,拿来用用。
//Person类 public class Person { @XmlElement(name = "id") private String id; @XmlElement(name = "age") private String age; @XmlElement(name = "address") private String address; ... } //XMLHelper类,用于解析接收到的xml格式数据并给javabean赋值 public final class XMLHelper { private final static Logger logger = LoggerFactory.getLogger(XMLHelper.class); private final static String DEFAULT_NAME = "##default";// @XmlElement注解属性的默认值 public static <T> T xmlInitBean(String xmlStr, T obj) { Document dom = getDocument(xmlStr); Element root = dom.getRootElement(); Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(XmlElement.class)) { XmlElement xe = field.getAnnotation(XmlElement.class); // 未指定name属性值的,使用字面量 String xmlNodeName = DEFAULT_NAME.equals(xe.name()) ? field.getName() : xe.name(); Element element = root.element(xmlNodeName); // 指定赋值字段未赋值打印警告并跳过 if(element == null) { logger.warn("{}未成功赋值", field.getName()); continue; } String text = element.getTextTrim(); // 忽略访问权限 field.setAccessible(true); try { field.set(obj, text); } catch (Exception e) { logger.error("反射赋值{}={}发生异常!", field.getName(), text, e); } } } return obj; } /** * 将输入xml数据输出成Document */ private static Document getDocument(String xmlStr) { Document dom = null; try { dom = DocumentHelper.parseText(xmlStr); } catch (DocumentException e) { logger.error("处理输入数据发生异常:", e); throw new RuntimeException(e); } return dom; } }
通过注解信息辅助标记,剥离字段名约束,反射动态赋值,剥离赋值对象类型,可以满足相同xml数据结构情况下,只使用一个方法满足不同类型数据字段。
创建一个类,用于 确认信息的返回,不同接口返回时有些微差异,主要部分基本相同,但仍可通过一个类进行返回xml格式数据的构建
public class ReturnMessage {
private String id;
private Integer rtnCode;
private Optional<String> rtnMsg;
private String assignOpr;
public ReturnMessage(String id, Integer rtnCode, String rtnMsg) {
this.id = id;
this.rtnCode = rtnCode;
this.rtnMsg = Optional.ofNullable(rtnMsg);
}
/**
* 返回一个表示成功的返回信息对象
*
* @param id
* @return
*/
public static ReturnMessage buildSuccessMsg(String id) {
return new ReturnMessage(id, 1, "");
}
/**
* 返回构建一个表示失败的信息对象
*
* @param id
* @param msg
* @return
*/
public static ReturnMessage buildErrorMsg(String id, String msg) {
return new ReturnMessage(id, 0, msg);
}
public ReturnMessage withAssignOpr(String assignOpr) {
this.assignOpr = assignOpr;
return this;
}
}
测试
如预计的一样的,实际数据格式是超出预期的,大约如下
<root code='1'>
<CL P = 'id'>1</CL>
<CL P = 'name'>xiaoming</CL>
<CL P = 'age'>18</CL>
<CL P = 'address'>shanghai</CL>
</root>
这只是举例数据,实际数据复杂度更高,但不宜公开。还存在个别字段实际使用属性名与文档不一致(由于功能实现问题做出的变更,未及时反映到文档并详细通知相关人员),时间格式不明确。
于是修改XmlHelper类,将属性名和值的映射构建成Key-Value形式,在获取属性值赋值时改为直接从构建好的map中获取:
/**
* 将各节点属性名到值信息进行压缩,直接得到k-v键值对
* @param root
* @return
*/
private static Map<String, String> getXmlMap(Element root){
Map<String, String> propMap = new HashMap<>();
@SuppressWarnings("unchecked")
List<Element> elements = root.elements("CL");
for (Element element : elements) {
Attribute attribute = element.attribute("P");
propMap.put(StringUtils.trim(attribute.getValue().trim()), element.getText());
}
return propMap;
}
// 取值部分修改
Map<String, String> propMap = getXmlMap(root);
String text = propMap.get(xmlNodeName);
// 同时可以增加空值校验,利用@XmlElement(name = "id", nillable = false)中nillable属性进行控制字段空值校验
// 指定赋值字段未赋值打印警告并跳过
if(StringUtils.isEmpty(text)) {
logger.warn("{}未成功赋值:"+xe.nillable(), field.getName());
if(!xe.nillable()){
throw new RuntimeException("【item/"+xmlNodeName+"】不可为空!");
}
continue;
}
总结
- 接口类设计文档,事前没有明确数据格式文档真的是比较坑的事情,时间格式,xml结构,数据类型不确定性太高,提前想到的情况下,也不能保证万无一失,而且联调测试人员还是其他公司的,他们开发还在外地远程改完部署再测。还好格式是统一的吧,只用改工具类就完事了。
- 通过反射和注解将数据绑定剥离出来,在控制方面灵活度提高了,增加空值校验的开发代价减少。当然运行时的反射操作其实是不推荐的,所以我赋值时入参用了实例对象,而不是输入class再生成实例,能少用就少用吧。
PS.
数据是假的,看看就好;代码是处理过的,毕竟工作时间写的代码归公司的。仅纪念一次不算太顺利的接口开发工作。
@XmlElement的标记信息还是有可以挖掘的地方的,比如可以标记类型用于格式转换,设置默认值。
希望代码和文档能优雅些。