Skip to content

[BUG] JSON.toJSON() 序列化同一泛型类不同参数时复用被污染的 ObjectWriter 缓存,抛 ClassCastException(Boolean cannot be cast to X) #7853

Description

@yuwensg

问题描述

JSON.toJSON(Object) 序列化同一个泛型类(如 ResponseResult)时,如果该 JVM 中先前已经用不同的泛型参数(如 ResponseResult)序列化过该类的实例,会抛出 ClassCastException。原因是 ObjectWriterProvider 中按原始 Class 缓存的 ObjectWriter 的泛型字段(fieldType 为 TypeVariable)的 initObjectWriter 被首次序列化时实际的值类型惰性绑定并全局共享,后续 JSON.toJSON 复用该缓存 writer 时不校验当前值的实际类型,直接把新值按旧类型序列化。
线上表现为:日志中 JSON.toJSON(ResponseResult) 抛出:
java.lang.ClassCastException: class java.lang.Boolean cannot be cast to class com.wt.wbp.bean.PagerDataBean
at com.alibaba.fastjson2.introspect.PropertyAccessorFactory$FunctionAccessorLongValue.getLongValue(PropertyAccessorFactory.java:3869)
at com.alibaba.fastjson2.introspect.PropertyAccessorFactory$PropertyAccessorLongValue.getObject(PropertyAccessorFactory.java:713)
at com.alibaba.fastjson2.writer.FieldWriterInt64Value.getFieldValue(FieldWriterInt64Value.java:37)
at com.alibaba.fastjson2.writer.ObjectWriterAdapter.toJSONObject(ObjectWriterAdapter.java:618)
at com.alibaba.fastjson2.writer.ObjectWriterAdapter.toJSONObject(ObjectWriterAdapter.java:610)
at com.alibaba.fastjson2.writer.ObjectWriterAdapter.toJSONObject(ObjectWriterAdapter.java:691)
at com.alibaba.fastjson2.JSON.toJSON(JSON.java:4084)
at com.alibaba.fastjson2.JSON.toJSON(JSON.java:4056)

环境信息

  • OS 信息:Linux(CentOS 8.x,4Core 16GB)— 与 OS 无关,Windows/JDK21 下同样可复现
  • JDK 信息:OpenJDK 21.0.11(Temurin);JDK 17 下同样可复现
  • 版本信息:Fastjson2 2.0.64(fastjson2-2.0.64.jar,唯一依赖,无 fastjson2-extension)

重现步骤

  1. 定义一个带泛型字段的类(如 ResponseResult,字段 public T result;),以及一个含 long 字段的 POJO(如 PagerDataBean,private 字段 + getter)
  2. 先用 JSON.toJSONString() 序列化一个 ResponseResult 实例(模拟分页查询结果,污染全局 writer 缓存)
  3. 再对同一个 JVM 中的 ResponseResult 实例调用 JSON.toJSON()
  4. 抛出 ClassCastException: Boolean cannot be cast to PagerDataBean
import com.alibaba.fastjson2.JSON;
import java.util.Collections;
import java.util.List;

public class ReproMin {
    public static class PagerBean { public String name; }

    public static class PagerDataBean<T> {
        private List<T> rows;
        private long page, size, totalPage, total;
        private PagerBean pager;
        public PagerDataBean() {}
        public List<T> getRows() { return rows; }
        public void setRows(List<T> rows) { this.rows = rows; }
        public long getPage() { return page; }
        public void setPage(long page) { this.page = page; }
        public long getSize() { return size; }
        public void setSize(long size) { this.size = size; }
        public long getTotalPage() { return totalPage; }
        public void setTotalPage(long totalPage) { this.totalPage = totalPage; }
        public long getTotal() { return total; }
        public void setTotal(long total) { this.total = total; }
        public PagerBean getPager() { return pager; }
        public void setPager(PagerBean pager) { this.pager = pager; }
    }

    public static class ResponseResult<T> {
        public int code;
        public boolean success;
        public String msg;
        public T result;
        public ResponseResult() {}
        public ResponseResult(boolean success, int code, String msg, T result) {
            this.success = success; this.code = code; this.msg = msg; this.result = result;
        }
        public void setCode(int code) { this.code = code; }
        public void setSuccess(boolean success) { this.success = success; }
        public void setMsg(String msg) { this.msg = msg; }
        public void setResult(T result) { this.result = result; }
        public int getCode() { return code; }
        public boolean isSuccess() { return success; }
        public String getMsg() { return msg; }
        public T getResult() { return result; }
    }

