[BUG]2.0.64 + JDK8(aarch64):合法 {"flag":false} 循环 parseObject 报 syntax error : f;toJSONString 截断字符串
- Dominant language
- Java
- Stars
- 4.4k
- Forks
- 613
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 6
Description
### 问题描述
从 Fastjson2 2.0.52 升级到 2.0.64 后,在 Oracle JDK 8 + aarch64 上出现两类问题:
1. `JSON.parseObject("{\"flag\":false}", FlagBean.class)` 第一次成功,循环热起来(C2)后间歇抛 `JSONException: syntax error : f`(`JSONReaderUTF16.readBoolValue`)。2.0.64 读 `false` 走 `IOUtils.isALSE`(`UNSAFE.getLong`);加 `-Xint` 或退回 2.0.52 则 20 万次全部成功。
2. `JSON.toJSONString` 在 Bean + 嵌套 JSONObject + 中文 + 较长 `getId()` 下会丢掉字符串开头字符(例如 `PREFIX_LONG_CONSTANT_SUFFIX` 写成 `EFIX_LONG_CONSTANT_SUFFIX`)。这与 boolean 报错是同一平台上的另一条路径,下面分开给出例子。
与 https://github.com/alibaba/fastjson2/issues/7828 同类:aarch64 + JDK8 对 `char[]` 做 unaligned `Unsafe.getLong` / `putLong`。#7828 是 `writeInt3` 数字路径;这里是 boolean 读取(`isALSE`)和字符串写入(`JSONWriterUTF16JDK8UF`)。
### 环境信息
- OS信息: macOS 12.6.5 (21G531) arm64 Apple M1 8Core 16 GB
- JDK信息: Oracle JDK 1.8.0_381(Java(TM) SE Runtime Environment 1.8.0_381-b09,HotSpot 64-Bit Server VM 25.381-b09 mixed mode,os.arch=aarch64)
- 版本信息: Fastjson2 2.0.64(对照:2.0.52 正常)
### 重现步骤
**问题 1:`syntax error : f`**
1. 使用 `JSON.parseObject(String, Class)` 方法
2. 输入字面量 `{"flag":false}`(合法 JSON,不是残缺的 `f`),循环调用 200000 次
3. 第一次解析成功;C2 编译后出现 `JSONException: syntax error : f`
```java
import com.alibaba.fastjson2.JSON;
public class ReproSyntaxF {
public static class FlagBean {
public Boolean flag;
}
public static void main(String[] args) {
System.out.println("java=" + System.getProperty("java.version"));
System.out.println("os.arch=" + System.getProperty("os.arch"));
System.out.println("fastjson2=" + JSON.VERSION);
final String json = "{\"flag\":false}";
FlagBean first = JSON.parseObject(json, FlagBean.class);
System.out.println("firstParse flag=" + first.flag);
int syntaxF = 0;
int firstFailAt = -1;
for (int i = 0; i < 200000; i++) {
try {
JSON.parseObject(json, FlagBean.class);
} catch (Exception e) {
if (String.valueOf(e.getMessage()).contains("syntax error : f")) {
syntaxF++;
if (firstFailAt < 0) {
firstFailAt = i;
e.printStackTrace(System.out);
}
}
}
}
System.out.println("syntaxErrorF=" + syntaxF + "/200000");
System.out.println("firstFailAt=" + firstFailAt);
}
}
```
实测(同一台机器、循环 200000 次):
| 组合 | 结果 |
|------|------|
| 2.0.64 + C2(默认) | syntaxErrorF=759/200000,第一次失败在 i=747 |
| 2.0.64 + `-Xint` | 0/200000 |
| 2.0.52 + C2 | 0/200000 |
**问题 2:`toJSONString` 截断字符串(本例子没有 boolean,不会报 `syntax error : f`)**
1. 使用 `JSON.toJSONString` 方法
2. 输入带嵌套 `JSONObject`、中文字段、较长 `getId()` 的 Bean,循环 5000 次
3. 输出里 `id` 丢掉首字符,例如 `PREFIX_LONG_CONSTANT_SUFFIX` → `EFIX_LONG_CONSTANT_SUFFIX`
```java
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
public class Repro {
public static class Event {
public static final String EVENT_ID = "PREFIX_LONG_CONSTANT_SUFFIX";
private String eventId = "evt-1";
private String eventType = "demo.changed";
private String source = "demo";
private String traceId = "trace-1";
private JSONObject body;
private long timestamp = 1788162120689L;
public String getId() { return EVENT_ID; }
public String getEventId() { return eventId; }
public String getEventType() { return eventType; }
public String getSource() { return source; }
public String getTraceId() { return traceId; }
public JSONObject getBody() { return body; }
public long getTimestamp() { return timestamp; }
public void setBody(JSONObject body) { this.body = body; }
}
public static void main(String[] args) {
int corrupt = 0;
String last = null;
for (int i = 0; i < 5000; i++) {
Event event = new Event();
JSONObject body = new JSONObject();
body.put("createBy", "u1");
body.put("dutyUser", "u2");
body.put("name", "任务甲");
body.put("planCode", "P-01");
body.put("planEndDate", "2026-12-31 00:00:00");
body.put("planName", "计划名");
body.put("projectCode", "PRJ-01");
body.put("sceneUser", 0);
body.put("sceneAffiliation", "部门A");
body.put("title", "项目A");
event.setBody(body);
String json = JSON.toJSONString(event);
if (!json.contains("\"sceneAffiliation\"")
|| json.contains("\u0000")
|| !json.contains(Event.EVENT_ID)) {
corrupt++;
last = json;
}
}
System.out.println("java=" + System.getProperty("java.version"));
System.out.println("fastjson2=" + JSON.VERSION);
System.out.println("corrupt=" + corrupt + "/5000");
System.out.println(last);
}
}
```
实测:`corrupt=4357/5000`。单次、字段很少时经常正确;反复 `toJSONString`(命中 `JSONWriterUTF16` 线程本地 `char[]` 缓存)才稳定损坏。
### 期待的正确结果
- 对字面量 `{"flag":false}` 循环 `parseObject` 必须稳定成功,C2 编译后也不能间歇报 `syntax error : f`
- `toJSONString` 不得截断字段名和字符串;对自身输出再 `parseObject` 必须成功
- aarch64 + JDK8 上对 `char[]` 的 unaligned Unsafe 读写应回退到对齐访问或普通数组赋值
### 相关日志输出
问题 1:
```
java=1.8.0_381
os.arch=aarch64
fastjson2=2.0.64
firstParse flag=false
syntaxErrorF=759/200000
firstFailAt=747
com.alibaba.fastjson2.JSONException: syntax error : f
at com.alibaba.fastjson2.JSONReaderUTF16.readBoolValue(JSONReaderUTF16.java:5534)
at com.alibaba.fastjson2.JSONReader.readBool(JSONReader.java:3864)
at com.alibaba.fastjson2.reader.FieldReaderBool.readFieldValue(FieldReaderBool.java:43)
at com.alibaba.fastjson2.reader.ObjectReaderAdapter.readFieldValue(ObjectReaderAdapter.java:520)
at com.alibaba.fastjson2.JSON.parseObject(JSON.java:992)
```
问题 2:
```
java=1.8.0_381
fastjson2=2.0.64
corrupt=4357/5000
{"body":{"createBy":"u1","dutyUser":"u2","name":"任务甲","planCode":"P-01","planEndDate":"2026-12-31 00:00:00","planName":"计划名","projectCode":"PRJ-01","sceneUser":0,"sceneAffiliation":"部门A","title":"项目A"},"eventId":"evt-1","eventType":"demo.changed","id":"EFIX_LONG_CONSTANT_SUFFIX","source":"demo","timestamp":1788162120689,"traceId":"trace-1"}
```
#### 附加信息
相关代码:
- `JSONReaderUTF16.readBoolValue`(2.0.64 约 5469 / 5534 行):`ch == 'f'` 后调用 `IOUtils.isALSE`
- `IOUtils.isALSE(char[])` → `getLongUnaligned` → JDK8 上就是 `UNSAFE.getLong`
- JDK8 默认写路径 `JSONWriter.of()` → `JSONWriterUTF16JDK8UF.writeString` → `JSONWriterUTF16.writeString(char[])` 的 `getLongLE` / `putLongLE`
- `IOUtils.putLongUnaligned(char[])` 在 JDK8 上就是 `UNSAFE.putLong`(JDK9 才有真正的 unaligned API)
业务侧还见过字段名被截成 `ion`、字符串里出现 `\u0000`、`illegal fieldName` / `not allow unquoted fieldName`。那些是 writer 损坏的下游解析报错;问题 1 的最小复现输入始终是完整的 `{"flag":false}`。
Contributor guide
Research direction
Start by running the two supplied reproductions on Oracle JDK 8 aarch64, then inspect JSONReaderUTF16.readBoolValue, IOUtils.isALSE, JSONWriterUTF16JDK8UF.writeString, and the related getLongLE/putLongLE paths. Compare behavior with 2.0.52 and verify that repeated parsing stays error-free and repeated serialization preserves the full strings and produces JSON that can be parsed successfully.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100