fastjson 泛型反序列化的诡异问题

2023-08-18 15:02:45 +08:00
 oneisall8955

版本:fastjson 1.2.76

环境:springboot 2.3.6

问题概述:通过构造 TypeReference 实现泛型反序列化,但实际上序列化出来的并不是想要的实际类型。并且,只有在于线上的某个节点出现,且本地无法复现。

简化业务逻辑代码:

    @Setter
    @Getter
    @ToString
    public static class BaseResponse<T> {
        private Integer code;
        private String msg;
        private T data;
    }
    
    @Setter
    @Getter
    @ToString
    public static class CountryResponse {
        private String countryId;
        private String countryName;
    }
    
    public static class HttpRequestUtil {
        public static <T> List<T> doGet4List(Class<T> responseCls) {
            // 工具类方法,body 变量模拟了请求响应 {"code":0,"success":true,"msg":"success","data":[{"countryId":"CN","countryName":"China"}]}
            String body = "{\"code\":0,\"success\":true,\"msg\":\"success\",\"data\":[{\"countryId\":\"CN\",\"countryName\":\"China\"}]}";
            log.info("doGet4List result: [{}]", body);
            BaseResponse<List<T>> baseResponse = JSON.parseObject(body, new TypeReference<>(responseCls) {
            });
            return Optional.ofNullable(baseResponse).map(BaseResponse::getData).orElse(null);
        }
    }

问题报错代码

    @Test
    public void testFastJsonParse() {
        List<CountryResponse> countryResponses = HttpRequestUtil.doGet4List(CountryResponse.class);
        for (CountryResponse countryResponse : countryResponses) { // 这行报错了
            System.out.println(countryResponse.getCountryId());
        }
    }