    public static void main(String[] args) {
        // Step 1: 先序列化 ResponseResult<PagerDataBean>(如分页查询结果)
        PagerDataBean<String> pagerData = new PagerDataBean<>();
        pagerData.setRows(Collections.singletonList("x"));
        pagerData.setTotal(1L);
        ResponseResult<PagerDataBean<String>> pagerResult = new ResponseResult<>(true, 0, "ok", pagerData);
        System.out.println("step1 toJSONString: " + JSON.toJSONString(pagerResult));

        // Step 2: 再序列化 ResponseResult<Boolean> -> ClassCastException
        ResponseResult<Boolean> boolResult = new ResponseResult<>(true, 0, "ok", Boolean.TRUE);
        try {
            Object json = JSON.toJSON(boolResult);
            System.out.println("step2 OK: " + json);
        } catch (Throwable t) {
            System.out.println("step2 EXCEPTION: " + t);
            for (int i = 0; i < Math.min(8, t.getStackTrace().length); i++) {
                System.out.println("    at " + t.getStackTrace()[i]);
            }
        }
        // step2 若改用 JSON.toJSONString 则正常(write 路径会校验值类型)
        System.out.println("step2b toJSONString: " + JSON.toJSONString(boolResult));
    }
}

期待的正确结果

JSON.toJSON(ResponseResult) 应当输出 {"code":0,"msg":"ok","result":true,"success":true},与 JSON.toJSONString 一致;不应当因之前序列化过同一泛型类的其他参数实例而抛出 ClassCastException。即缓存复用时必须校验 initValueClass 与当前值的实际类型是否匹配(FieldWriterObject.getObjectWriter 的 typeMatch 校验应当同样作用于 ObjectWriterAdapter.toJSONObject 的 getInitWriter() 路径)。

相关日志输出

step1 toJSONString: {"code":0,"msg":"ok","result":{"page":0,"rows":["x"],"size":0,"total":1,"totalPage":0},"success":true}
step2 EXCEPTION: java.lang.ClassCastException: class java.lang.Boolean cannot be cast to class ReproMin$PagerDataBean (java.lang.Boolean is in module java.base of loader 'bootstrap'; ReproMin$PagerDataBean is in unnamed module of loader 'app')
at com.alibaba.fastjson2.introspect.PropertyAccessorFactory$FunctionAccessorLongValue.getLongValue(PropertyAccessorFactory.java:3869)
at com.alibaba.fastjson2.introspect.PropertyAccessorFactory$PropertyAccessorLongValue.getObject(PropertyAccessorFactory.java:713)
at com.alibaba.fastjson2.writer.FieldWriterInt64Value.getFieldValue(FieldWriterInt64Value.java:37)
at com.alibaba.fastjson2.writer.ObjectWriterAdapter.toJSONObject(ObjectWriterAdapter.java:618)
at com.alibaba.fastjson2.writer.ObjectWriterAdapter.toJSONObject(ObjectWriterAdapter.java:610)
at com.alibaba.fastjson2.writer.ObjectWriterAdapter.toJSONObject(ObjectWriterAdapter.java:691)
at com.alibaba.fastjson2.JSON.toJSON(JSON.java:4084)
at com.alibaba.fastjson2.JSON.toJSON(JSON.java:4056)
step2b toJSONString: {"code":0,"msg":"ok","result":true,"success":true}

附加信息

  • 根因分析:ObjectWriterProvider.cache 以原始 Class 为 key 缓存 ASM 生成的 ObjectWriter。ResponseResult 的 result 字段 fieldType=T(TypeVariable)且 initObjectWriter 初始为 null。首次序列化 ResponseResult 时,FieldWriterObject.getObjectWriterVoid() 将该字段的 initObjectWriter 就地改为 PagerDataBean 的 writer(全局共享)。之后 ObjectWriterAdapter.toJSONObject(offset 513)直接 getInitWriter() 拿到该 writer,不校验值类型,于是把 Boolean 按 PagerDataBean 的字段迭代(FieldWriterInt64Value.getFieldValue → ((PagerDataBean) Boolean).getPage())→ CCE。而 JSON.toJSONString 走 FieldWriterObject.write 路径,会校验 initValueClass 与实际值类型(getObjectWriter 的 typeMatch),因此不崩溃。
  • 通过反射验证:崩溃前 result 字段的 initWriter=null,序列化一次 ResponseResult 后变为 initWriter=com...PagerDataBean。
  • 临时规避:provider.cleanup(ResponseResult.class) 清缓存后 JSON.toJSON 正常;或改用 JSON.toJSONString / 显式 TypeReference。
  • 影响面:任何“同一 JVM 内同一泛型类以不同泛型参数混用”的应用都可能触发,生产环境已在真实业务中遇到。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions