资讯专栏INFORMATION COLUMN

XStream自定义XML转换器

Little_XM / 3529人阅读

摘要:跟进解析的源码,没找到加载的地方,时间紧迫,也没时间去仔细阅读文档,于是干脆自己动手重写了一个简单的从到的转换器。自定义直接实现这个接口,方法返回,直接接手整个的解析工作。

莫名其妙的异常

昨天做一个项目时用到了XStream来做XML到Bean的转换器,需要转换的Bean格式如下:

</>复制代码

  1. @Data
  2. @XStreamAlias("Document")
  3. public class AccountTradeHistoryResponseVo {
  4. @XStreamAlias("ResponseHeader")
  5. private CommonResponseHeader header;
  6. @XStreamAlias("Content")
  7. private List content;
  8. }

本以为一切顺利,结果却报了个意料之外的异常:

</>复制代码

  1. java.lang.ClassCastException: com.xinzhen.pay.vo.jj.powercore.response.AccountTradeHistoryDetail cannot be cast to com.xinzhen.pay.vo.jj.powercore.response.AccountTradeHistoryDetail

明明是同一个类,怎么就转换异常了呢,百思不得其解!

Converter链

XStream提供了Converter接口可以用来自定义转换器,接口定义如下:

</>复制代码

  1. public interface Converter extends ConverterMatcher {
  2. // Bean -> XML/Json
  3. void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext context);
  4. // XML/Json -> Bean
  5. Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context);
  6. }
  7. public interface ConverterMatcher {
  8. // 是否支持clazz类型的转换
  9. boolean canConvert(Class clazz);
  10. }

Converter的设计使用了责任链模式,类似于SpringMVC的ViewResolvers链,通过canConverter()方法判断是否支持该元素类型的转换,如果支持则调用这个Converter的marshal()或unmarshal()来做Bean到XML/Json之间的转换;否则转移到下一个注册的Converter继续判断流程。

先简单继承了一下AbstractCollectionConverter,然后在解析的时候注册这个Converter,查看一下这里的Class之间到底有什么猫腻。

</>复制代码

  1. public class CustomCollectionConverter extends AbstractCollectionConverter {
  2. public CustomCollectionConverter(Mapper mapper) {
  3. super(mapper);
  4. }
  5. @Override
  6. public boolean canConvert(Class clazz) {
  7. Class clazz1 = AccountTradeHistoryDetail.class;
  8. System.out.println(clazz1 == clazz);
  9. ClassLoader classLoader1 = clazz.getClassLoader();
  10. ClassLoader classLoader2 = clazz1.getClassLoader();
  11. return clazz1 == clazz;
  12. }
  13. @Override
  14. public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext context) {
  15. }
  16. @Override
  17. public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
  18. return null;
  19. }
  20. }

果然不出所料,当传进来的clazz是AccountTradeHistoryDetail.class时,跟clazz1竟然不是同一个Class对象,两个ClassLoader也不相同,一个是RestartClassLoader, 另一个是AppClassLoader;因为项目是使用SpringBoot构建的,有两个ClassLoader是正常的,但为什么AccountTradeHistoryDetail.class这个类会被这两个ClassLoader分别加载一次呢?为了排除SpringBoot本身的问题,于是又写了个方法测试了一下:

</>复制代码

  1. Class clazz = AccountTradeHistoryDetail.class;
  2. Field f = AccountTradeHistoryResponseVo.class.getDeclaredField("content");
  3. // content为List,很明显是泛型参数
  4. ParameterizedType t = (ParameterizedType) f.getGenericType();
  5. Type[] types = t.getActualTypeArguments();
  6. Class clazz1 = (Class) types[0]; // 第一个类型就是实际泛型类型
  7. System.out.println(clazz == clazz1);

这个地方为true,说明在这里这两个AccountTradeHistoryDetail是同一个Class对象,那么就可以排除SpringBoot的问题;看来是XStream出于什么原因重新加载了这个类,但是明明可以通过反射从字段中得出实际的参数类型,不知道XStream为什么要这么做。跟进XStream解析的源码,没找到加载Class的地方,时间紧迫,也没时间去仔细阅读文档,于是干脆自己动手重写了一个简单的从XML到Bean的转换器。

自定义Converter

直接实现Converter这个接口,canConvert()方法返回true,直接接手整个Document的解析工作。

