1. 簡介
將完整的對象數據結構序列化為 JSON,採用所有字段的精確一對一表示形式,在某些情況下可能不合適,或者可能不是我們想要的結果。相反,我們可能想要創建一個擴展或簡化的數據視圖。 這正是自定義 Jackson 序列化器發揮作用的地方。
但是,實施自定義序列化器可能很繁瑣,尤其是在我們的模型對象具有大量字段、集合或嵌套對象時。 幸運的是,Jackson 庫提供了幾個可以簡化此任務的機制。
在本簡短教程中,我們將探討自定義 Jackson 序列化器,並演示如何在自定義序列化器中訪問默認序列化器。
2. 示例數據模型
在深入探討 Jackson 的自定義之前,讓我們先查看一下我們想要序列化的示例 文件夾 類:
public class Folder {
private Long id;
private String name;
private String owner;
private Date created;
private Date modified;
private Date lastAccess;
private List<File> files = new ArrayList<>();
// standard getters and setters
}
並且有 File 類,它被定義為在我們的 Folder 類中的一個 List:
public class File {
private Long id;
private String name;
// standard getters and setters
}
3. 自定義序列化器在 Jackson 中的應用
使用自定義序列化的主要優勢在於,我們無需修改我們的類結構。此外,我們能夠輕鬆地將我們期望的行為與類本身解耦。
因此,讓我們假設我們想要一個簡化的 Folder 類視圖:
{
"name": "Root Folder",
"files": [
{"id": 1, "name": "File 1"},
{"id": 2, "name": "File 2"}
]
}
在接下來的章節中,您將會看到,我們可以通過多種方式在 Jackson 中實現我們期望的輸出。
3.1. 暴力枚舉方法
首先,在不使用 Jackson 的默認序列化器的情況下,我們可以創建一個自定義序列化器,其中我們自己完成所有繁重的工作。
讓我們為我們的 Folder 類創建一個自定義序列化器來實現這一點:
public class FolderJsonSerializer extends StdSerializer<Folder> {
public FolderJsonSerializer() {
super(Folder.class);
}
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
gen.writeArrayFieldStart("files");
for (File file : value.getFiles()) {
gen.writeStartObject();
gen.writeNumberField("id", file.getId());
gen.writeStringField("name", file.getName());
gen.writeEndObject();
}
gen.writeEndArray();
gen.writeEndObject();
}
}因此,我們可以將 Folder 類序列化為僅包含我們想要字段的簡化視圖。
3.2. 使用內部 ObjectMapper
雖然自定義序列化器允許我們對每個屬性進行精細調整,但為了簡化工作,我們可以重用 Jackson 的默認序列化器。
使用默認序列化器的一種方式是訪問內部的 ObjectMapper 類:
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
ObjectMapper mapper = (ObjectMapper) gen.getCodec();
gen.writeFieldName("files");
String stringValue = mapper.writeValueAsString(value.getFiles());
gen.writeRawValue(stringValue);
gen.writeEndObject();
}
因此,Jackson 負責處理繁重的工作,通過序列化 List 中的 File 對象,然後我們的輸出結果將一致。
3.3. 使用 SerializerProvider
使用默認序列化器的一種方式是使用 SerializerProvider。因此,我們將該過程委託給 File 類型的默認序列化器。
現在,藉助 SerializerProvider,我們可以簡化我們的代碼:
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
provider.defaultSerializeField("files", value.getFiles(), gen);
gen.writeEndObject();
}
而且,正如之前一樣,我們得到相同的輸出。
4. 可能的遞歸問題
根據用例,我們可能需要通過包含更多關於 文件夾 的細節來擴展我們的序列化數據。這可能是為了 一個無法修改的遺留系統或外部應用程序,以便集成。
讓我們修改我們的序列化器,創建一個 詳細信息 字段,以便將 文件夾 類的所有字段暴露在序列化數據中:
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
provider.defaultSerializeField("files", value.getFiles(), gen);
// this line causes exception
provider.defaultSerializeField("details", value, gen);
gen.writeEndObject();
}
這次我們遇到了 StackOverflowError 異常。
當我們定義一個自定義序列化器時,Jackson 內部會覆蓋為類型 Folder 創建的原始 BeanSerializer 實例。 結果,我們的 SerializerProvider 每次都能找到定製化的序列化器,而不是默認的,從而導致了無限循環。
那麼,如何解決這個問題呢?在下一部分我們將看到一個可行的解決方案。
5. 使用 BeanSerializerModifier
一種可能的解決方法是使用 BeanSerializerModifier,用於存儲 Folder 類型的默認序列化器,在 Jackson 內部覆蓋它。
讓我們修改我們的序列化器並添加一個額外的字段——defaultSerializer:
private final JsonSerializer<Object> defaultSerializer;
public FolderJsonSerializer(JsonSerializer<Object> defaultSerializer) {
super(Folder.class);
this.defaultSerializer = defaultSerializer;
}
接下來,我們將創建一個 BeanSerializerModifier 的實現,以通過默認序列化器:
public class FolderBeanSerializerModifier extends BeanSerializerModifier {
@Override
public JsonSerializer<?> modifySerializer(
SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (beanDesc.getBeanClass().equals(Folder.class)) {
return new FolderJsonSerializer((JsonSerializer<Object>) serializer);
}
return serializer;
}
}
現在,我們需要將 BeanSerializerModifier 註冊為模塊,以便使其生效:
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setSerializerModifier(new FolderBeanSerializerModifier());
mapper.registerModule(module);
然後,我們使用 defaultSerializer 對 details 字段進行處理:
@Override
public void serialize(Folder value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("name", value.getName());
provider.defaultSerializeField("files", value.getFiles(), gen);
gen.writeFieldName("details");
defaultSerializer.serialize(value, gen, provider);
gen.writeEndObject();
}
最後,我們可能需要從 details 字段中移除 files 字段,因為我們已經在序列化數據中將其單獨寫入。
因此,我們只需在 Folder 類中忽略 files 字段。
@JsonIgnore
private List<File> files = new ArrayList<>();
終於,問題得到解決,我們獲得了預期的輸出結果:
{
"name": "Root Folder",
"files": [
{"id": 1, "name": "File 1"},
{"id": 2, "name": "File 2"}
],
"details": {
"id":1,
"name": "Root Folder",
"owner": "root",
"created": 1565203657164,
"modified": 1565203657164,
"lastAccess": 1565203657164
}
}
6. 結論
在本教程中,我們學習瞭如何在 Jackson 庫中的自定義序列化器內部調用默認序列化器。