如何使用 Lombok 构建器反序列化 Java 的 Optional 字段?

huangapple go评论161阅读模式
英文:

How to deserialize Java Optional field with lombok builder?

问题

我想要在 lombok 的 builder 中反序列化 Java Optional 字段以下是我的代码示例

```Java
    @JacksonXmlRootElement(localName = "Availability")
    @JsonDeserialize(builder = AvailabilityResponse.Builder.class)
    @Getter
    @ToString
    @EqualsAndHashCode
    @Builder(setterPrefix = "with", builderClassName = "Builder", toBuilder = true)
    public class AvailabilityResponse {

        private final List<Train> trainDetails;
        private final String name;
        private final Optional<String> detail;

        public static class Builder {

            @JacksonXmlProperty(localName = "TRAIN")
            private List<Train> trainDetails;

            @JacksonXmlProperty(localName = "NAME")
            private String name;

            @JacksonXmlProperty(localName = "DETAIL_TRAIN", isAttribute = true)
            private String detail;

            public AvailabilityResponse build() {
                return new AvailabilityResponse(
                    trainDetails, // 这里有字段验证。如果为 null 或空,则抛出异常
                    name, // 这里有字段验证。如果为 null 或空,则抛出异常
                    Optional.ofNullable(detail)); // 这是一个 Optional 字段
            }
        }	
    }

如果我像下面这样覆盖 builder 方法,就能够反序列化:

    private Optional<String> name; // 这是静态 builder 类的字段

    @JacksonXmlProperty(localName = "DETAIL_TRAIN", isAttribute = true)
    public Builder detail(final String theDetail) {
        this.detail = Optional.ofNullable(theDetail);
        return this;
    }

我在 @Builder 中使用了 setterPrefix = "with"。但如果我使用 "with" 前缀覆盖上述方法,它就无法正常工作。

请有人帮助我实现这个目标。


<details>
<summary>英文:</summary>

I want to deserialize Java Optional field in lombok builder. Below is my code

```    JacksonXmlRootElement(localName = &quot;Availability&quot;)
@JsonDeserialize(builder = AvailabilityResponse.Builder.class)
@Getter
@ToString
@EqualsAndHashCode
@Builder(setterPrefix = &quot;with&quot;, builderClassName = &quot;Builder&quot;, toBuilder = true)
public class AvailabilityResponse {

    private final List&lt;Train&gt; trainDetails;
    private final String name;
    private final Optional&lt;String&gt; detail;

    public static class Builder {

        @JacksonXmlProperty(localName = &quot;TRAIN&quot;)
        private List&lt;Train&gt; trainDetails;

        @JacksonXmlProperty(localName = &quot;NAME&quot;)
        private String name;

        @JacksonXmlProperty(localName = &quot;DETAIL_TRAIN&quot;, isAttribute = true)
        private String detail;

        public AvailabilityResponse build() {
            return new AvailabilityResponse(
                trainDetails, // I have field validation here. if null or empty throwing Exception
                name, // I have field validation here. if null or empty throwing Exception
                Optional.ofNullable(detail)); // This is Optional field
        }
    }	

}    

If I override the builder method like below, able to deserialize

    private Optional&lt;String&gt; name; // this is static builder class field

        @JacksonXmlProperty(localName = &quot;DETAIL_TRAIN&quot;, isAttribute = true)
        public Builder detail(final String theDetail) {
            this.detail = Optional.ofNullable(theDetail);
            return this;
        }   

I have used setterPrefix ="with" in @Builder. But if I override the above method with "with" prefix, its not working.

Please someone help me to acheive this

答案1

得分: 4

Jackson支持使用其jackson-datatype-jdk8模块来处理Optional。您只需将其添加到您的pom.xml文件的<dependencies>部分中:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jdk8</artifactId>
    <version>2.10.3</version>
</dependency>

然后,按以下方式初始化您的映射器:

ObjectMapper mapper = new XmlMapper().registerModule(new Jdk8Module());

该映射器将自动检测Optional:如果XML字段具有值,它将被包装在Optional中;如果XML字段为空(即<DETAIL_TRAIN/>),则结果将是Optional.empty()。(在您的情况下,您有一个属性;这些属性不能是空的。)

Jackson将不存在于XML中的字段或属性映射为null(或者更准确地说,它根本不调用setter方法,因此字段值仍将是默认值)。如果您还想在这种情况下拥有一个空的Optional,您需要将字段默认值设置为Optional.empty(),并在该字段上添加@Builder.Default注解。

最后,Lombok可以自动将字段上的注解复制到生成的构建器中。您需要使用项目根目录中的lombok.config文件告诉Lombok要复制哪些注解:

