Contents

Jackson 库的自定义序列化程序中调用默认序列化程序

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. 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 只是通过序列化File对象List来处理繁重的工作,然后我们的输出将是相同的。

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. 一个可能的递归问题

根据用例,我们可能需要通过为Folder包含更多详细信息来扩展我们的序列化数据。这可能是为了集成一个遗留系统或一个我们没有机会修改的外部应用程序

让我们更改我们的序列化程序,为我们的序列化数据创建一个details字段,以简单地公开Folder类的所有字段:

@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 在 Jackson 内部覆盖它之前存储类型Folder的默认序列化程序。

让我们修改我们的序列化器并添加一个额外的字段 - 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);

然后,我们为details字段使用defaultSerializer

@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
    }
}