解析JSON數組正常,卻在獲取數組元素時拋出了類型轉換異常

關注“Java藝術”一起來充電吧!

1

BUG重現與原因分析

下面這段代碼會拋出類型轉換異常(ClassCastException),JVM給出的解釋是:不能將Double類型對象轉換String類型 (java.lang.Double connot be cast to java.lang.String)。

public class JsonUtilTest{
    @Test
    public void testToObjectArray() {
        String jsonArray = "[222.22,11.22,12.24]";
        List<String> list = JsonUtils.fromJsonArray(jsonArray, String.class);
        String item = list.get(0);
    }
}

根據異常棧信息得知類型轉換異常發生在String item = list.get(0);這行代碼。

可是解析都正常,爲什麼調用Listget方法卻拋出類型轉換異常呢?

這就不得不提泛型的"類型擦除"了。

List<String>經過類型擦除後變爲裸類型List, 而List存儲的元素類型變爲Object類型,上面的代碼編譯後等價於:

public class JsonUtilTest{
    @Test
    public void testToObjectArray() {
        String jsonArray = "[222.22,11.22,12.24]";
        List list = JsonUtils.fromJsonArray(jsonArray, String.class);
        String item = (String)list.get(0);
    }
}

由此可以定位到問題就出在JsonUtilsfromJsonArray方法。fromJsonArrayjson解析爲Double類型的數組了, 所以會拋出ClassCastException異常,Double類型對象強制轉爲String類型失敗。

JsonUtils工具類是筆者爲項目封裝的一個Json解析工具類,目的是適配多個json解析框架。

例子中調用 JsonUtilsfromJsonArray方法可能是調用GsonParserfromJsonArray方法,也可能是調用 JacksonParserfromJsonArray方法,會根據項目中依賴了哪個json解析框架決定。

假設我們項目中使用的是Gson,那麼調用JsonUtilsfromJsonArray方法最終會調用GsonParserfromJsonArray方法, GsonParser實現的fromJsonArray方法如下:

public class GsonParser implements JsonParser{
    
    @Override
    public <T> List<T> fromJsonArray(String jsonStr, Class<T> tClass) {
        GsonBuilder gsonBuilder = new GsonBuilder();
                //.registerTypeAdapter(Date.class, new DateTypeAdapter(null))
                //.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter(null))
                //.addDeserializationExclusionStrategy(new GsonExclusionStrategy());
        return gsonBuilder.create().fromJson(jsonStr, new TypeToken<List<T>>(){}.getType());
    }

}

問題就出現在new TypeToken<List<T>>(){}.getType()這行,這行代碼編譯後會生成一個繼承TypeToken的匿名內部類, 但由於TypeToken指定的參數化類型爲List<T>,將getType()方法返回的Type對象傳給Gson框架, Gson框架是不知道List<T>的參數化類型T是什麼的。Gson框架只知道將json解析爲一個List,但不知道 List的參數化類型T是什麼,所以就根據json的信息將其轉換爲Double類型了。

我們來看個例子:

public class GsonTypeTokenTest{

        private <T> void getTypeToken2() {
            Type type = new TypeToken<List<T>>() {}.getType();
            System.out.println(type);
        }
    
        private void getTypeToken1() {
            Type type = new TypeToken<List<String>>() {}.getType();
            System.out.println(type);
        }
    
        @Test
        public void testTypeToken() {
            getTypeToken1();
            getTypeToken2();
        }

}

上面代碼輸出的結果如下:

java.util.List<java.lang.String>
java.util.List<T>

從結果可以看出,getTypeToken2方法我們無法獲取到List的參數化類型T的實際類型,而getTypeToken1方法中指定了List的參數化類型爲String, 因此能夠獲取到。


2

BUG修復

如果只是使用Gson解析框架,修改該BUG的辦法很簡單,將GsonParserfromJsonArray方法改爲如下即可:

public <T> List<T> fromJsonArray(String jsonStr, TypeToken<List<T>> type){
    .....
    return gsonBuilder.create().fromJson(jsonStr, type.getType());
}

因爲筆者寫的JsonUtils工具類要適配多種解析框架,因此我們不能使用Gson框架的TypeToken, 也不能使用Jackson框架的TypeReference,而是抽象出一箇中間類。

  • 1、自定義TypeReference

public abstract class TypeReference<T> {

    protected final Type _type;

    protected TypeReference() {
        Type superClass = this.getClass().getGenericSuperclass();
        if (superClass instanceof Class) {
            throw new IllegalArgumentException("TypeReference constructed without actual type information");
        } else {
            this._type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
        }
    }

    public Type getType() {
        return this._type;
    }

}
  • 2、修改JsonParser接口的fromJsonArray方法

public interface JsonParser {

    <T> String toJsonString(T obj, boolean serializeNulls, String pattern);

    <T> T fromJson(String jsonStr, Class<T> tClass);

    <T> List<T> fromJsonArray(String jsonStr, TypeReference<List<T>> typeReference);

}
  • 3、修改GsonParserJacksonParserfromJsonArray方法

GsonParserfromJsonArray方法修改後如下:

public class GsonParser implements JsonParser {

    @Override
    public <T> List<T> fromJsonArray(String jsonStr, TypeReference<List<T>> typeReference) {
        GsonBuilder gsonBuilder = new GsonBuilder();
                //.registerTypeAdapter(Date.class, new DateTypeAdapter(null))
                //.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeTypeAdapter(null))
                //.addDeserializationExclusionStrategy(new GsonExclusionStrategy());
        return gsonBuilder.create().fromJson(jsonStr, typeReference.getType());
    }

}

JacksonParserfromJsonArray方法修改後如下:

public class JacksonParser implements JsonParser {
    
    @Override
    public <T> List<T> fromJsonArray(String jsonStr, TypeReference<List<T>> typeReference) {
        ObjectMapper objectMapper = new ObjectMapper();
        // ......
        try {
            return objectMapper.readValue(jsonStr, new com.fasterxml.jackson.core.type.TypeReference() {
                @Override
                public Type getType() {
                    return typeReference.getType();
                }
            });
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}
  • 4、修改JsonUtilsfromJsonArray方法

public class JsonUtils {

    private static JsonParser chooseJsonParser;

    static {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        try {
            classLoader.loadClass("com.google.gson.GsonBuilder");
            chooseJsonParser = new GsonParser();
        } catch (ClassNotFoundException e) {
            try {
                classLoader.loadClass("com.fasterxml.jackson.databind.ObjectMapper");
                chooseJsonParser = new JacksonParser();
            } catch (ClassNotFoundException ex) {
                throw new RuntimeException("未找到任務json包,請先在當前項目的依賴配置文件中加入 gson或fackson");
            }
        }
    }

    public static <T> String toJsonString(T obj) {
        return toJsonString(obj, false, null);
    }

    public static <T> String toJsonString(T obj, boolean serializeNulls) {
        return toJsonString(obj, serializeNulls, null);
    }

    public static <T> String toJsonString(T obj, boolean serializeNulls, String datePattern) {
        return chooseJsonParser.toJsonString(obj, serializeNulls, datePattern);
    }

    public static <T> T fromJson(String jsonStr, Class<T> tClass) {
        return chooseJsonParser.fromJson(jsonStr, tClass);
    }

    // 修改後的fromJsonArray方法
    public static <T> List<T> fromJsonArray(String jsonStr, TypeReference<List<T>> typeReference) {
        return chooseJsonParser.fromJsonArray(jsonStr, typeReference);
    }

}

公衆號:Java藝術

掃碼關注最新動態

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