lombok.copyableAnnotations += com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
config.stopBubbling = true

这意味着您不需要定制您的构建器类:

@JsonDeserialize(builder = AvailabilityResponse.Builder.class)
@Getter
@ToString
@EqualsAndHashCode
@Builder(setterPrefix = "with", builderClassName = "Builder", toBuilder = true)
public class AvailabilityResponse {

    @JacksonXmlProperty(localName = "TRAIN")
    @JsonProperty("TRAIN")
    private final List<Train> trainDetails;

    @JacksonXmlProperty(localName = "NAME")
    @JsonProperty("NAME")
    private final String name;

    @JacksonXmlProperty(localName = "DETAIL_TRAIN", isAttribute = true)
    @JsonProperty("DETAIL_TRAIN")
    @Builder.Default
    private final Optional<String> detail = Optional.empty();
}

请注意,由于Jackson的一个错误,您还需要在字段上使用@JsonProperty注解。

如果您想要验证字段值,您应该在构造函数中进行,而不是在构建器中。通过这种方式,您还将捕获那些未使用构建器的实例化。要实现这一点,只需自己实现全参构造函数;build()方法将自动使用它:

private AvailabilityResponse(List<Train> trainDetails, String name, Optional<String> detail) {
    this.trainDetails = Objects.requireNonNull(trainDetails);
    this.name = Objects.requireNonNull(name);
    this.detail = Objects.requireNonNull(detail);
}

我建议将此构造函数设置为private,因为:

  • 它强制要求使用您的类的用户使用构建器,以及

  • 在您的公共API中将Optional作为参数是不好的风格。

英文:

Jackson supports Optional using its jackson-datatype-jdk8 module. You can simply add it to the &lt;dependencies&gt; section in your pom.xml:

&lt;dependency&gt;
	&lt;groupId&gt;com.fasterxml.jackson.datatype&lt;/groupId&gt;
	&lt;artifactId&gt;jackson-datatype-jdk8&lt;/artifactId&gt;
	&lt;version&gt;2.10.3&lt;/version&gt;
&lt;/dependency&gt;

Then, initialize your mapper as follows:

ObjectMapper mapper = new XmlMapper().registerModule(new Jdk8Module());

That mapper will automatically detect Optionals: If the XML field has a value, it will be wrapped in an Optional; if the XML field is empty (i.e., &lt;DETAIL_TRAIN/&gt;), then the result will be an Optional.empty(). (In your case you have an attribute; those cannot be empty.)

Jackson maps fields or attributes that do not exist in the XML to null (or, more precisely, it simply does not call the setter, so the field value will still be the default). If you also want to have an empty Optional in that case, you need to set the field default to Optional.empty() and add a @Builder.Default to that field.

Finally, lombok can automatically copy your annotations from the fields to the generated builder. You need to tell lombok which annotations to copy using a lombok.config file in your project root containing:

lombok.copyableAnnotations += com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
config.stopBubbling = true

This means you do not need to customize your builder class at all:

@JsonDeserialize(builder = AvailabilityResponse.Builder.class)
@Getter
@ToString
@EqualsAndHashCode
@Builder(setterPrefix = &quot;with&quot;, builderClassName = &quot;Builder&quot;, toBuilder = true)
public class AvailabilityResponse {

    @JacksonXmlProperty(localName = &quot;TRAIN&quot;)
    @JsonProperty(&quot;TRAIN&quot;)
    private final List&lt;Train&gt; trainDetails;

    @JacksonXmlProperty(localName = &quot;NAME&quot;)
    @JsonProperty(&quot;NAME&quot;)
    private final String name;

    @JacksonXmlProperty(localName = &quot;DETAIL_TRAIN&quot;, isAttribute = true)
    @JsonProperty(&quot;DETAIL_TRAIN&quot;)
    @Builder.Default
    private final Optional&lt;String&gt; detail = Optional.empty();
}

Note that you also need @JsonProperty annotations on your fields due to a Jackson bug.

If you want to validate field values, you should do that in the constructor, not in the builder. In this way you'll also catch those instantiations where the builder is not used. To do so, simply implement the all-args constructor yourself; the build() method will use it automatically:

private AvailabilityResponse(List&lt;Train&gt; trainDetails, String name, Optional&lt;String&gt; detail) {
	this.trainDetails = Objects.requireNonNull(trainDetails);
	this.name = Objects.requireNonNull(name);
	this.detail = Objects.requireNonNull(detail);
}

I suggest making this constructor private, because

  • it enforces users of your class to use the builder, and
  • it is bad style to have Optionals as parameters in your public API.

huangapple
  • 本文由 发表于 2020年4月5日 10:49:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/61037425.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定