XStream自定義XML轉換器

莫名其妙的異常

昨天做一個項目時用到了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: 泛型類型的字段名和實際類型的Map
  • Map<String, Class> listItemType: List類型的字段名和實際類型的Map

雖然功能簡單,但至少滿足了目前轉換的需求;有關XStream類加載的問題,有時間還得好好研究。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章