【问题标题】:How to create default constructor for immutable class如何为不可变类创建默认构造函数
【发布时间】:2019-02-25 11:39:00
【问题描述】:

我喜欢基于this article (Why objects must be immutable) 使我的对象不可变。

但是,我正在尝试使用 Jackson Object Mapper 解析对象。我最初收到的是JsonMappingException: No suitable constructor found for type [simple type, class ]: cannot instantiate from JSON object.

我可以像here 中提到的那样修复它,方法是提供一个默认构造函数并使我的字段不是最终的。

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;

@AllArgsConstructor
// @NoArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@Data
public class School {

    @NonNull
    private final String schoolId;

    @NonNull
    private final String schoolName;
}

我应该遵循什么好的编程风格来克服这个问题?唯一的办法是让我的对象可变吗?

我可以使用不使用默认构造函数的不同映射器吗?

【问题讨论】:

标签: java jackson immutability lombok


【解决方案1】:

TL;DR:使用 lombok 并避免使用默认构造函数

  • 使用@Value 制作不可变数据类
  • @JsonProperty("name-of-property")注释所有字段
  • lombok.copyableAnnotations += com.fasterxml.jackson.annotation.JsonProperty 添加到您的 lombok.config 以将其复制到生成的构造函数中
  • 创建一个带有@JsonCreator 注释的全参数构造函数

示例:

@Value
@AllArgsConstructor(onConstructor_ = @JsonCreator)
class School {
    @JsonProperty("schoolId")
    String schoolId;
    @JsonProperty("schoolName")
    String schoolName;
}

长答案

对于使用@JsonCreator 注释的静态工厂方法,imo 有一个更好的替代方法,那就是为所有元素提供一个构造函数(无论如何都是不可变类所必需的)。用@JsonCreator 注释那个 并用@JsonProperty 注释所有参数,如下所示:

class School {
    //fields

    @JsonCreator
    public School(
            @JsonProperty("id") String id,
            @JsonProperty("name") String name) {
        this.schoolId = id;
        this.schoolName = name;
    }

    //getters
}

这些是@JsonCreator 注释为您提供的选项。它在其文档中这样描述它们:

  • 单参数构造函数/工厂方法,没有参数的 JsonProperty 注释:如果是这样,这就是所谓的“委托创建者”,在这种情况下,杰克逊首先将 JSON 绑定到参数类型,然后调用创建者。这通常与 JsonValue 结合使用(用于序列化)。
  • 构造函数/工厂方法,其中每个参数都使用 JsonProperty 或 JacksonInject 进行注释,以指示要绑定到的属性名称

在某些情况下,您甚至可能不需要显式指定参数名称。有关@JsonCreator 的文档进一步指出:

还请注意,所有 JsonProperty 注释必须指定实际名称(“默认”不是空字符串),除非您使用可以检测参数名称的扩展模块之一;这是因为 8 之前的默认 JDK 版本无法从字节码存储和/或检索参数名称。但是对于 JDK 8(或使用辅助库,如 Paranamer,或其他 JVM 语言,如 Scala 或 Kotlin),指定名称是可选的。

另外,这也可以很好地与 lombok 版本 1.18.3 或更高版本一起使用,您可以将 lombok.copyableAnnotations += com.fasterxml.jackson.annotation.JsonProperty 添加到您的 lombok.config 并因此让它将 JsonProperty 注释复制到构造函数,因为您确实注释了所有带有它的字段(海事组织无论如何都应该这样做)。要将@JsonCreator-annotation 放在构造函数上,您可以使用实验性的onX feature。将 lombok 的 @Value 用于不可变数据类,您的 DTO 可能看起来像这样(未经测试):

@Value
//@AllArgsConstructor(onConstructor = @__(@JsonCreator)) // JDK7 or below
@AllArgsConstructor(onConstructor_ = @JsonCreator) // starting from JDK8
class School {
    @JsonProperty("schoolId")
    String schoolId;
    @JsonProperty("schoolName")
    String schoolName;
}

【讨论】:

    【解决方案2】:

    您可以使用 Jackson 工厂(使用 @JsonCreator 注释的方法)从地图中读取字段并调用您的非默认构造函数:

    class School {
        //fields
    
        public School(String id, String name) {
            this.schoolId = id;
            this.schoolName = name;
        }
    
        @JsonCreator
        public static School create(Map<String, Object> object) {
            return new School((String) object.get("schoolId"), 
                              (String) object.get("schoolName"));
        }
    
        //getters
    }
    

    Jackson 将使用 Map 版本的 json 调用 create 方法。这有效地解决了问题。

    我相信您的问题是在寻找杰克逊的解决方案,而不是新的模式/风格。

    【讨论】:

    • 感谢您的建议!但是,由于@NonNull,这不起作用。我得到NPE。 "java.lang.NullPointerException: schoolId 被标记为@NonNull 但为空
    • @SyncMaster 那一定是因为你的 JSON。 schoolId 在您的 json 中是 null 还是缺失(或具有不同的字段名称)?你能展示一个你的 json 内容的例子吗?
    猜你喜欢
    • 2023-01-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-10
    • 2018-10-02
    • 1970-01-01
    • 2021-04-27
    • 1970-01-01
    相关资源
    最近更新 更多