深入研究 ObjectMapper

发布于:2024-05-11 ⋅ 阅读:(16) ⋅ 点赞:(0)

1. 序列化/反序列化问题

当基于 EDA(事件驱动架构)进行操作时,必须特别注意消息的序列化/反序列化。这是因为一般eda系统是分布式的。

例如,考虑如下场景:System1 修改消息相关对象,如下所示。 System1 负责序列化和发布消息。但是,其他组织系统(例如 System2 和 System3)可能具有不同的部署时间。如果变化没有得到反映,消费规则的不匹配可能会导致失败。因此,有效地管理ObjectMapper至关重要。

1-1. FAIL_ON_UNKNOWN_PROPERTIES

FAIL_ON_UNKNOWN_PROPERTIES选项始终需要仔细注意。

FAIL_ON_UNKNOWN_PROPERTIES选项是一个属性,用于确定反序列化过程中没有匹配字段(未知属性)时反序列化是否应该失败。
cf) 当将新字段添加到对象以进行序列化并部署时,但使用系统(其中对象被反序列化)没有反映该字段。

如果此属性设置为 true,则会出现 JsonMappingException 异常,从而导致失败。为了防止这种情况,应该将其设置为 false(以忽略未知属性),或者正在反序列化的对象应该另外用 进行注释@JsonIgnoreProperties

1-2. Spring Boot FAIL_ON_UNKNOWN_PROPERTIES

使用Spring Boot时,Jackson2ObjectMapperFactoryBean中该FAIL_ON_UNKNOWN_PROPERTIES选项默认调整为FALSE。

然而,如果直接创建一个新的ObjectMapper,它不会通过Jackson2ObjectMapperFactoryBean,因此FAIL_ON_UNKNOWN_PROPERTIES保持true(默认值)。

因此,需要禁用它,如下面的代码所示。

private static ObjectMapper objectMapper() {
    ObjectMapper objectMapper = JsonMapper.builder()
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .build()
}

1-3. Codehaus 和 FasterXML

有些服务混合使用org.codehaus.jackson(Jackson 1.x) 和com.fasterxml.jackson(Jackson 2.x)。由于该服务具有悠久的历史,因此它有多个 ObjectMappers。因此,尽管希望向 ObjectMapper 添加选项,但预测影响范围具有困难性,使得修改变得困难。换句话说,如果忽视 ObjectMapper 管理,就会存在很大的风险。如果在更改 ObjectMapper 设置时预测影响范围变得困难,则将@JsonIgnoreProperties(ignoreUnknown = true)注释添加到所有类会更安全。

实践中,也曾出现过出现问题的情况。尽管导入com.fasterxml.jackson.annotation.JsonIgnoreProperties并添加了@JsonIgnoreProperties注释,但用于反序列化的 ObjectMapper 来自 codehaus。结果,Could not read JSON: Unrecognized field出现了错误。导入过程中的错误可能会导致问题,而且即使在代码审查期间,这些错误也很有可能被忽视。在service lib 混合的情况下,建议对 codehaus 进行标准化,因为 Jackson 2 也支持它,这使其成为一个安全的选择。后来逐步淘汰Jackson 1时,最好统一切换到fasterxml。


2. 理解反序列化原理

如果 Jackson 决定反序列化为 Sample 类类型,它首先在 Sample 类中查找无参数构造函数。如果存在,它会使用该构造函数创建一个 Sample 实例,然后查找 setter。然后,它检查与 JSON 字符串中的键同名的 setter,并相应地注入值。

public static class Sample {
    private String name;
    private String address;

    public Sample() { }