查看线上日志,报错行数是:for (CountryResponse countryResponse : countryResponses) { 这一行。

java.lang.ClassCastException: class com.alibaba.fastjson.JSONObject cannot be cast to class com.foo.entity.CountryResponse(com.alibaba.fastjson.JSONObject and com.foo.entity.CountryResponse are in unnamed module of loader org.springframework.boot.loader.LaunchedURLClassLoader @2a2d45ba)

猜测:countryResponses 这个 List 的元素实际上是 JSONObject 对象,遍历时候强转了一次。

主要是线上的所有节点都是同样的镜像,只有某个节点会出这个错误,本地无法复现,拿相同的 json 数据去反序列,都能正确反序列出 CountryResponse 。这个工具类方法都跑了大半年了。。。

自我怀疑人生 ing......

1900 次点击
所在节点    Java
11 条回复
sumarker
2023-08-18 15:11:11 +08:00
我没太懂 BaseResponse<List<T>> 这个里不是一个 list 吗? responseCls 不是 object.class 吗?
oneisall8955
2023-08-18 15:19:21 +08:00
@sumarker #1

接口定义:public static <T> List<T> doGet4List(Class<T> responseCls) {... }

接口调用:List<CountryResponse> countryResponses = HttpRequestUtil.doGet4List(CountryResponse.class);

这里入参不是传递了 CountryResponse.class 了吗?

另外,是不是上面例子用错了 api ,应该用 fastjson 的其他 api 来反序列化 BaseResponse<List<T>?
sumarker
2023-08-18 15:37:49 +08:00
@oneisall8955 #2
我可能没说明白:
1. 我记得 TypeReference 的用法是 在 <> 里指明转换的类型
2. 你的 BaseResponse 里写的是 List<T> 但是你传入方法的是 CountryResponse.class ,我理解是 你的 T 是 CountryResponse ? 那你是要将 body 转成 BaseResponse<T> ?

所以我上面说没看懂你的意图 (另外 你的代码是 java 环境吧?)
oneisall8955
2023-08-18 16:03:46 +08:00
@sumarker #3 Sorry ,我写的有问题

- 普通类 CountryResponse 通过 json 数据反序列出:BaseResponse<CountryResponse> 这里我写错了,应该为 BaseResponse<List<CountryResponse>>

- 是 Java 环境

- TypeReference 确实可以在<>这里指明,例如
BaseResponse<List<T>> baseResponse = JSON.parseObject(body, new TypeReference<>(responseCls);
这里是个简化的写法,你应该也看明白。实际是 BaseResponse<List<T>> baseResponse = JSON.parseObject(body, new TypeReference<BaseResponse<List<T>>>(responseCls) {
});,被编辑器提示且自动简化了。

- TypeReference<>这里指明,那么
如果想反序列 Foo.class ,那就写一个方法 new TypeReference<BaseResponse<List<Foo>>>(){};
如果想反序列 Bar.class ,那就写一个方法 new TypeReference<BaseResponse<List<Bar>>>(){};
确实这样也可以,但是会重复大量类似代码。想提供一个传入 Class<T> clazz 参数的方法,该方法返回对应的 List<T>,就如同 doGet4List 的用途。很可惜,并不能 new TypeReference<BaseResponse<List<clazz>>>(){};这样是不符合语法的,那么,该如何正确的编写这样的方法呢?

- 让我疑惑的是,如果这种写法是错误的,为什么集群中只有一个实例会抛出异常,其他的实例都正常反序列化
yazinnnn
2023-08-18 16:17:04 +08:00
doGet4List 改成这样


public static <T> List<T> doGet4List(String body,TypeReference<BaseResponse<List<T>>> typeReference) {
BaseResponse<List<T>> resp = JSON.parseObject(body, typeReference);
return resp.data;
}


java 运行时无法获取到泛型的真实类型, 如果你用 kotlin 可以 inline + refied 实现你期望的效果


inline fun <reified T> doGet4List(body: String): List<T> {
val resp = JSON.parseObject(body, object : TypeReference<BaseResponse<List<T>>>() {})
return resp.data
}


fun main() {
val list = doGet4List<CountryResponse>("body")
}
4kingRAS
2023-08-18 16:20:51 +08:00
就别用这垃圾,jackson gson 不香吗
sumarker
2023-08-18 16:32:43 +08:00
@oneisall8955 #4

"被编辑器提示且自动简化了。"
-- 我用 JetBrains (#IU-232.8660.185 )没有发现这样的提示 想反 如果你没写 ,编译器是会报错的
TypeReference<>这里指明 的问题
-- 因为你在方法外已经指明方法的泛型 T ,那么,你在实际运行过程中已经将 T 那么 你直接用 上面写的
BaseResponse<List<T>> baseResponse = JSON.parseObject(body, new TypeReference<BaseResponse<List<T>>>(responseCls) {
});
就可以了。

至于你说的问题,我尝试着复现 了一下(只能大概复现出来),实际出错的部分确实是 转换的部分(如果你提供的代码经过处理的话) 出现的原因可能是 数据中的 data 为空 或者 null
oneisall8955
2023-08-18 17:47:03 +08:00
@sumarker #7



这不是 jdk 的语法糖吗?
cheng6563
2023-08-18 18:00:39 +08:00
这个 new TypeReference<>(responseCls)确实有问题,首先这个泛型应该是会被擦掉的。

但是你传了个指定的类型 responseCls 进去,fastjson 应该会使用 responseCls 进行反序列化。
但我稍微瞄了一眼源代码,new TypeReference<Foo>(){} 和 new TypeReference<>(Foo.class){}的行为不是一样的。

你的 doGet4List 方法应当提供 TypeReference 参数而不是提供 Class 参数。
TanKuku
2023-08-19 01:40:58 +08:00
不传方法参数,用泛型参数相当于声明一个固定泛型的 class ,这个是运行时可以获取的,传了参数就相当于泛型擦除掉了,只是一个泛型方法
ikas
2023-08-19 10:22:47 +08:00
应该是这里 TypeReference 的实现方式要求直接使用具体类型..使用传递不行..
如果想传递,需要自己实现内部获取 Type 的方式..通过反射推断具体类型还是比较复杂的..

--
如果是传递一个 class 类型的,使用 jackson,一般是使用其 JavaType 来构造类型,fastjson 从来不用,不知是否有这样的 api

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/966447

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX