如图,反序列化JSON数组正常,却在获取数组元素时抛出了类型转换异常。
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);
这行代码。
可是Json反序列化都正常,为什么调用List
的get
方法却抛出类型转换异常呢?
这就不得不提泛型的”类型擦除”了。
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);
}
}
由此可以定位到问题就出在JsonUtils
的fromJsonArray
方法。fromJsonArray
将json
解析为Double
类型的数组了, 所以会抛出ClassCastException
异常,Double
类型对象强制转为String
类型失败。
JsonUtils
工具类是笔者为项目封装的一个Json
解析工具类,目的是适配多个json
解析框架。
例子中调用 JsonUtils
的fromJsonArray
方法可能是调用GsonParser
的fromJsonArray
方法,也可能是调用 JacksonParser
的fromJsonArray
方法,会根据项目中依赖了哪个json
解析框架决定。
假设我们项目中使用的是Gson
,那么调用JsonUtils
的fromJsonArray
方法最终会调用GsonParser
的fromJsonArray
方法, GsonParser
实现的fromJsonArray
方法如下:
public class GsonParser implements JsonParser{
@Override
public <T> List<T> fromJsonArray(String jsonStr, Class<T> tClass) {
GsonBuilder gsonBuilder = new GsonBuilder();
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
, 因此能够获取到。
BUG修复
如果只是使用Gson
解析框架,修改该BUG
的办法很简单,将GsonParser
的fromJsonArray
方法改为如下即可:
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、修改
GsonParser
与JacksonParser
的fromJsonArray
方法
GsonParser
的fromJsonArray
方法修改后如下:
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());
}
}
JacksonParser
的fromJsonArray
方法修改后如下:
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、修改
JsonUtils
的fromJsonArray
方法
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);
}
}