莫名其妙的異常
昨天做一個項目時用到了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
接口可以用來自定義轉換器,接口定義如下:
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<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, Map<String, 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, 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<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;
}
}
該註冊器的構造方法有幾個關鍵參數:
Class instanceType
: 要轉換的目標類型Map<String, Class> typeMap
: 泛型類型的字段名和實際類型的MapMap<String, Class> listItemType
: List類型的字段名和實際類型的Map
雖然功能簡單,但至少滿足了目前轉換的需求;有關XStream類加載的問題,有時間還得好好研究。