</>复制代码

  1. public class CustomConverter implements Converter {
  2. // 根结点下的成员变量类型
  3. private Map rootTypeMap;
  4. // 根结点下List成员类型(若泛型 T=List, 也应该放在listItemType里)
  5. private Map listItemMap;
  6. // 根结点下的成员变量字段
  7. private Map rootFieldMap;
  8. // 要解析的类型实例(ROOT)
  9. private Object instance;
  10. /**
  11. * @param instanceType 要解析的实例类型
  12. * @param typeMap 泛型<成员变量名, 类型>Map
  13. * @param listItemType List类型<成员变量名, 类型>Map
  14. * @throws Exception
  15. */
  16. public CustomConverter(Class instanceType, Map typeMap, Map listItemType) throws Exception {
  17. instance = instanceType.newInstance();
  18. this.rootTypeMap = typeMap == null ? new HashMap<>() : rootTypeMap;
  19. this.listItemMap = listItemType == null ? new HashMap<>() : listItemType;
  20. rootFieldMap = new HashMap<>();
  21. Field[] fields = instanceType.getDeclaredFields();
  22. for (Field field : fields) {
  23. XStreamAlias annotation = field.getAnnotation(XStreamAlias.class);
  24. // 字段名, 如果设置了别名则使用别名
  25. String fieldName = annotation == null ? field.getName() : annotation.value();
  26. rootFieldMap.put(fieldName, field);
  27. }
  28. }
  29. @Override
  30. public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext context) {
  31. }
  32. @Override
  33. public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
  34. try {
  35. // Root下节点处理
  36. while (reader.hasMoreChildren()) {
  37. reader.moveDown();
  38. String nodeName = reader.getNodeName();
  39. Field field = rootFieldMap.get(nodeName);
  40. if (field == null) {
  41. reader.moveUp();
  42. continue;
  43. }
  44. Class type = rootTypeMap.get(nodeName);
  45. if (type == null) {
  46. type = field.getType();
  47. }
  48. field.setAccessible(true);
  49. // 该节点为List类型
  50. if (listItemMap.containsKey(nodeName)) {
  51. List list = new ArrayList();
  52. Class itemType = listItemMap.get(nodeName);
  53. if (itemType == String.class) { // List
  54. while (reader.hasMoreChildren()) {
  55. reader.moveDown();
  56. list.add(reader.getValue());
  57. reader.moveUp();
  58. }
  59. } else { // List
  60. while (reader.hasMoreChildren()) {
  61. reader.moveDown();
  62. list.add(parseObject(itemType, reader));
  63. reader.moveUp();
  64. }
  65. }
  66. field.set(instance, list);
  67. } else if (type == String.class) { // 该节点为String类型, 直接设置value
  68. field.set(instance, reader.getValue());
  69. } else { // 非String类型, 解析该节点
  70. field.set(instance, parseObject(type, reader));
  71. }
  72. reader.moveUp();
  73. }
  74. } catch (Exception e) {
  75. e.printStackTrace();
  76. return null;
  77. }
  78. return instance;
  79. }
  80. /**
  81. * 解析子节点: 子节点只能是非基本类型(包括String)
  82. *
  83. * @param type
  84. * @param reader
  85. * @return
  86. */
  87. public Object parseObject(Class type, HierarchicalStreamReader reader) throws Exception {
  88. Object obj = type.newInstance();
  89. Map fieldMap = new HashMap<>();
  90. Field[] fields = type.getDeclaredFields();
  91. for (Field field : fields) {
  92. XStreamAlias annotation = field.getAnnotation(XStreamAlias.class);
  93. // 字段名, 如果设置了别名则使用别名
  94. String fieldName = annotation == null ? field.getName() : annotation.value();
  95. fieldMap.put(fieldName, field);
  96. }
  97. while (reader.hasMoreChildren()) {
  98. reader.moveDown();
  99. String nodeName = reader.getNodeName();
  100. // 获取对应的字段
  101. Field field = fieldMap.get(nodeName);
  102. if (field == null) {
  103. reader.moveUp();
  104. continue;
  105. }
  106. Class fType = field.getType();
  107. field.setAccessible(true);
  108. if (fType == String.class) { // String类型, 直接设置value
  109. field.set(obj, reader.getValue());
  110. } else { // 其他类型, 继续解析
  111. field.set(obj, parseObject(fType, reader));
  112. }
  113. reader.moveUp();
  114. }
  115. return obj;
  116. }
  117. /**
  118. * 这个Converter作为所有字段的Converter
  119. *
  120. * @param type
  121. * @return
  122. */
  123. @Override
  124. public boolean canConvert(Class type) {
  125. return true;
  126. }
  127. }

该注册器的构造方法有几个关键参数:

</>复制代码

  1. Class instanceType: 要转换的目标类型

  2. Map typeMap: 泛型类型的字段名和实际类型的Map

  3. Map listItemType: List类型的字段名和实际类型的Map

虽然功能简单,但至少满足了目前转换的需求;有关XStream类加载的问题,有时间还得好好研究。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68107.html

相关文章

  • XStream定义XML换器

    摘要:跟进解析的源码,没找到加载的地方,时间紧迫,也没时间去仔细阅读文档,于是干脆自己动手重写了一个简单的从到的转换器。自定义直接实现这个接口,方法返回,直接接手整个的解析工作。 莫名其妙的异常 昨天做一个项目时用到了XStream来做XML到Bean的转换器,需要转换的Bean格式如下: @Data @XStreamAlias(Document) public class AccountT...

    Nosee 评论0 收藏0
  • 使用XStream实现Java对象与XML互相转换

    摘要:简介是一个对象与互相转换的工具类库。官网链接简单使用下载页面使用构建项目的加入以下依赖创建对象转使用方法。创建解析对象设置别名默认会输出全路径转为转换后的文本为转对象使用方法。 XStream简介 XStream是一个Java对象与XML互相转换的工具类库。 官网链接: http://x-stream.github.io/index.html 简单使用 下载页面:http://x-st...

    崔晓明 评论0 收藏0
  • 2016年度最受欢迎的100个 Java 库

    摘要:最受欢迎的个库连续两年,二度成为中最受欢迎的库。此外,谷歌的开源项目来势汹汹,勇夺第三名,该库包含了一系列谷歌内含的核心库。在本次最受欢迎的个库中,个库与相关。 【编者按】本文作者为 Henn Idan,主要介绍基于 GitHub 中的数据分析,得出的2016年度最受欢迎的100个 Java 库。本文系国内 ITOM 管理平台 OneAPM 编译呈现。 谁拔得头筹?谁又落于人后?我们分...

    nihao 评论0 收藏0
  • java版微信公众号开发(四):定义菜单的实现

    摘要:想要实现自定义菜单的功能,需要有已认证订阅号和已认证服务号。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。 想要实现自定义菜单的功能,需要有已认证订阅号和已认证服务号。对于测试开发来说,可以直接申请一个测试账号:http://mp.weixin.qq.com/debug... 同样需要token的验证,前期接口已经定义好了,直接拿来就可以 showImg(https...

    mo0n1andin 评论0 收藏0

发表评论

0条评论

Little_XM

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<