Java反序列化JSON,要避免泛型的类型擦除问题

原创 吴就业 84 0 2020-05-27

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/9680e6c23b954571a67cf0efe5509aff

作者:吴就业
链接:https://wujiuye.com/article/9680e6c23b954571a67cf0efe5509aff
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

如图,反序列化JSON数组正常,却在获取数组元素时抛出了类型转换异常。

img

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反序列化都正常,为什么调用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();
        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的办法很简单,将GsonParserfromJsonArray方法改为如下即可:

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

因为笔者写的JsonUtils工具类要适配多种解析框架,因此我们不能使用Gson框架的TypeToken, 也不能使用Jackson框架的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;
    }

}
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);

}

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);
        }
    }

}
  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);
      }
  
  }
#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

为什么要选择Spring Cloud Kubernetes?

选择Spring Cloud Kubernetes意味着我们想要将服务部署到Kubernetes集群,Spring Cloud Kubernetes为我们实现了Spring Cloud的一些接口,让我们可以快速搭建Spring Cloud微服务项目框架,并能使用Kubernetes云原生服务。

Spring Cloud Kubernetes入门必知运维知识之Kubernetes

作为开发者,只有足够了解容器技术,才能做好技术选型,以及开发部署在Kubernetes容器服务之上的应用应该要注意哪些问题。如果运维不了解代码,开发也不了解Kubernetes,谁能解决将服务迁移到Kubernetes上遇到的各种问题呢?

Spring动态代理奇怪的空指针异常,字段明明不为空,但方法中获取字段的值确是空的

使用`cglib`生成的代理对象 (继承方式),在父类中,通过代理对象调用父类私有方法不会报错,但字段都是空的。

基准测试框架JMH快速上手

基准测试Benchmark是测量、评估软件性能指标的一种测试,对某个特定目标场景的某项性能指标进行定量的和可对比的测试。

如何获取泛型类的参数化类型?解密Java泛型

框架怎么知道这个`T`到底是什么类型呢?

这又是导致事务注解@Transactional不生效的一个原因

事务方法`A`调用事务方法`B`,当方法`B`抛出的异常被方法`A` `catch`后会发生什么?