1. 簡介
將完整的對象數據結構序列化為 JSON,採用所有字段的精確一對一表示,有時可能不合適,或者可能不是我們想要的結果。相反,我們可能想要創建一個對我們的數據進行擴展或簡化視圖。 這就是自定義 Jackson 序列化器發揮作用的地方。
但是,實現自定義序列化器可能很繁瑣,尤其當我們的模型對象具有大量的字段、集合或嵌套對象時。 幸運的是,Jackson 庫提供了幾個可以使這項工作更容易的機制。
在本簡短教程中,我們將探討自定義 Jackson 序列化器,並展示 如何在自定義序列化器內部訪問默認序列化器。
2. 示例數據模型
在深入探討 Jackson 的自定義之前,讓我們先查看我們想要序列化的示例 Folder 類:
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. Custom Serializers in Jackson
使用自定義序列器的主要優勢在於,我們不必修改我們的類結構。此外,我們可以輕鬆地將我們期望的行為與類本身分離。
因此,假設我們想要一個減少視圖的 Folder 類:
{
"name": "Root Folder",
"files": [
{"id": 1, "name": "File 1"},
{"id": 2, "name": "File 2"}
]
}
正如我們在下一部分所看到的,有幾種方法可以實現我們想要的結果在 Jackson 中。
3.1. Brute Force Approach
首先,在不使用 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. Using Internal 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. Using 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);
// 這行代碼導致異常
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 字段,因為我們已經在序列化數據中將其寫入了。
因此,我們簡單地忽略 files 字段在 Folder 類中的。
@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 庫中的自定義序列化器中調用默認序列化器。