alibaba / alibaba/fastjson2

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

Open
#7,853 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Java
Stars
4.4k
Forks
613
Avg merge
1d 22h
Merged PRs (30d)
6

Description

### 问题描述
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 {
private List rows;
private long page, size, totalPage, total;
private PagerBean pager;
public PagerDataBean() {}
public List getRows() { return rows; }
public void setRows(List 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 {
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 pagerData = new PagerDataBean<>();
pagerData.setRows(Collections.singletonList("x"));
pagerData.setTotal(1L);
ResponseResult> pagerResult = new ResponseResult<>(true, 0, "ok", pagerData);
System.out.println("step1 toJSONString: " + JSON.toJSONString(pagerResult));

// Step 2: 再序列化 ResponseResult -> ClassCastException
ResponseResult 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 内同一泛型类以不同泛型参数混用”的应用都可能触发,生产环境已在真实业务中遇到。

Contributor guide

Open the contributing guide

Research direction

Start with ObjectWriterAdapter.toJSONObject and compare its getInitWriter() path with FieldWriterObject.getObjectWriter's type check, then inspect ObjectWriterProvider.cache behavior. Run the supplied ReproMin sequence and add a regression test covering different generic values. Done means JSON.toJSON returns the expected Boolean result without ClassCastException and remains consistent with JSON.toJSONString.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.