Apache Johnzon 常用 API 与示例详解
详解 Apache Johnzon 2.2.0 三套常用 API——JSON-P(JsonObject/JsonReader/JsonParser/JsonPointer)、JSON-B(Jsonb)与 Mapper,每个分节给出可直接运行的示例代码与真实回显。
Apache Johnzon 常用 API 与示例详解
目标:系统梳理 Apache Johnzon 2.2.0 的三套常用 API——JSON-P 模型与流式 API(jakarta.json.)、JSON-B 标准绑定(jakarta.json.bind.)、Johnzon 自带 Mapper(org.apache.johnzon.mapper.*),每个分节给出可直接运行的示例代码与真实回显。已在 Ubuntu 22.04 + OpenJDK 17.0.2 + Johnzon 2.2.0 实测通过。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS,Kernel 6.8.0-138-generic,x86_64
- OpenJDK 17:17.0.2(build 17.0.2+8-86,
/opt/java/jdk-17.0.2) - Apache Maven:3.9.9(
/opt/maven) - Johnzon:2.2.0(johnzon-core / johnzon-mapper / johnzon-jsonb)
- Jakarta API:jakarta.json-api 2.1.3、jakarta.json.bind-api 3.0.1
- 示例工程目录:
/opt/johnzon-demo(依赖坐标见《Ubuntu 22.04 安装 Apache Johnzon 指南》4.4 节)
二、工具概览
| API 族 | 包 | 入口类 | 典型场景 |
|---|---|---|---|
| JSON-P 模型 API | jakarta.json | Json / JsonObject / JsonArray / JsonReader / JsonWriter | 一次性构建/解析中小 JSON 文档 |
| JSON-P 流式 API | jakarta.json.stream | JsonParser / JsonGenerator | 大文档逐事件处理,内存占用低 |
| JSON-P 指针/补丁 | jakarta.json | JsonPointer / JsonPatch | 结构化修改 JSON |
| JSON-B 绑定 | jakarta.json.bind | JsonbBuilder / Jsonb / JsonbConfig | POJO ↔ JSON 标准绑定(注解驱动) |
| Johnzon Mapper | org.apache.johnzon.mapper | MapperBuilder / Mapper | 无需注解的 POJO 映射(反射驱动) |
| Johnzon 注解 | org.apache.johnzon.mapper | @JohnzonProperty / @JohnzonIgnore / @JohnzonConverter | Mapper 细粒度控制 |
运行示例的通用方式(先编译,再按主类执行):
cd /opt/johnzon-demo
mvn -q compile
mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt
java -Xms256m -Xmx512m -Dfile.encoding=UTF-8 -cp target/classes:$(cat cp.txt) demo.JsonpDemo
三、前置条件
- 已引入 johnzon-core、johnzon-mapper、johnzon-jsonb 与 jakarta 两个 API 依赖(provided,必须显式声明)。
- 工程已编译:
cd /opt/johnzon-demo && mvn compile,依赖 classpath 文件cp.txt已生成。 - 本文示例均为
demo.*包下的可运行主类,输出为实测回显。
四、API 详解
4.1 JSON-P:构建 JsonObject 与美化输出
Json.createObjectBuilder() 链式构建,JsonWriterFactory 配置 PRETTY_PRINTING 美化输出:
JsonObject obj = Json.createObjectBuilder()
.add("id", 1001)
.add("name", "johnzon")
.add("price", 49.9)
.add("tags", Json.createArrayBuilder().add("json").add("parser").build())
.add("author", Json.createObjectBuilder().add("name", "Apache").add("site", "johnzon.apache.org"))
.build();
System.out.println("[build] compact = " + obj.toString());
JsonWriterFactory wf = Json.createWriterFactory(Map.of(JsonGenerator.PRETTY_PRINTING, true));
StringWriter sw = new StringWriter();
try (JsonWriter w = wf.createWriter(sw)) { w.writeObject(obj); }
System.out.println("[pretty]");
System.out.println(sw.toString());
真实回显:
[build] compact = {"id":1001,"name":"johnzon","price":49.9,"tags":["json","parser"],"author":{"name":"Apache","site":"johnzon.apache.org"}}
[pretty]
{
"id":1001,
"name":"johnzon",
"price":49.9,
"tags":[
"json",
"parser"
],
"author":{
"name":"Apache",
"site":"johnzon.apache.org"
}
}
4.2 JSON-P:JsonReader 解析与取值
String text = "{\"user\":\"root\",\"port\":22,\"roles\":[\"admin\",\"dev\"]}";
try (JsonReader r = Json.createReader(new StringReader(text))) {
JsonObject parsed = r.readObject();
System.out.println("[parse] user = " + parsed.getString("user"));
System.out.println("[parse] port = " + parsed.getInt("port"));
System.out.println("[parse] roles = " + parsed.getJsonArray("roles"));
System.out.println("[parse] size = " + parsed.size());
}
真实回显:
[parse] user = root
[parse] port = 22
[parse] roles = ["admin","dev"]
[parse] size = 3
4.3 JSON-P:JsonParser 流式解析
事件驱动的拉式解析器,逐事件推进(大文档首选):
try (jakarta.json.stream.JsonParser p = Json.createParser(new StringReader(text))) {
while (p.hasNext()) {
jakarta.json.stream.JsonParser.Event e = p.next();
switch (e) {
case KEY_NAME -> System.out.println(" KEY_NAME : " + p.getString());
case VALUE_STRING -> System.out.println(" VALUE_STRING: " + p.getString());
case VALUE_NUMBER -> System.out.println(" VALUE_NUMBER: " + p.getString());
default -> System.out.println(" " + e);
}
}
}
真实回显:
[stream]
START_OBJECT
KEY_NAME : user
VALUE_STRING: root
KEY_NAME : port
VALUE_NUMBER: 22
KEY_NAME : roles
START_ARRAY
VALUE_STRING: admin
VALUE_STRING: dev
END_ARRAY
END_OBJECT
4.4 JSON-P:JsonPointer 与 JsonPatch 修改
按 RFC 6901 指针路径读取,RFC 6902 补丁原地替换:
System.out.println("[pointer] before price = " + obj.getJsonNumber("price"));
JsonObject after = jakarta.json.Json.createPatchBuilder().replace("/price", jakarta.json.Json.createValue(59.9)).build().apply(obj);
System.out.println("[pointer] after price = " + after.getJsonNumber("price"));
真实回显:
[pointer] before price = 49.9
[pointer] after price = 59.9
4.5 JSON-B:POJO 序列化与反序列化
POJO(注解:@JsonbProperty 重命名、@JsonbNillable 保留 null、@JsonbDateFormat 日期格式、@JsonbPropertyOrder 排序):
public static class User {
public int id;
@JsonbProperty("username")
public String name;
public String role;
@JsonbNillable
public String comment;
@JsonbDateFormat("yyyy-MM-dd HH:mm:ss")
public LocalDateTime createdAt;
// 构造与 setter 略
}
Jsonb jsonb = JsonbBuilder.create();
String json = jsonb.toJson(new User(1, "root", "admin", null));
System.out.println("[toJson] " + json);
User back = jsonb.fromJson(json, User.class);
System.out.println("[fromJson] id=" + back.id + " name=" + back.name + " role=" + back.role + " comment=" + back.comment);
真实回显:
[toJson] {"id":1,"role":"admin","createdAt":"2026-10-11 10:30:00","comment":null,"username":"root"}
[fromJson] id=1 name=root role=admin comment=null
4.6 JSON-B:集合绑定与 Map 绑定
泛型集合通过匿名子类保留类型信息;Map 直接绑定:
Type listType = new ArrayList<String>() { }.getClass().getGenericSuperclass();
List<String> tags = jsonb.fromJson("[\"json\",\"bind\",\"johnzon\"]", listType);
System.out.println("[list] " + tags);
Map<String, Object> map = jsonb.fromJson("{\"host\":\"db1\",\"port\":3306}", Map.class);
System.out.println("[map] " + map);
真实回显:
[list] [json, bind, johnzon]
[map] {host=db1, port=3306}
4.7 Mapper:POJO 对象映射与读回
MapperBuilder 零注解构建映射器,writeObjectAsString / readObject 完成往返:
Mapper mapper = new MapperBuilder().build();
Server s = new Server(); // 字段:id=1, host=web1.example.com, port=22, user=root, password=q1w2e3r4
String json = mapper.writeObjectAsString(s);
System.out.println("[write] " + json);
Server back = mapper.readObject(json, Server.class);
System.out.println("[read] host=" + back.getHost() + " port=" + back.getPort() + " user=" + back.getUser());
真实回显:
[write] {"password":"q1w2e3r4","internal":"hidden","port":22,"host":"web1.example.com","id":1,"user":"root"}
[read] host=web1.example.com port=22 user=root
💡 说明:默认 AccessMode 按字段扫描,
transient修饰的internal字段也会输出;如需剔除请用@JohnzonIgnore注解或字段过滤策略。
4.8 Mapper:集合与泛型类型
数组序列化用 writeArrayAsString(Collection);反序列化用 readObject(String, Type) 配合 JohnzonParameterizedType:
List<Server> servers = Arrays.asList(new Server(), new Server());
String arrayJson = mapper.writeArrayAsString(servers);
System.out.println("[array] " + arrayJson);
List<Server> backList = mapper.readObject(arrayJson,
new JohnzonParameterizedType(List.class, Server.class));
System.out.println("[array-read] size=" + backList.size() + " first=" + backList.get(0).getHost());
Map<String, Object> m = mapper.readObject("{\"a\":1,\"b\":\"x\"}", Map.class);
System.out.println("[map] " + m);
真实回显:
[array] [{"password":"q1w2e3r4","internal":"hidden","port":22,"host":"web1.example.com","id":1,"user":"root"},{"password":"q1w2e3r4","internal":"hidden","port":22,"host":"web1.example.com","id":1,"user":"root"}]
[array-read] size=2 first=web1.example.com
[map] {a=1, b=x}
4.9 Mapper:注解配置(@JohnzonProperty / @JohnzonIgnore / @JohnzonConverter)
public static class Account {
@JohnzonProperty("login_name")
private String name = "root";
@JohnzonIgnore
private String password = "q1w2e3r4";
@JohnzonConverter(SecretConverter.class) // 自定义转换器:掩码输出
private String token = "raw-token-abc";
// getter/setter 略
}
Mapper mapper = new MapperBuilder().build();
System.out.println("[write] " + mapper.writeObjectAsString(new Account()));
Account back = mapper.readObject("{\"login_name\":\"admin\",\"token\":\"x\"}", Account.class);
System.out.println("[read] name=" + back.getName() + " password=" + back.getPassword() + " token=" + back.getToken());
真实回显:
[write] {"login_name":"root","token":"******"}
[read] name=admin password=q1w2e3r4 token=decoded
💡 说明:
@JohnzonIgnore的字段不参与序列化/反序列化,读回时保留字段默认值(q1w2e3r4);转换器在写入时调toString(掩码)、读取时调fromString(还原)。
4.10 SPI:Provider 自动发现
Johnzon 通过 META-INF/services 注册(johnzon-core 注册 jakarta.json.spi.JsonProvider → org.apache.johnzon.core.JsonProviderImpl;johnzon-jsonb 注册 jakarta.json.bind.spi.JsonbProvider → org.apache.johnzon.jsonb.JohnzonProvider)。classpath 加入即接管默认实现:
JsonProvider p = JsonProvider.provider();
System.out.println("[JsonProvider.provider] " + p.getClass().getName());
System.out.println("[Json.createObjectBuilder] " + Json.createObjectBuilder().getClass().getName());
JsonProvider explicit = new org.apache.johnzon.core.JsonProviderImpl();
System.out.println("[explicit] " + explicit.getClass().getName());
System.out.println("[JsonbProvider.provider] " + JsonbProvider.provider().getClass().getName());
Jsonb jsonb = JsonbBuilder.create();
System.out.println("[JsonbBuilder.create] " + jsonb.getClass().getName());
真实回显:
[JsonProvider.provider] org.apache.johnzon.core.JsonProviderImpl
[Json.createObjectBuilder] org.apache.johnzon.core.JsonObjectBuilderImpl
[explicit] org.apache.johnzon.core.JsonProviderImpl
[JsonbProvider.provider] org.apache.johnzon.jsonb.JohnzonProvider
[JsonbBuilder.create] org.apache.johnzon.jsonb.JohnzonJsonb
五、注意事项
- API 选型:无 POJO 的动态 JSON 用 JSON-P;标准 Java EE 栈(Jakarta EE 10+)优先 JSON-B;纯反射零注解映射用 Mapper。三套可共存于同一工程。
- 资源关闭:
JsonReader/JsonWriter/JsonParser/JsonGenerator均实现 AutoCloseable,务必 try-with-resources。 - Mapper 与 JSON-B 并存:JSON-B 注解(@JsonbProperty 等)对 Mapper 无效;反之 Johnzon 注解对 JSON-B 亦无效,同一 POJO 混用两套时按各自体系配置。
- 敏感字段:序列化前用
@JohnzonIgnore/@JohnzonConverter(掩码)处理账号密码类字段(示例账号 root/q1w2e3r4 仅作演示,生产请勿输出明文口令)。 - 堆内存:示例进程以
-Xms256m -Xmx512m运行;处理大 JSON 时用流式 API(4.3)并配合setMaxSize护栏。
六、常见问题
Q1:JsonProvider.provider() 返回的不是 Johnzon
classpath 中存在其他 JSON-P 实现(如 Glassfish parsson)。用 mvn dependency:tree 排查冲突并排除,或直接 new org.apache.johnzon.core.JsonProviderImpl() 显式实例化。
Q2:Mapper 读 List<POJO> 泛型丢失、返回 LinkedHashMap
直接传 List.class 会丢泛型。用 readObject(json, new JohnzonParameterizedType(List.class, Server.class))(见 4.8)。
Q3:JSON-B 日期序列化成 ISO 带时分秒的默认格式
在字段上加 @JsonbDateFormat("yyyy-MM-dd HH:mm:ss"),或全局 JsonbConfig 设 DATE_FORMAT 属性。
Q4:编译报 no suitable method found for readArray(String, ...)
Mapper.readArray 只接受 Reader/InputStream;字符串场景统一用 readObject(String, Type)(同 4.8 用法),该方法对数组和对象均适用。
Q5:运行中文输出乱码
启动参数加 -Dfile.encoding=UTF-8(或 JAVA_TOOL_OPTIONS 全局设置),Mapper 用 setEncoding("UTF-8")。
七、参考链接
- Apache Johnzon 官网(文档):https://johnzon.apache.org/
- Johnzon GitHub 源码:https://github.com/apache/johnzon
- Jakarta JSON Processing 2.1 API:https://jakarta.ee/specifications/jsonp/2.1/apidocs/
- Jakarta JSON Binding 3.0 API:https://jakarta.ee/specifications/jsonb/3.0/apidocs/
- Johnzon Maven 仓库:https://repo.maven.apache.org/maven2/org/apache/johnzon/