莫名其妙的异常

昨天做一个项目时用到了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类加载的问题,有时间还得好好研究。


低调不奢华
115 声望0 粉丝