    public void setName(String name) {
        this.name = name;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

我们再来看另一个案例。 JSON 传递姓名、地址、性别等数据,如下所示

name : "aaa",
address : "bbb",
gender : "1"

但在反序列化到的 Sample 对象中,没有性别字段的设置器。在本例中,名称和地址是通过其 setter 注入的,并且由于没有性别的 setter,因此它会检查具有相同名称的字段。如果找到,它会使用反射注入该值。如果未找到,它会忽略它或引发异常,具体取决于设置FAIL_ON_UNKNOWN_PROPERTIES

// 1. setter
// 2. reflection
public static class Sample {
    private String name;
    private String address;
    private String gender;

    public Sample() { }

    public void setName(String name) {
        this.name = name;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

接下来,如果有一个全参数构造函数,它将通过该构造函数注入值。然而,仅仅添加构造函数是不够的;必须用@JsonCreator@JsonProperty注释来表示。

// 1. constructor
// 2. setter
// 3. reflection
public static class Sample {
    private String name;
    private String address;
    private String gender;

    @JsonCreator
    public Sample(
        @JsonProperty("name") String name,
        @JsonProperty("address") String address,
        @JsonProperty("gender") String gender
    ) {
        this.name = name;
        this.address = address;
        this.gender = gender;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

但是,由于上述方法可能很麻烦,因此有一种涉及使用@ConstructorProperties注释的替代方法,如下所示。

public static class Sample {
    private final String name;
    private final String address;
    private final String gender;

    @ConstructorProperties({"name", "address", "gender"})
    public Sample(String name, String address, String gender) {
        this.name = name;
        this.address = address;
        this.gender = gender;
    }
}

反序列化如何与继承一起工作?首先,它通过构造函数注入值,但没有父级的字段。这意味着它不是通过构造函数设置的。接下来,它会寻找一个setter,但如果没有可用的setter,最后,它会诉诸反射来注入该字段,因为该字段存在

// 1. constructor
// 2. setter
// 3. reflection
public static abstract class Parent {
    private String parent;
}

public static class Sample extends Parent {

    @ConstructorProperties({"name", "address"})
    public Sample(String name, String address) {
        this.name = name;
        this.address = address;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAddress(String address) {
        this.address = address;
    }    
}

但是,由于使用 setter 优于反射,因此可以按如下方式创建 setter

public static abstract class Parent {
    private String parent;

    void setParent(String parent) {
        this.parent = parent;
    }
}

对于 Collection 类型,还有一个额外的步骤(getter)。如下面的代码所示,可能没有 setter,但可以存在 getter。如果是 Collection 类型,Jackson 会调用 getter。如果 getter 返回的列表为 null,则继续下一步。但是,如果列表不为空并且返回列表,则 Jackson 将添加到该列表中。

// 1. constructor
// 2. setter
// 3. getter !!
// 4. reflection
public static class Sample {
    private List<String> samples = new ArrayList<>();

    public List<String> getSamples() {
        return samples;
    }
}

有一个需要注意的警告。您不应使用List.of()如下所示的运算符,因为它会产生ImmutableCollections.emptyList()不可修改的 。因此,当杰克逊试图添加时就会发生错误。

public static class Sample {
    private List<String> samples = List.of();

    public List<String> getSamples() {
        return samples;
    }
}

类似地,如果 getter 用 包裹列表unmodifiableList,那么 Jackson 尝试向其中添加内容时就会发生错误。因此,作为一种解决方案,应该使用@JsonIgnore或避免使用 unmodifyingList 来处理它。

public static class Sample {
    private List<String> samples = new ArrayList<>();

    // Add @JsonIgnore or remove unmodifiableList()
    public List<String> getSamples() {
        return Collections.unmodifiableList(samples);
    }
}

3. 多态反序列化

当您在将 readValue 与 ObjectMapper 结合使用时指定必要的具体类 (Sample) 时,它会自动反序列化为 Sample 类型。

Sample sample = OBJECT_MAPPER.readValue(json, Sample.class);

当您需要使用接口进行反序列化时,框架会自动为您处理反序列化。例如,考虑有 AEvent、BEvent 和 CEvent 类实现 Event 接口的场景。 raiseEvent 方法接收 Event 类型的参数,但您希望根据情况将其反序列化为适当的具体类。

public static interface Event {
  ...
}

public static class AEvent implements Event { ... }
public static class BEvent implements Event { ... }
public static class CEvent implements Event { ... }

... 

protected void raiseEvent(Event event) {
    delegate.raiseEvent(event);
}

当然,我们可以创建一个能够保存所有消息的类并反序列化到其中。然而,这种方法需要了解哪些字段被使用,哪些字段未被使用,这使得管理具有挑战性。可空案例的普遍存在使得它一开始很方便,但随着时间的推移很难维持。

即使在处理接口类型时,为了反序列化为适当的具体类,也需要有一些东西可以识别正确的类。这就是@JsonTypeInfo派上用场的地方。通过附加此注释,可以在序列化期间嵌入类型信息,指示要反序列化到哪个类。

@JsonTypeInfo(use = Id.NAME, include = As.WRAPPER_OBJECT)
private List<Event> events = new ArrayList<>();

换句话说,当序列化接口类型时,类型名称也会与数据一起序列化,以确保在反序列化时可以识别该类型。这是通过嵌入一个 id 值来完成的,该值用于选择要反序列化的正确类。如果策略是WRAPPER_OBJECT,则数据将被包装,从而产生额外的深度层。这可以在 JSON 结构中观察到,例如,AEvent 将用作键,实际数据将嵌套在其下方的附加层中。

{
  "events":
    [
      {
        "AEvent":{"sourceVersion":100,"id":1}
      }

        ...
    ]
}

更改@JsonTypeInfo设置需要小心,因为如果反序列化端未正确对齐,则可能会发生错误。由于Id.NAME与 一起使用@JsonTypeInfo,序列化仅传递类名,因此包中的更改不会影响它。但是,如果Id.CLASS使用 ,它会序列化完全限定的 Java 类名,包括包名称,因此包中的任何更改都会影响反序列化。

{
  "events":
    [
      {
        "com.toy.AEvent":{"sourceVersion":100,"id":1}
      }

        ...
    ]
}

如果PROPERTY使用 代替WRAPPER_OBJECT,则使用密钥传递类型信息@type。”

{
  "events":
    [
      {
        "@type":"AEvent"
        "sourceVersion":100,
        "id":1
      }

        ...
    ]
}

接下来,在 setter 上单独指定@JsonSubTypes与 匹配的注释。@JsonTypeInfo这会注册映射信息,以便在反序列化接口类型时根据序列化名称确定恢复类型。

@JsonSubTypes({
    @Type(value = AEvent.class, name = "AEvent"),
    @Type(value = BEvent.class, name = "BEvent"),
    @Type(value = CEvent.class, name = "CEvent"),
})
protected void setEvents(List<Event> Events) {
    this.Events = Events;
}

cf) 注意:添加新的事件类型时,必须将其添加到此处以确保正确的反序列化。

如果创建了 DEvent 但未将其添加到 JsonSubTypes 中,则会出现以下错误:

com.fasterxml.jackson.databind.exc.InvalidTypeIdException:
Could not resolve type id 'DEvent' as a subtype of `com.toy.Event`: known type ids = [AEvent, BEvent, CEvent] (for POJO property 'events')
at [Source: (String)"{"events":[{"AEvent":{"type":"AEvent","sourceVersion":100,"id":1}},{"BEvent":{"type":"BEvent","sourceVersion":200,"id":2}},{"CEvent":{"type":"CEvent","sourceVersion":300,"id":3}},{"DEvent":{"type":"DEvent","sourceVersion":400,"id":4}}]}"; line: 1, column: 181] (through reference chain: com.toy.Process["events"]->java.util.ArrayList[3])

4.ObjectMapper中的其他选项

首先,我们禁用该ALLOW_FINAL_FIELDS_AS_MUTATORS设置。

.disable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS)

“在下面的代码中,最终字段“name”在声明时被初始化,使其之后不可变。但是,在反序列化期间,Jackson 可以使用反射来修改其值。为了防止这种情况,我们禁用该ALLOW_FINAL_FIELDS_AS_MUTATORS设置。

public static class Sample {
    private final String name = "ybs";
}

我们还禁用 FAIL_ON_EMPTY_BEANS 设置。

.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)

反序列化 Sample 类时,如果类中没有定义字段,Jackson 基本上会抛出错误。必须至少存在一个字段。因此,我们禁用该FAIL_ON_EMPTY_BEANS设置。

public static class Sample {
}

需要使用此选项的情况如下:当从 Kafka Topic 接收到 A、B、C 类型的数据,但您希望忽略 C 类型时。虽然可以在前端使用 Kafka header 值进行过滤,但在某些情况下,您需要反序列化数据以确定其类型。但是,由于不需要为类型 C 定义您不会使用的字段,因此您可以创建一个 shell 类并在必要时跳过它。

类型缓存

LRUMap 用作 TypeFactory 的缓存,允许 Jackson 在序列化和反序列化期间在内部缓存有关类型的信息。这意味着当搜索所需类型时,Jackson 首先检查缓存,如果找到则检索它并使用它。

return objectMapper.setTypeFactory(
    objectMapper.getTypeFactory()
        .withCache((LookupCache<Object, JavaType>)new LRUMap<Object, JavaType>(5120, 5120))
);

默认缓存大小为 200。

protected TypeFactory(LookupCache<Object,JavaType> typeCache, TypeParser p,
                      TypeModifier[] mods, ClassLoader classLoader)
{
    if (typeCache == null) {
        // initialEntries : 16
        // maxEntries : 200
        typeCache = new LRUMap<>(16, 200);
    }
}

当缓存已满时,如果需要添加新类型,Jackson 会在内部清除同步块内的 LRUMap 并从头开始。由于 200 个插槽可以很快填满,因此缓存大小增加到 5120。

package com.fasterxml.jackson.databind.util;

public class LRUMap<K,V>
    implements LookupCache<K,V>, // since 2.12
        java.io.Serializable
{
    @Override
    public V put(K key, V value) {
        if (_map.size() >= _maxEntries) {
            // double-locking, yes, but safe here; trying to avoid "clear storms"
            synchronized (this) {
                if (_map.size() >= _maxEntries) {
                    clear();
                }
            }
        }
        return _map.put(key, value);
    }

    ...
}

最后,让我们准确检查一下缓存的内容。在 中LookupCache<Object, JavaType>,键是一个 Object,值是一个 JavaType。 JavaType( com.fasterxml.jackson.databind.JavaType)包含了Object(key)对应的类型信息,包括超类类型、超接口类型等。

将以下 JSON 字符串反序列化为 YBS 对象时,YBS 类的 items 字段是 List 接口类型,但实际创建的具体类型是 ArrayList。

// json = "{"items":["item1","item2"]}";

@Getter
@Setter
public class YBS {

    private List<String> items;

    @ConstructorProperties({"items"})
    public YBS(List<String> items) {
        this.items = items;
    }
}

因此,会检索并缓存与 ArrayList 关联的所有对象的 JavaType。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable { ... }

最终,可以观察到为单个列表字段缓存了各种对象。

image.png


网站公告

今日签到

点亮在社区的每一天
去签到