问题描述
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)
重现步骤
- 定义一个带泛型字段的类(如 ResponseResult,字段 public T result;),以及一个含 long 字段的 POJO(如 PagerDataBean,private 字段 + getter)
- 先用 JSON.toJSONString() 序列化一个 ResponseResult 实例(模拟分页查询结果,污染全局 writer 缓存)
- 再对同一个 JVM 中的 ResponseResult 实例调用 JSON.toJSON()
- 抛出 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 内同一泛型类以不同泛型参数混用”的应用都可能触发,生产环境已在真实业务中遇到。
问题描述
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)
环境信息
重现步骤
期待的正确结果
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}
附加信息