莫名其妙的异常
昨天做一个项目时用到了XStream来做XML到Bean的转换器,需要转换的Bean格式如下:
@Data
@XStreamAlias("Document")
public class AccountTradeHistoryResponseVo {
@XStreamAlias("ResponseHeader")
private CommonResponseHeader header;
@XStreamAlias("Content")
private List 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接口可以用来自定义转换器,接口定义如下:
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的设计使用了责任链模式,类似于SpringMVC的ViewResolvers链,通过canConverter()方法判断是否支持该元素类型的转换,如果支持则调用这个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, HierarchicalStreamWriter writer, MarshallingContext context) {
}
@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
return null;
}
}
果然不出所料,当传进来的clazz是AccountTradeHistoryDetail.class时,跟clazz1竟然不是同一个Class对象,两个ClassLoader也不相同,一个是RestartClassLoader, 另一个是AppClassLoader;因为项目是使用SpringBoot构建的,有两个ClassLoader是正常的,但为什么AccountTradeHistoryDetail.class这个类会被这两个ClassLoader分别加载一次呢?为了排除SpringBoot本身的问题,于是又写了个方法测试了一下:
Class> clazz = AccountTradeHistoryDetail.class;
Field f = AccountTradeHistoryResponseVo.class.getDeclaredField("content");
// content为List,很明显是泛型参数
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 rootTypeMap;
// 根结点下List成员类型(若泛型 T=List, 也应该放在listItemType里)
private Map listItemMap;
// 根结点下的成员变量字段
private Map rootFieldMap;
// 要解析的类型实例(ROOT)
private Object instance;
/**
* @param instanceType 要解析的实例类型
* @param typeMap 泛型Map
* @param listItemType List类型Map
* @throws Exception
*/
public CustomConverter(Class instanceType, Map typeMap, Map 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, HierarchicalStreamWriter writer, MarshallingContext context) {
}
@Override
public Object unmarshal(HierarchicalStreamReader reader, 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
while (reader.hasMoreChildren()) {
reader.moveDown();
list.add(reader.getValue());
reader.moveUp();
}
} else { // List
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 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;
}
}
该注册器的构造方法有几个关键参数:
Class instanceType: 要转换的目标类型
Map typeMap: 泛型类型的字段名和实际类型的Map
Map listItemType: List类型的字段名和实际类型的Map
虽然功能简单,但至少满足了目前转换的需求;有关XStream类加载的问题,有时间还得好好研究。