| 构件(Artifact) | io.github.microsphere-projects:microsphere-java-core(0.3.20-SNAPSHOT,release 分支) |
| 包(Package) | io.microsphere.json —— 8 个主源码类型 |
| JDK 级别 | Java 8 字节码,继承自外部构建父 POM;CI 在 JDK 8 / 11 / 17 / 21 / 25 上验证 |
| 来源(Derived from) | 一个 org.json 的内部 fork(vendored fork),具体是 Android 的 org.json 实现 —— json/package-info.java:19 明确写着 "Fork the source code from org.json",且 JSON.java:2、JSONArray.java:2、JSONException.java:2、JSONObject.java:2、JSONStringer.java:2、JSONTokener.java:2 都带有 Copyright (C) 2010 The Android Open Source Project。JSONUtils.java:2-16 与 package-info.java:2-16 使用 Apache 许可证头,是 Microsphere 的原创代码。 |
依赖 core 中的其他包 |
io.microsphere.beans(BeanUtils.resolvePropertiesAsMap、BeanUtils.findWriteMethod、BeanUtils.getBeanMetadata、BeanMetadata)、io.microsphere.collection(ListUtils、MapUtils、EnumerationUtils、QueueUtils、SetUtils)、io.microsphere.util(ClassUtils、CharSequenceUtils、StringUtils、ExceptionUtils、IterableUtils、Utils)、io.microsphere.reflect(ConstructorUtils、MethodUtils、TypeUtils)、io.microsphere.convert(Converter.convertIfPossible)、io.microsphere.constants(SymbolConstants)、io.microsphere.lang.function(ThrowableAction)、io.microsphere.annotation.Nonnull / Nullable(属于独立构件) |
| 外部依赖 | 无 —— 纯 JDK。整个 reactor 中没有任何为这个包引入的 Jackson、Gson、org.json、jakarta.json 或 javax.json 依赖;microsphere-java-core/pom.xml:22-74 只列出了 microsphere-java-annotations、两个可选日志后端以及 test 作用域的构件。 |
| 被谁使用 | io.microsphere.metadata(core)、microsphere-lang-model、microsphere-annotation-processor。已在开发者页面的 §1 中核实。 |
本页面所有 file:line 引用都相对于 microsphere-java-core/src/main/java/io/microsphere/(主源码)
或 microsphere-java-core/src/test/java/io/microsphere/(测试),且读取自 release worktree。
首次提及之后会简写为 json/JSONUtils.java:40-55。
当你需要一个体积很小、零依赖的 JSON 对象模型,而所在库又无法承受引入第三方 JSON 栈的代价时,就应该使用
io.microsphere.json。 这个包是 Android org.json 的内部 fork(json/package-info.java:19),所以它的形状就是
经典的 JSONObject / JSONArray / JSONTokener / JSONStringer / JSONException 那一套,再加上一个原创工具类
JSONUtils,负责把 JSON 桥接到 Java 集合与 JavaBean。microsphere-java-core 中所有其他产生或读取 JSON 的代码
都使用这些类型 —— metadata 用 JSONUtils.append 写出配置属性文档,用 JSONUtils.readValues 读回来
(metadata/DefaultConfigurationPropertyGenerator.java:29-30、metadata/DefaultConfigurationPropertyReader.java:29,43)。
当你要解析的是一串值而不是单个文档时,请使用 JSONTokener —— 它同时是词法分析器和递归下降语法分析器,
nextValue() 会返回下一个出现的 JSON 类型(json/JSONTokener.java:102-122)。
当你希望 JSON 这一侧完全消失、只留下 Java 类型时,请使用 JSONUtils ——
readValue(json, Person.class)、readValues(json, List.class, Data.class)、readArray(json, String.class)、
readValueAsMap(jsonObject)、writeValueAsString(anyObject)、writeBeanAsString(bean)
(json/JSONUtils.java:884、:1041、:1172、:948、:1235、:1273)。
不要用它的场景:
- 你需要一个生产级的 JSON 栈。 这里没有流式解析器、没有
JsonPointer、没有 schema、没有BigDecimal精度保持、 没有可配置的Mapper,除了readValues(JSONArray, Type)(json/JSONUtils.java:1074)之外也没有基于类型令牌 (type-token)的泛型绑定 API。解析器就是 Android 那个,连同 Android 的全部宽松行为。 - 你需要严格符合 RFC 的读取器。
JSONTokener是故意接受注释、#注释、单引号、=/=>作为键值分隔符、;作为元素分隔符、0x/0整数前缀,以及表示null的悬空分隔符的 (json/JSONTokener.java:44-62)。解析成功并不能证明输入是合法 JSON。 - 你需要
null只有一种含义。 该模型携带两种互不兼容的 null —— Java 的null引用和JSONObject.NULL哨兵对象(json/JSONObject.java:101-114,以及json/JSONArray.java:49-52处的警告)。见 §10。 - 你需要线程安全。 包中没有任何类型可以共享;其中三个类明确说明了这一点
(
json/JSONTokener.java:64-68、json/JSONArray.java:71-74、json/JSONStringer.java:64-68),而JSONObject对此只字未提。 - 你想要的是
java.util.Map或java.util.List。JSONObject不实现任何接口(json/JSONObject.java:83),JSONArray同样不实现(json/JSONArray.java:76),两者也都不是Serializable。没有entrySet(),没有值上的Iterator,也没有List视图。请显式使用JSONUtils.readValueAsMap转换(json/JSONUtils.java:948-957)。
Important
这个包是本库对"静默降级"错误策略的、已被文档记录的例外。 在别处 microsphere-java 会返回中性值或干脆空操作;
而在这里,解析以及带类型的取值方法会抛出受检的 JSONException(json/JSONException.java:41-46)。
精确的分界线见 §3.3 —— 而四条读/写路径之间本身并不一致,这恰恰是关于这个包最需要注意的一件事。
<dependency>
<groupId>io.github.microsphere-projects</groupId>
<artifactId>microsphere-java-core</artifactId>
<version>0.3.20</version> <!-- 最新已发布的 tag;0.3.20-SNAPSHOT 是同一分支线上的开发版本 -->
</dependency>针对 JSON 没有任何需要额外添加的依赖。io.microsphere.json 就打包在 microsphere-java-core 里,
不会拖入任何传递性的 JSON 构件。
| 事实 | 值 | 证据(release) |
|---|---|---|
groupId |
io.github.microsphere-projects |
pom.xml:12 |
artifactId |
microsphere-java-core |
模块 pom.xml:14 |
| 版本属性 | ${revision},会被 flatten |
pom.xml:14、:15 |
release 上 ${revision} 的值 |
0.3.20-SNAPSHOT |
pom.xml:54 |
| 最新已发布 tag | 0.3.20 |
分组索引中的发布线表格(docs/current/user-guide/microsphere-java-core/README.md:70) |
| 构建父 POM | microsphere-build:0.3.16 |
pom.xml:6-10 |
| Java 级别 | reactor 中任何位置都没有 maven.compiler.* / <release> 属性;字节码版本继承自外部父 POM microsphere-build:0.3.16 |
CI 矩阵 java: [ '8', '11', '17', '21', '25' ],.github/workflows/maven-build.yml:25,distribution: 'temurin' :33 |
| Spring Framework,JDK 17+ | 7.0.9 |
microsphere-java-parent/pom.xml:23 |
Spring Framework,JDK [1.8,17) |
5.3.39,通过 profile java8-16,激活条件 <jdk>[1.8,17)</jdk> |
microsphere-java-parent/pom.xml:54-62,激活条件在 :57 |
| 导入的 BOM | microsphere-all-bom:0.3.13(:22、:33-36)、spring-framework-bom(:40-46) |
microsphere-java-parent/pom.xml |
原创类型的 @since |
1.0.0 |
json/package-info.java:22、json/JSONUtils.java:111 |
fork 来的类型的 @since |
不存在 —— JSONObject、JSONArray、JSONTokener、JSONStringer、JSONException、JSON 完全没有 @author/@since |
对本包的 grep:只有两个 @since 标签 |
| 源码中引用的语法规范 | RFC 4627 | json/JSONTokener.java:29 |
| 源码中引用的转义规范 | RFC 7159 | json/JSONUtils.java:121-129 |
整包在 CI 矩阵列出的 JDK(8、11、17、21、25)上都能编译和运行,不使用任何 JDK 内部 API,没有 sun.*,
没有 Unsafe,没有 JPMS module-info(整个仓库里都不存在)。包内没有任何代码读取系统属性或与 Spring 交互,
因此 java8-16 profile 在这里完全不相关。
API 表面就是 Android org.json 的表面,上游是稳定的;请把它当作只增不改来看待 ——
你的代码不应依赖包私有的辅助物(json/JSON.java:25 是包私有的,JSONStringer.Scope 在
json/JSONStringer.java:81-118 是包私有的嵌套枚举)。
这是必须记住的表格。"中性值"指 null、某个默认值,或原样返回输入。
| 路径 | 在格式错误 / 键缺失时的行为 | 证据 |
|---|---|---|
new JSONObject(String) / new JSONArray(String) / JSONTokener.nextValue() |
抛受检 JSONException |
json/JSONObject.java:178-180、json/JSONArray.java:140-142、json/JSONTokener.java:102-122;json/JSONObjectTest.java:105-107、json/JSONTokenerTest.java:111-118 |
JSONObject.get(name) / JSONArray.get(index) 以及每一个 getX |
抛受检 JSONException |
json/JSONObject.java:393-399、json/JSONArray.java:333-343;json/JSONObjectTest.java:218-220 |
每一个 optX(opt、optString、optInt、optJSONObject,……) |
永不抛异常 —— 返回 null、""、0、0L、false、NaN 或你传入的 fallback |
json/JSONObject.java:407-409、:436-452、:479-495、:521-537、:566-584、:609-625、:652-655、:682-685;json/JSONObjectTest.java:307-339 |
JSONObject.put(name, value) |
name 为 null 时抛 JSONException,NaN/无穷数时也抛;值为 null 时删除该键 |
json/JSONObject.java:274-285、:340-350;json/JSONObjectTest.java:174-176、:537-541 |
JSONArray.put(double) / put(int, Object) |
NaN/无穷数时抛 JSONException |
json/JSONArray.java:189-192、:299-310;json/JSONArrayTest.java:450-461 |
JSONArray.put(Object) |
完全没有检查 —— 源码自己承认了这一点 | json/JSONArray.java:225-228,在 json/JSONObject.java:323-324 处被承认 |
toString() / toString(int) |
抛出非受检的 RuntimeException,包裹着 JSONException |
json/JSONObject.java:762-766、json/JSONArray.java:713-717、lang/function/ThrowableAction.java:79-81 |
JSONUtils.jsonObject(String) / jsonArray(String) |
抛出非受检的 IllegalArgumentException(cause 为原来的 JSONException) |
json/JSONUtils.java:803-812、:843-852 |
JSONUtils.writeValueAsString(Object) |
当对象不可包装时返回 null |
json/JSONUtils.java:1234-1241;json/JSONUtilsTest.java:678-687 |
JSONObject.wrap(Object) |
任何失败都返回 null,异常被吞掉 |
json/JSONObject.java:867-885;json/JSONObjectTest.java:518-522 |
JSONUtils.escape(String) |
原样返回输入;null 进 → null 出 |
json/JSONUtils.java:1342-1386;json/JSONUtilsTest.java:760-762 |
Warning
一个包中并存着四套失败词汇:受检 JSONException(解析与 getX)、非受检 RuntimeException(toString)、
非受检 IllegalArgumentException(JSONUtils.jsonObject/jsonArray)、以及静默 null
(wrap、writeValueAsString、readValues 遇到不支持的目标类型)。如果你只捕获 JSONException,
后面三种会击穿你。§10 的第 5、8、9、11 条指出了具体的非受检逃逸口。
| 条目 | 值 | 依据 |
|---|---|---|
| 文档所在线 | release 分支,${revision} = 0.3.20-SNAPSHOT |
根 pom.xml:54 |
| 最新已发布版本 | 0.3.20,标签日期 2026-10-16 |
标签 0.3.20 的提交信息为 chore: merge main into release [skip ci],提交时间 2026-10-16T03:43:20Z |
| 上一版本 | 0.3.19,标签日期 2026-08-30 |
标签 0.3.19,提交时间 2026-08-30T12:51:37Z |
| 变更日志来源 | 仓库根目录的 release-notes.md |
本项目不发布 CHANGELOG.md;release-notes.md 的 v0.3.16–v0.3.20 段落写着 Release notes generation failed 并保留原始提交列表,因此下文一律以提交主题引用,并逐条与 release 源码核对。 |
0.3.20 在这一区域没有新增 API 面——0.3.19...0.3.20 区间是一次加固性发布。它实际改变了什么、以及这些改动触及本页源码的程度如下:
- 空值契约。 本页覆盖范围内 1 / 8 个主源码在签名上声明了
@Nonnull/@Nullable。没有单独点名本区域的提交,由全库性的34e27823 Refine nullability contracts across core APIs覆盖。 - 该区间没有其它改动触及本页覆盖的区域。
Note
如何读这份提交清单:JSONUtils 是唯一的带注解源码,且该区间没有以 io.microsphere.json 点名的提交;本次发布中的两处序列化清理属于 io.microsphere.io.serializer。就本条线而言 JSON 层行为未变。
| # | 类型 | 可见性 | 职责 | 是否 fork 自 AOSP? |
|---|---|---|---|---|
| 1 | JSONObject |
public |
保序的 name→value 映射;NULL 哨兵;wrap、quote、numberToString |
是(json/JSONObject.java:2) |
| 2 | JSONArray |
public |
紧凑的索引序列;与 JSONObject 相同的取值/强制转换规则 |
是(json/JSONArray.java:2) |
| 3 | JSONTokener |
public |
作用于单个 String 的词法器 + 递归下降解析器 |
是(json/JSONTokener.java:2) |
| 4 | JSONStringer |
public |
JSONObject/JSONArray 唯一的写出器;作用域栈 + 缩进 |
是(json/JSONStringer.java:2) |
| 5 | JSONException |
public |
本包唯一的受检异常 | 是(json/JSONException.java:2) |
| 6 | JSON |
package-private | 强制转换辅助(toBoolean/toDouble/toInteger/toLong/toString)、checkDouble、typeMismatch 工厂 |
是(json/JSON.java:2、:25) |
| 7 | JSONUtils |
public abstract … implements Utils |
Microsphere 原创的桥接层:StringBuilder JSON 写出器 + JSON↔集合/bean 读取器 |
否 —— ASF 许可证头,@since 1.0.0(json/JSONUtils.java:2、:109-113) |
| 8 | package-info.java |
— | 声明了这个 fork:"Fork the source code from org.json" | —(json/package-info.java:19-22) |
JSONObject object = new JSONObject("{\"name\":\"Mercy\",\"age\":18}");
assertTrue("Mercy".equals(object.getString("name"))); // JSONException 是受检异常 —— 必须声明或捕获json/JSONObjectTest.java:110-114 的压缩版本,该测试同时证明了宽松的 = 分隔符:
// json/JSONObjectTest.java:110-113
JSONObject jsonObject = new JSONObject("{ \"name\" : \"Mercy\" , \"age\" = 18 }");
assertEquals("Mercy", jsonObject.getString("name"));
assertEquals(18, jsonObject.getInt("age"));new JSONArray(String) 是其镜像(json/JSONArray.java:140-142),在 json/JSONArrayTest.java:98-104 得到证明:
// json/JSONArrayTest.java:99-103
JSONArray jsonArray = new JSONArray("[ 1 ,2 , 3]");
assertEquals(3, jsonArray.length());
assertEquals(1, jsonArray.getInt(0));两个构造函数都委托给 JSONTokener,然后窃取解析出来的内部 map/list
(json/JSONObject.java:158-169、json/JSONArray.java:120-131)。如果字符串中的第一个值不是正确的 JSON 类型,
你会从 JSON.typeMismatch 得到 JSONException(json/JSON.java:117-123)——
在 json/JSONArrayTest.java:92-95(把 {} 喂给 new JSONArray(tokener))与 json/JSONObjectTest.java:105-107
(把 Hello 喂给 new JSONObject(tokener))处有断言。
当输入包含多个值,或者你想用词法器自己的方法时,直接使用 tokener:
// json/JSONTokenerTest.java:59-65 —— 该资源是 microsphere-java-core/src/test/resources/test/json/data.json
JSONTokener jsonTokener = new JSONTokener(getResourceAsString("test/json/data.json"));
JSONObject jsonObject = (JSONObject) jsonTokener.nextValue();
assertEquals("Mercy", jsonObject.getString("name"));nextValue() 可能返回 JSONObject、JSONArray、String、Boolean、Integer、Long、Double、
JSONObject.NULL 或未加引号的 String —— 见 json/JSONTokener.java:95-101 处的 javadoc 与 :102-122 处的分派。
字符串可以用双引号或单引号,裸词也被接受为字面量:
// json/JSONTokenerTest.java:76-88
assertEquals("Hello", new JSONTokener("\"Hello\"").nextValue());
assertEquals("hello", new JSONTokener("'hello'").nextValue());
assertEquals("Hello", new JSONTokener("Hello").nextValue()); // 未加引号的字面量 → String前导的 UTF-8 BOM 会在构造函数中被剥离一次(json/JSONTokener.java:87-93);
json/JSONTokenerTest.java:43-46 构造了 new JSONTokener("\ufeffHello")。
readObject() 与 readArray() 是 public 的,且假定左括号已经被消费掉了 —— 这就是直接调用它们时看起来奇怪的
原因(json/JSONTokener.java:360-413、:415-461)。测试就是这样调用的:
// json/JSONTokenerTest.java:92-98 —— 注意:没有前导的 "{"
JSONObject empty = new JSONTokener("}").readObject(); // length 0
JSONObject one = new JSONTokener("\"name\" : \"Mercy\" }").readObject();
assertEquals("Mercy", one.getString("name"));如果你不想处理受检异常:
// json/JSONUtilsTest.java:508-512
JSONObject jsonObject = JSONUtils.jsonObject("{\"name\":\"John\", \"age\":30}");
assertEquals("John", jsonObject.getString("name"));
assertEquals(30, jsonObject.getInt("age"));
// json/JSONUtilsTest.java:521-526
JSONArray jsonArray = JSONUtils.jsonArray("[1,2,3]");
assertEquals(1, jsonArray.getInt(0));格式错误的输入会被重新包装为非受检的 IllegalArgumentException,其 cause 是原来的 JSONException
(json/JSONUtils.java:803-812、:843-852,经由 util/ExceptionUtils.java:97-104 与 :304-308):
// json/JSONUtilsTest.java:516-518 与 :529-531
assertThrows(IllegalArgumentException.class, () -> jsonObject("{name}"));
assertThrows(IllegalArgumentException.class, () -> jsonArray("[1,2,3"));put 返回 this,因此构造可以链式流畅书写。每个 put 重载也都声明 throws JSONException
(json/JSONObject.java:218-260),连 boolean/int/long 那几个也不例外,因为它们都要经过 checkName
(:345-350)。
// json/JSONObjectTest.java:123-136
long time = currentTimeMillis();
jsonObject.put("name", "Mercy")
.put("age", 18)
.put("workingTime", time)
.put("sex", true)
.put("salary", 1000.0)
.put("salary", null) // 删除该映射,见 §10 第 1 条
.put("salary", Double.valueOf(1000.0));
assertEquals("Mercy", jsonObject.get("name"));
assertEquals(1000.0, jsonObject.getDouble("salary"));
assertTrue(jsonObject.getBoolean("sex"));putOpt 是 null 容忍的变体 —— 任一参数为 null 时它静默空操作
(json/JSONObject.java:296-301,在 json/JSONObjectTest.java:140-145 得到证明)。
accumulate 让一个名字逐个累积成一个数组;同一个名字的第二个值会把第一个值提升进一个 JSONArray
(json/JSONObject.java:317-338):
// json/JSONObjectTest.java:155-170
jsonObject.accumulate("alias", "mercyblitz");
jsonObject.accumulate("alias", "mz");
assertEquals("mercyblitz", jsonObject.getJSONArray("alias").get(0));
assertEquals("mz", jsonObject.getJSONArray("alias").get(1));数组也支持按下标写入,中间的空洞用 null 填充(json/JSONArray.java:299-310):
// json/JSONArrayTest.java:143-148
jsonArray.put(0, true).put(1, 1).put(2, 2L).put(3, 3.0).put(4, "4");
assertEquals(3.0, jsonArray.get(3));remove 返回被移出的值或 null,且永不抛异常(json/JSONObject.java:359-361、json/JSONArray.java:366-371;
测试在 json/JSONObjectTest.java:179-184、json/JSONArrayTest.java:187-198)。isNull 与 has 是 Membership
探测的两个入口(json/JSONObject.java:370-373、:382-384;json/JSONArray.java:319-322;
测试 json/JSONObjectTest.java:187-203、json/JSONArrayTest.java:152-158)。
JSONObject 与 JSONArray 暴露完全相同的取值方法族;只有键类型不同(String 名字 vs int 下标)。
强制转换本身实现在包私有的 JSON 辅助类中。
| 取值方法 | 接受的输入 | 失败时 | JSONObject 行号 |
JSONArray 行号 |
|---|---|---|---|---|
get / opt |
任意对象 | get 抛 JSONException,opt 返回 null |
:393-399 / :407-409 |
:333-343 / :352-357 |
getBoolean / optBoolean |
Boolean,或字符串 "true"/"false"(忽略大小写) |
抛异常 / false 或 fallback |
:420-427 / :436-452 |
:382-389 / :398-414 |
getDouble / optDouble |
任意 Number,或可解析的 String |
抛异常 / NaN 或 fallback |
:463-470 / :479-495 |
:425-432 / :441-457 |
getInt / optInt |
任意 Number(会窄化),或可解析的 String |
抛异常 / 0 或 fallback |
:505-512 / :521-537 |
:468-475 / :484-500 |
getLong / optLong |
任意 Number(会窄化),或可解析的 String |
抛异常 / 0L 或 fallback |
:548-555 / :566-584 |
:511-518 / :527-543 |
getString / optString |
String,或任何东西 —— 非 null 的值都走 String.valueOf |
抛异常 / "" 或 fallback |
:593-600 / :609-625 |
:552-559 / :568-584 |
getJSONArray / optJSONArray |
只接受 JSONArray |
抛异常 / null |
:636-643 / :652-655 |
:595-602 / :611-614 |
getJSONObject / optJSONObject |
只接受 JSONObject |
抛异常 / null |
:666-673 / :682-685 |
:625-632 / :641-644 |
上面每一个格子都在测试套件中被断言过 —— 例如无 fallback 的 opt 形式在 json/JSONObjectTest.java:307-339,
显式 fallback 形式在 :544-577;数组侧在 json/JSONArrayTest.java:463-509。
数字不会保持你放进去时的类型。JSON.toDouble / toInteger / toLong 接受任意 Number 与任意可解析的
String(json/JSON.java:50-96),于是:
// json/JSONArrayTest.java:226-232、255-261、285-291
jsonArray.put(1.0); jsonArray.put("2.0"); jsonArray.put(3);
assertEquals(2.0, jsonArray.getDouble(1)); // String → double
assertEquals(2, jsonArray.getInt(1)); // String → double → int
assertEquals(3.0, jsonArray.getDouble(2)); // int → double
assertEquals(3L, jsonArray.getLong(2)); // double → long(有损;javadoc 在 json/JSONObject.java:540-544 承认了这点)而非数字字符串则直接失败:
// json/JSONObjectTest.java:229-233、242-246
jsonObject.put("value", "Hello");
assertThrows(JSONException.class, () -> jsonObject.getDouble("value"));
assertThrows(JSONException.class, () -> jsonObject.getInt("value"));getString 是唯一一个把所有东西都强制转换的取值方法,因为 JSON.toString 对任何非 String 都会落到
String.valueOf(value)(json/JSON.java:98-106):
// json/JSONTest.java:99-103
assertEquals("1", JSON.toString("1"));
assertEquals("1", JSON.toString(Integer.valueOf(1))); // number → 其文本
assertNull(JSON.toString(null));Note
美化输出的参数名是 indentSpaces,不是 indentFactor —— 见 json/JSONObject.java:758-762 与
json/JSONArray.java:706-713。它是每个嵌套层级所用的空格字符个数,没有制表符模式,也没有键排序选项。
// json/JSONObjectTest.java:419-420
assertEquals("{\"name\":\"Mercy\",\"age\":18,\"workingTime\":" + time + "}", jsonObject.toString());
assertEquals("{\n \"name\": \"Mercy\",\n \"age\": 18,\n \"workingTime\": " + time + "\n}",
jsonObject.toString(1));
// json/JSONArrayTest.java:414-415
assertEquals("[1,2,3]", jsonArray.toString());
assertEquals("[\n 1,\n 2,\n 3\n]", jsonArray.toString(1));注意 indentSpaces = 1 时的精确形状:每个成员前是一个换行加一个空格,键与值之间是 ": "
(json/JSONStringer.java:388-397、:451-453)。闭合括号永远不会被缩进。缩进字符串在每个实例上只构建一次
(json/JSONStringer.java:136-144),然后按栈深度重复拼接(:394-396)。
toString(0) —— 因而 toString() 也是 —— 是紧凑形式;toString(负数) 同样是紧凑形式,因为只要参数 <= 0,
indent 字段就保持 null(json/JSONStringer.java:137-143)。
JSONStringer 是 public 的,但基本上属于实现细节(json/JSONStringer.java:33-35)。它的值分派值得细读,
因为它定义了每个非标量值会变成什么:
// json/JSONStringerTest.java:136-161 —— 同样九个值,分别带缩进与不带缩进
"[null,true,null,1,2,3,4,\"5\",\"java.lang.String\"]"Float 4.0f 输出为 4,Double 2.0 输出为 2(json/JSONObject.java:816-819);StringBuilder 输出为带引号的
toString()(json/JSONStringer.java:282-284);Class 输出为其类型名(:280-281)。
其余三个 public 编码器:
// json/JSONObjectTest.java:437-442
assertEquals("0", numberToString(Double.valueOf(0.0))); // 整数值的 double → 不输出 ".0"
assertEquals("-0", numberToString(NEGATIVE_ZERO)); // json/JSONObject.java:811-814
assertEquals("1.23", numberToString(Double.valueOf(1.23)));
// json/JSONObjectTest.java:457-464
assertEquals("\"Mercy\"", quote("Mercy"));
assertEquals("\"\"", quote(null)); // json/JSONObject.java:832-834
// json/JSONArrayTest.java:402-407
jsonArray.put(1).put(2).put(3);
assertEquals("1,2,3", jsonArray.join(",")); // json/JSONArray.java:679-690quote 与 join 都走 JSONStringer 的 Scope.NULL 无括号模式
(json/JSONObject.java:835-841、json/JSONArray.java:680-689),因此它们输出裸值,不带 {}/[]。
JSONUtils 把 JSON 直接写进你提供的 StringBuilder,中间不经过任何对象模型。它有 34 个 append 重载 ——
每种基本类型、包装类型、String[]、基本类型数组、对象数组以及 Object 各一个
(json/JSONUtils.java:146-467)—— 外加 appendName(:500-508)与 appendValue(:469-486)。
这正是 metadata 使用的路径(metadata/DefaultConfigurationPropertyGenerator.java:88-117)。
// json/JSONUtilsTest.java:121-124、230-232、205-208
StringBuilder builder = new StringBuilder();
append(builder, "name", 1); // "name":1
append(builder, "name", "a"); // "name":"a" (经由 JSONUtils.escape 转义)
append(builder, "name", String.class); // "name":"java.lang.String"appendValue 按运行时类型分派:数组 → [...],Map → {...},Iterable → [...],
Character/CharSequence/Enum → 带引号,Type → 带引号的类型名,其余全部走 StringBuilder.append(value)
(json/JSONUtils.java:469-486,由 json/JSONUtilsTest.java:312-369 证明)。
Warning
JSONUtils.append 与 JSONObject.toString() 是两个不同的编码器,有不同的转义规则和不同的校验规则。
append 不做有限性检查,也不转义名字;toString() 两者都做。不要在同一个文档中混用它们还期待字节一致。
§10 的第 4、6、13 条给出了具体的差异点。
这就是 JSONUtils,也是 io.microsphere.json 不只是一个 fork 的原因。
// json/JSONUtilsTest.java:663-675
String json = writeValueAsString(ofMap("key1", "value1", "key2", 2));
assertEquals(new JSONObject(map).toString(), json);
Object array = ofArray("value1", "value2");
assertEquals(new JSONArray(array).toString(), writeValueAsString(array));writeValueAsString 委托给 JSONObject.wrap,当 wrap 的结果不是 JSON 容器时返回 null
(json/JSONUtils.java:1234-1241)—— 所以 writeValueAsString("Hello") 是 null,而不是 "\"Hello\""
(json/JSONUtilsTest.java:678-681)。
wrap 的阶梯就是整个转换契约(json/JSONObject.java:859-886),测试套件钉住了每一级:
// json/JSONObjectTest.java:467-515(节选)
assertEquals(NULL, wrap(null)); // :469
assertEquals(new JSONArray(ofList(values)), wrap(ofList(values))); // :479 Iterable
assertEquals(new JSONArray(ofArray(values)), wrap(ofArray(values))); // :481 array
assertEquals(new JSONArray(ofEnumeration(values)), wrap(ofEnumeration(values))); // :483
assertEquals(new JSONObject(ofMap("name", "Mercy")), wrap(ofMap("name", "Mercy"))); // :485
assertEquals("java.lang.String", wrap(String.class)); // :507
assertEquals("NEW", wrap(NEW)); // :509 enum → toString()
assertEquals(date.toString(), wrap(date)); // :512 任何 java.* → toString()
assertEquals(new JSONObject(), wrap(this)); // :514 JavaBean → 属性bean 会经过 BeanUtils.resolvePropertiesAsMap(json/JSONObject.java:881),writeBeanAsString 是这条路的直接
入口(json/JSONUtils.java:1272-1277,在 json/JSONUtilsTest.java:689-694 得到证明)。
// json/JSONUtilsTest.java:594-599(List)
List<Data> dataList = ofList(createData());
String json = writeValueAsString(dataList);
List<Data> copy = readValues(json, List.class, Data.class);
assertEquals(dataList, copy);readValues(String, Class, Class) 恰好支持六种目标形状,其他一律返回 null
(json/JSONUtils.java:1125-1141):数组(isArray)、List、Set、Queue、Enumeration、Iterable
(其余情况)。null 兜底在 json/JSONUtilsTest.java:639-645 被断言:
Object dataCopy = readValues(json, Object.class, Data.class); // 不支持 → null,不抛异常
assertNull(dataCopy);其他入口:
// json/JSONUtilsTest.java:647-654(从 JSON 字符串得到类型化数组)
Data[] dataArrayCopy = readArray(json, Data.class);
assertArrayEquals(dataArray, dataArrayCopy);
// json/JSONUtilsTest.java:534-548(JSONObject → Map,尽可能转换值)
Map<String, Object> readValue = readValue(json, Map.class);
assertEquals(data.getName(), readValue.get("name"));
assertArrayEquals(data.getNames(), (String[]) readValue.get("names"));readValue(json, Map.class) 之所以能走到 readValueAsMap,靠的是 isAssignableFrom(Map.class, targetType)
分支(json/JSONUtils.java:917-922、:948-957)。还有一个感知 Type 的重载 ——
readValues(JSONArray, Type)(json/JSONUtils.java:1073-1094)—— 当目标未被参数化时,它借助
determineElementClass(:1302-1322)从内容推断元素类型。
// json/JSONUtilsTest.java:551-557
Data data = createData();
String json = writeValueAsString(data);
Data readValue = readValue(json, Data.class); // Data 具备无参构造 + setter
assertEquals(data, readValue);以及多值 bean,这才是有意思的情形,因为每个属性会落进不同的集合类型:
// json/JSONUtilsTest.java:560-571
md.setStringList(ofList("a", "b", "c"));
md.setIntegerSet(ofSet(1, 2, 3));
md.setDataQueue(ofQueue(createData()));
md.setClassEnumeration(ofEnumeration(String.class, Integer.class));
md.setObjects(ofArray("Hello", 123, true, 45.67));
MultipleValueData readValue = readValueAsBean(jsonObject(writeValueAsString(md)), MultipleValueData.class);
assertEquals(md, readValue);其机制是:先用 ConstructorUtils.newInstance(true, beanClass) 实例化,然后为每个 JSON 键找到写方法,并把值
转换为目标属性的泛型参数类型(json/JSONUtils.java:988-1007)。任何匹配不上的东西都会被安静跳过 ——
见 json/JSONUtilsTest.java:573-581,那里一个值为 null 的键和一个无关的 "test-name" 键都消失了。
Tip
让 char 类型的 bean 属性经 JSON 往返会丢失其类型:Character 是包装类型,所以 wrap 原样返回它
(json/JSONObject.java:864),写出器给它加引号(json/JSONStringer.java:282-284),回来时它就变成了一个字符的
String。测试套件在 json/JSONUtilsTest.java:546 精确记录了这一点
(用 String.valueOf(data.getSex()) 与读回的值比较)。同样,在 Map 路径中 short/byte 会以 Integer 回来
(:544-545),float 会以 Double 回来(:542)。
-
有两种 null,它们不能互换。
JSONObject.NULL是一个哨兵,它的equals故意对null返回true(json/JSONObject.java:101-114,在json/JSONObjectTest.java:525-529得到断言),而 Java 的null引用并 不是合法的 map 值。后果是:put(name, null)会删除该键(json/JSONObject.java:275-278),所以new JSONObject().put("a", null)序列化为{},而new JSONObject("{\"a\":null}")序列化为{"a":null}, 并且has("a")为true(:382-384),但isNull("a")也为true(:370-373、json/JSONObjectTest.java:187-195)。JSONArray.get(index)遇到裸null元素会抛异常,遇到NULL则成功 (json/JSONArray.java:333-343,在:49-52有警告,在json/JSONArrayTest.java:167-170有断言)。 如果你想要一个统一覆盖两者的谓词,请使用JSONUtils.isNull(Object)(json/JSONUtils.java:697-699)。 -
getX对于它报告的是哪一种失败并不统一。getInt、getLong、getDouble、getJSONArray和getJSONObject全都从get(name)出发,后者对缺失键抛"No value for <name>"(json/JSONObject.java:393-399,被:463、:505、:548、:636、:666使用)。而getBoolean与getString从opt(name)出发(:421、:594),所以同一个缺失键会产生来自JSON.typeMismatch的"Value at <name> is null."(json/JSON.java:108-115)。如果你靠匹配消息来分支,你会漏掉一半情形。 -
JSONObject.getString报告了错误的目标类型。 在json/JSONObject.java:597它调用的是typeMismatch(name, object, "long")—— 一个复制粘贴缺陷;它的孪生方法json/JSONArray.java:556正确传入了"String"。抛出的异常类型是对的,只是消息说成"cannot be converted to long"。 -
NaN/Infinity在一条写入路径上被拒绝,在另一条上被原样写出。 模型路径拒绝它们:JSON.checkDouble(json/JSON.java:27-32)会被put(String,double)(json/JSONObject.java:232-234)、put(String,Object)中对任意Number的检查(:279-283)、accumulate(:324-326)、JSONArray.put(double)(json/JSONArray.java:189-192)以及put(int,Object)(:299-304)调用;测试在json/JSONObjectTest.java:537-541与json/JSONArrayTest.java:450-461。JSONUtils.append完全不做这些事 ——append(builder, name, 1.0)直接走到StringBuilder.append(double)(json/JSONUtils.java:176-179), 而append(builder, name, (Object) Double.NaN)落到appendValue的else分支(:483-485)。 两者都会产出NaN/Infinity,这不是合法 JSON,且不抛任何异常。更糟的是,JSON.toDouble因为调用Double.valueOf(String),会欣然解析"NaN"、"Infinity"和"-Infinity"(json/JSON.java:50-64)。 -
JSONArray.put(Object)没有任何防护,而失败会在之后以非受检异常的形式浮出。json/JSONArray.java:225-228这个重载不做任何Number检查;json/JSONObject.java:323-324甚至写了原因 ("check in accumulate, since array.put(Object) doesn't do any checking")。所以new JSONArray().put((Object) NaN)会成功,而之后的toString()会抛出包裹着JSONException的RuntimeException(json/JSONArray.java:713-717→lang/function/ThrowableAction.java:79-81)。 没有任何测试覆盖这个形状 ——json/JSONArrayTest.java:450-461只测了带检查的put(double)与put(int,double)重载。 -
两个编码器,两套转义规则。
JSONStringer.string()转义"、\、/、六个具名转义以及所有<= 0x1F(json/JSONStringer.java:338-386;json/JSONStringerTest.java:188-194钉住了"\"\\\//\t\b\n\r\fa"→"\"\\\\\\/\\t\\b\\n\\r\\fa"以及 U+001F →\u001f)。JSONUtils.escape()转义"、\和控制字符区间,不转义/,但会转义U+2028/U+2029(json/JSONUtils.java:115-144、:1342-1386;json/JSONUtilsTest.java:806-815)。因此JSONObject.toString()产出\/的地方JSONUtils.append产出/,反之 U+2028 处亦然。 -
JSONUtils从不转义属性名,也从不转义char值。appendName写的是'"' + name + '"' + ':',毫无转义(json/JSONUtils.java:500-508),而append(builder, name, 'a')/append(builder, name, Character.valueOf('a'))会把字符原样写出(:181-186、:223-228)。含"的名字或取值为"的char会静默地产出畸形 JSON。测试只使用了无害的名字 (json/JSONUtilsTest.java:372-384、:144-148、:192-196)。 -
畸形的
\uXXXX转义抛的是NumberFormatException,不是JSONException。readEscapeCharacter用Integer.parseInt(hex, 16)解析四个十六进制字符且不捕获任何东西 (json/JSONTokener.java:254-260);javadoc 在:200与:248承认了这一点。因此"\uZZZZ"会从catch (JSONException)逃逸出去。测试套件只覆盖了被截断的变体"u123",它命中的是长度检查, 确实抛JSONException(json/JSONTokenerTest.java:223-227)。 -
两侧都没有嵌套深度限制,也没有环检测。 解析器递归
nextValue()→readObject()/readArray()→nextValue()(json/JSONTokener.java:102-122、:367-413、:423-461),没有计数器;写出器递归JSONStringer.value(Object)→JSONObject.writeTo(stringer)→stringer.value(...)(json/JSONStringer.java:266-271、json/JSONObject.java:768-774)。因此深层嵌套的输入会以StackOverflowError告终,那是一个catch (JSONException | RuntimeException)拦不住的Error。JSONStringer的类 javadoc 仍然声称"at most 20 levels of nesting"(json/JSONStringer.java:61-62), 而紧接其下的字段注释说的恰恰相反 —— "Unlike the original implementation, this stack isn't limited to 20 levels of nesting"(:120-123)。环也可以手工构造出来 (json/JSONArrayTest.java:338-341把一个数组放进了它自己),对这样的数组调用toString()就会栈溢出。 -
数字永远不会变成
BigDecimal或BigInteger。readLiteral先试Integer,再试Long,再试Double, 最后退化为未加引号的String(json/JSONTokener.java:305-339);整包对BigDecimal/BigInteger的引用为 零。21474836470会解析为Long(json/JSONTokenerTest.java:241),但任何超过Long.MAX_VALUE的值都会 静默变成Double并丢失精度。JSON.toInteger与toLong用 C 风格的强制转换窄化(json/JSON.java:75、:91),所以getInt("999999999999")返回Integer.MAX_VALUE而不是报错 ——JSONTest.java:84已经展示了"1.0"变成1。 -
jsonObject(null)/jsonArray(null)抛的是NullPointerException,而不是文档承诺的IllegalArgumentException。new JSONTokener(null)接受这个null并存下来 (json/JSONTokener.java:87-93,javadoc 在:84-85承认了这点);第一次读取会在:501-503解引用它。 由于JSONUtils.jsonObject只捕获JSONException(json/JSONUtils.java:806-810),NPE 会不被包装地逃逸出去,:801处的@throws IllegalArgumentException契约没有被满足。没有针对null输入的测试 ——json/JSONTokenerTest.java:49-51用null构造了 tokener 却从不从中读取。 -
wrap与readValues会把失败吞成null。JSONObject.wrap把整个分派包在catch (Exception ignored) { }里(json/JSONObject.java:867-885),所以键为null的 map、getter 抛异常的 bean、或者自毁equals的值,最后都会变成null—— 在json/JSONObjectTest.java:518-522有断言, 然后又被JSONUtils.writeValueAsString序列化掉(json/JSONUtils.java:1235-1241、json/JSONUtilsTest.java:678-687)。readValues同样对不支持的目标类型返回null(:1125-1141、json/JSONUtilsTest.java:639-645)。两者都不算 bug,但它们都意味着一个null结果并不能告诉你为什么。 -
JSONUtils的 null 处理与模型的恰好相反。appendValue的第一条语句就是value.getClass()(json/JSONUtils.java:469-470),所以append(builder, "k", (Object) null)抛NullPointerException—— 任何包含一个null元素的Map或Iterable也一样,经由appendMap/appendIterable(:1388-1415)。determineElementClass同理,它会解引用每一个元素(:1303-1310);readValueAsMap/readValueAsBean也一样, 它们在任何 null 检查之前就调用jsonObject.length()/getBeanMetadata(...)(:948-957、:988-990)。json/JSONUtilsTest.java:319-334中的测试对 map 这一情形没有做任何断言 —— 那个测试根本没有断言。
- 配置属性元数据 ——
metadata是库内最大的消费者:DefaultConfigurationPropertyGenerator用JSONUtils.append写 JSON (metadata/DefaultConfigurationPropertyGenerator.java:29-30,88-117),ReflectiveConfigurationPropertyGenerator用writeBeanAsString(metadata/ReflectiveConfigurationPropertyGenerator.java:24,78),DefaultConfigurationPropertyReader用readValues(content, List.class, Map.class)读 (metadata/DefaultConfigurationPropertyReader.java:29,43)。 - 反射、类型与 JavaBean ——
BeanUtils.resolvePropertiesAsMap让JSONObject.wrap能够处理 JavaBean(json/JSONObject.java:25,881),而ConstructorUtils/MethodUtils/TypeUtils驱动着readValueAsBean(json/JSONUtils.java:988-1007)。 - 类型转换 ——
Converter.convertIfPossible是每一次 JSON→Java 标量转换的叶子节点 (json/JSONUtils.java:63,1427-1442)。 - 集合与过滤器 ——
ListUtils.newArrayList与MapUtils.newLinkedHashMap决定了JSONArray的存储方式与JSONObject的键顺序(json/JSONArray.java:84、json/JSONObject.java:122)。 - 工具类与常量 ——
ClassUtils提供wrap用到的所有类型谓词 (json/JSONObject.java:39-45),SymbolConstants提供JSONUtils用到的标点(json/JSONUtils.java:56-62)。 - 序列化 —— 相邻的
io.microsphere.io.serializer包,处理二进制的Object↔byte[]; 当你不想要文本格式时它是正确的选择。 - 开发者指南 — JSON —— tokener 状态机、
JSONObject的 map 选型、写出器的作用域栈、异常分类学、设计模式清单、包依赖图与评审意见。