V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
oneisall8955
V2EX  ›  Java

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

  •  
  •   oneisall8955 · 2023-08-18 15:02:45 +08:00 · 1903 次点击
    这是一个创建于 466 天前的主题,其中的信息可能已经有所发展或是发生改变。

    版本:fastjson 1.2.76

    环境:springboot 2.3.6

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

    简化业务逻辑代码:

    • 泛型类 BaseResponse<T> 装载通用的接口响应,其属性 private T data是不同类型的数据;
    • 普通类 CountryResponse 通过 json 数据反序列出:BaseResponse<CountryResponse>
    • 工具类 HttpRequestUtil,有个 static 方法将接口的 response body 反序列出 BaseResponse<T>
        @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......

    第 1 条附言  ·  2023-08-18 16:10:01 +08:00

    描述有误,应改为:

    简化业务逻辑代码:

    • 泛型类 BaseResponse<T> 装载通用的接口响应,其属性 private T data是不同类型的数据,可能是Foo,也可能是List<Bar>;
    • 普通类 CountryResponse 通过 json 数据反序列出:BaseResponse<List<CountryResponse>>
    • 工具类 HttpRequestUtil,有个 static doGet4List 方法将接口的 response body 反序列出 BaseResponse<List<T>>,并且返回 List<T>
    11 条回复    2023-08-19 10:22:47 +08:00
    sumarker
        1
    sumarker  
       2023-08-18 15:11:11 +08:00
    我没太懂 BaseResponse<List<T>> 这个里不是一个 list 吗? responseCls 不是 object.class 吗?
    oneisall8955
        2
    oneisall8955  
    OP
       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
        3
    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
        4
    oneisall8955  
    OP
       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
        5
    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
        6
    4kingRAS  
       2023-08-18 16:20:51 +08:00
    就别用这垃圾,jackson gson 不香吗
    sumarker
        7
    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
        8
    oneisall8955  
    OP
       2023-08-18 17:47:03 +08:00
    @sumarker #7



    这不是 jdk 的语法糖吗?
    cheng6563
        9
    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
        10
    TanKuku  
       2023-08-19 01:40:58 +08:00 via Android
    不传方法参数,用泛型参数相当于声明一个固定泛型的 class ,这个是运行时可以获取的,传了参数就相当于泛型擦除掉了,只是一个泛型方法
    ikas
        11
    ikas  
       2023-08-19 10:22:47 +08:00
    应该是这里 TypeReference 的实现方式要求直接使用具体类型..使用传递不行..
    如果想传递,需要自己实现内部获取 Type 的方式..通过反射推断具体类型还是比较复杂的..

    --
    如果是传递一个 class 类型的,使用 jackson,一般是使用其 JavaType 来构造类型,fastjson 从来不用,不知是否有这样的 api
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3592 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 04:41 · PVG 12:41 · LAX 20:41 · JFK 23:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.