莫名其妙的异常
昨天做一个项目时用到了XStream来做XML到Bean的转换器,需要转换的Bean格式如下:
@Data @XStreamAlias("Document") public class AccountTradeHistoryResponseVo { @XStreamAlias("ResponseHeader") private CommonResponseHeader header; @XStreamAlias("Content") private List<AccountTradeHistoryDetail> content; }
本以为一切顺利,结果却报了个意料之外的异常:
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@H_404_16@接口可以用来自定义转换器,接口定义如下:
public interface Converter extends ConverterMatcher { // Bean -> XML/Json void marshal(Object obj,HierarchicalStreamWriter writer,MarshallingContext context); // XML/Json -> Bean Object unmarshal(HierarchicalStreamReader reader,UnmarshallingContext context); } public interface ConverterMatcher { // 是否支持clazz类型的转换 boolean canConvert(Class clazz); }
Converter的设计使用了责任链模式@H_404_16@,类似于SpringMVC的ViewResolvers链,通过
canConverter()@H_404_16@方法判断是否支持该元素类型的转换,如果支持则调用这个Converter的marshal()或unmarshal()来做Bean到XML/Json之间的转换;否则转移到下一个注册的Converter继续判断流程。
先简单继承了一下AbstractCollectionConverter,然后在解析的时候注册这个Converter,查看一下这里的Class之间到底有什么猫腻。
public class CustomCollectionConverter extends AbstractCollectionConverter { public CustomCollectionConverter(Mapper mapper) { super(mapper); } @Override public boolean canConvert(Class clazz) { Class<?> clazz1 = AccountTradeHistoryDetail.class; System.out.println(clazz1 == clazz); ClassLoader classLoader1 = clazz.getClassLoader(); ClassLoader classLoader2 = clazz1.getClassLoader(); return clazz1 == clazz; } @Override public void marshal(Object obj,MarshallingContext context) { } @Override public Object unmarshal(HierarchicalStreamReader reader,UnmarshallingContext context) { return null; } }
果然不出所料,当传进来的clazz是AccountTradeHistoryDetail.class时,跟clazz1竟然不是同一个Class对象,两个ClassLoader也不相同,一个是RestartClassLoader@H_404_16@, 另一个是
AppClassLoader@H_404_16@;因为项目是使用SpringBoot构建的,有两个ClassLoader是正常的,但为什么AccountTradeHistoryDetail.class这个类会被这两个ClassLoader分别加载一次呢?为了排除SpringBoot本身的问题,于是又写了个方法测试了一下:
Class<?> clazz = AccountTradeHistoryDetail.class; Field f = AccountTradeHistoryResponseVo.class.getDeclaredField("content"); // content为List<AccountTradeHistoryDetail>,很明显是泛型参数 ParameterizedType t = (ParameterizedType) f.getGenericType(); Type[] types = t.getActualTypeArguments(); Class<?> clazz1 = (Class) types[0]; // 第一个类型就是实际泛型类型 System.out.println(clazz == clazz1);
这个地方为true,说明在这里这两个AccountTradeHistoryDetail是同一个Class对象,那么就可以排除SpringBoot的问题;看来是XStream出于什么原因重新加载了这个类,但是明明可以通过反射从字段中得出实际的参数类型,不知道XStream为什么要这么做。跟进XStream解析的源码,没找到加载Class的地方,时间紧迫,也没时间去仔细阅读文档,于是干脆自己动手重写了一个简单的从XML到Bean的转换器。
自定义Converter
直接实现Converter这个接口,canConvert()方法返回true,直接接手整个Document的解析工作。
public class CustomConverter implements Converter { // 根结点下的成员变量类型 private Map<String,Class> rootTypeMap; // 根结点下List成员类型(若泛型 T=List<Item>,也应该放在listItemType里) private Map<String,Class> listItemMap; // 根结点下的成员变量字段 private Map<String,Field> rootFieldMap; // 要解析的类型实例(ROOT) private Object instance; /** * @param instanceType 要解析的实例类型 * @param typeMap 泛型<成员变量名,类型>Map * @param listItemType List类型<成员变量名,类型>Map * @throws Exception */ public CustomConverter(Class instanceType,Map<String,Class> typeMap,Class> listItemType) throws Exception { instance = instanceType.newInstance(); this.rootTypeMap = typeMap == null ? new HashMap<>() : rootTypeMap; this.listItemMap = listItemType == null ? new HashMap<>() : listItemType; rootFieldMap = new HashMap<>(); Field[] fields = instanceType.getDeclaredFields(); for (Field field : fields) { XStreamAlias annotation = field.getAnnotation(XStreamAlias.class); // 字段名,如果设置了别名则使用别名 String fieldName = annotation == null ? field.getName() : annotation.value(); rootFieldMap.put(fieldName,field); } } @Override public void marshal(Object obj,UnmarshallingContext context) { try { // Root下节点处理 while (reader.hasMoreChildren()) { reader.moveDown(); String nodeName = reader.getNodeName(); Field field = rootFieldMap.get(nodeName); if (field == null) { reader.moveUp(); continue; } Class type = rootTypeMap.get(nodeName); if (type == null) { type = field.getType(); } field.setAccessible(true); // 该节点为List类型 if (listItemMap.containsKey(nodeName)) { List list = new ArrayList(); Class itemType = listItemMap.get(nodeName); if (itemType == String.class) { // List<String> while (reader.hasMoreChildren()) { reader.moveDown(); list.add(reader.getValue()); reader.moveUp(); } } else { // List<T> while (reader.hasMoreChildren()) { reader.moveDown(); list.add(parSEObject(itemType,reader)); reader.moveUp(); } } field.set(instance,list); } else if (type == String.class) { // 该节点为String类型,直接设置value field.set(instance,reader.getValue()); } else { // 非String类型,解析该节点 field.set(instance,parSEObject(type,reader)); } reader.moveUp(); } } catch (Exception e) { e.printStackTrace(); return null; } return instance; } /** * 解析子节点: 子节点只能是非基本类型(包括String) * * @param type * @param reader * @return */ public Object parSEObject(Class type,HierarchicalStreamReader reader) throws Exception { Object obj = type.newInstance(); Map<String,Field> fieldMap = new HashMap<>(); Field[] fields = type.getDeclaredFields(); for (Field field : fields) { XStreamAlias annotation = field.getAnnotation(XStreamAlias.class); // 字段名,如果设置了别名则使用别名 String fieldName = annotation == null ? field.getName() : annotation.value(); fieldMap.put(fieldName,field); } while (reader.hasMoreChildren()) { reader.moveDown(); String nodeName = reader.getNodeName(); // 获取对应的字段 Field field = fieldMap.get(nodeName); if (field == null) { reader.moveUp(); continue; } Class fType = field.getType(); field.setAccessible(true); if (fType == String.class) { // String类型,直接设置value field.set(obj,reader.getValue()); } else { // 其他类型,继续解析 field.set(obj,parSEObject(fType,reader)); } reader.moveUp(); } return obj; } /** * 这个Converter作为所有字段的Converter * * @param type * @return */ @Override public boolean canConvert(Class type) { return true; } }
虽然功能简单,但至少满足了目前转换的需求;有关XStream类加载的问题,有时间还得好好研究。