記錄Jackson和Lombok的坑

su_xtf2009發表於2021-07-23

記錄Jackson和Lombok的坑

今天遇到Jackson反序列化json缺少了欄位,後來研究下發現是Jackson的機制和Lombok生成的setter不一致,導致沒有正確呼叫setter。

復現

Java實體類

@Data
public class DemoData{
    private Double t;
    private Double eDay;
}

Json字串

{
    "t":12.23,
    "eDay":123.321
}

使用Jackson解析下來,發現只有t有值,而eDay沒有解析到。

原因分析

首先第一反應是Lombok生成的getter和setter也許有問題,於是去掉@Data註解,用IDEA生成getter和setter,再進行反序列化,發現已經可以正常反序列化了。

於是看了下編譯生成的程式碼:

public class DemoData{
    private Double t;
    private Double eDay;

    public Double getT() {
        return this.t;
    }

    public Double getEDay() {
        return this.eDay;
    }

    public void setT(final Double t) {
        this.t = t;
    }

    public void setEDay(final Double eDay) {
        this.eDay = eDay;
    }
}

去掉lombok的註解,直接用IDEA生成getter和setter,生成之後是這樣的:

public class DemoData{
    private Double t;
    private Double eDay;

    public Double getT() {
        return t;
    }

    public void setT(Double t) {
        this.t = t;
    }

    public Double geteDay() {
        return eDay;
    }

    public void seteDay(Double eDay) {
        this.eDay = eDay;
    }
}

顯然兩邊的Getter和Setter是不一樣的,那麼Jackson是怎麼尋找屬性和Setter的呢?

Jackson2在初始化序列器時,對pojo型別物件會收集其屬性資訊,屬性包括成員變數及方法,然後屬性名稱和處理過後的方法名稱做為key儲存到一個LinkedHashMap中。
收集過程中會呼叫com.fasterxml.jackson.databind.util.BeanUtil中的legacyManglePropertyName方法用來處理方法名稱,它會將get/set方法字首,即get或set去掉,並將其後面的連續大寫字元轉換成小寫字元返回。
例如: getNEWString會轉變成newstring返回。你的屬性名稱如果有這樣的"nSmallSellCount",lombok自動生成的get方法就會是這樣的"getNSmallSellCount",處理過後就是這樣的"nsmallSellCount",這與屬性nSmallSellCount並不衝突,可以同時存在於HashMap中。

所以,當Jackson掃描由Lombok生成的POJO時,讀取到setEDay,會把set去掉,拿到EDay,然後轉成eday。由此導致json中的eDay屬性在LinkedHashMap中沒有找到setter方法,反序列化就丟失了欄位。

所以原因已經確定了:當使用Lombok修飾的POJO中存在由aAxxx這樣的(單個小寫字母跟著大寫字母)的屬性時,反序列化會丟失這個欄位。

如何解決

DeLombok

當程式碼中出現這樣的欄位時,由IDEA生成對應的getter和setter,會自動覆蓋lombok生成的方法。

使用Builder來做Jackson的反序列化器

Lombok似乎意識到了這個問題(所以為啥不改下setter的生成呢???),編寫了@Jacksonized這個註解來為Jackson反序列提供支援,但是這個註解必須配合@Builder或者@SuperBuilder一起使用才會生效。

我們看下@Jacksonized的官方說明:

/**
 * The {@code @Jacksonized} annotation is an add-on annotation for
 * {@code @}{@link Builder} and {@code @}{@link SuperBuilder}. It automatically
 * configures the generated builder class to be used by Jackson's
 * deserialization. It only has an effect if present at a context where there is
 * also a {@code @Builder} or a {@code @SuperBuilder}; a warning is emitted
 * otherwise.
 * <p>
 * In particular, the annotation does the following:
 * <ul>
 * <li>Configure Jackson to use the builder for deserialization using
 * {@code @JsonDeserialize(builder=Foobar.FoobarBuilder[Impl].class)}
 * on the class (where <em>Foobar</em> is the name of the annotated class).</li>
 * <li>Copy Jackson-related configuration annotations (like
 * {@code @JsonIgnoreProperties}) from the class to the builder class. This is
 * necessary so that Jackson recognizes them when using the builder.</li>
 * <li>Insert {@code @JsonPOJOBuilder(withPrefix="")} on the generated builder
 * class to override Jackson's default prefix "with". If you configured a
 * different prefix in lombok using {@code setterPrefix}, this value is used. If
 * you changed the name of the {@code build()} method using using
 * {@code buildMethodName}, this is also made known to Jackson.</li>
 * <li>For {@code @SuperBuilder}, make the builder implementation class
 * package-private.</li>
 * </ul>
 * This annotation does <em>not</em> change the behavior of the generated builder.
 * A {@code @Jacksonized} {@code @SuperBuilder} remains fully compatible to
 * regular {@code @SuperBuilder}s.
 */

簡單來說,這個註解會做下面的事:

  1. 會通過@JsonDeserialize註解讓Jackson使用Builder來構建物件;
  2. 拷貝Jackson相關的註解到Builder中(比如@JsonIgnoreProperties);
  3. 生成的Builder類會新增@JsonPOJOBuilder註解並寫入prefix;

因此,把上面的Pojo改寫成這樣:

@Data
@Builder
@Jacksonized
public class DemoData {
    private Double t;
    private Double eDay;
}

會生成下面的POJO:

@JsonDeserialize(
    builder = DemoData.DemoDataBuilder.class
)
public class DemoData {
    private Double t;
    private Double eDay;

    DemoData(final Double t, final Double eDay) {
        this.t = t;
        this.eDay = eDay;
    }

    public static DemoData.DemoDataBuilder builder() {
        return new DemoData.DemoDataBuilder();
    }

    public Double getT() {
        return this.t;
    }

    public Double getEDay() {
        return this.eDay;
    }

    public void setT(final Double t) {
        this.t = t;
    }

    public void setEDay(final Double eDay) {
        this.eDay = eDay;
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof DemoData)) {
            return false;
        } else {
            DemoData other = (DemoData)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                Object this$t = this.getT();
                Object other$t = other.getT();
                if (this$t == null) {
                    if (other$t != null) {
                        return false;
                    }
                } else if (!this$t.equals(other$t)) {
                    return false;
                }

                Object this$eDay = this.getEDay();
                Object other$eDay = other.getEDay();
                if (this$eDay == null) {
                    if (other$eDay != null) {
                        return false;
                    }
                } else if (!this$eDay.equals(other$eDay)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof DemoData;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $t = this.getT();
        int result = result * 59 + ($t == null ? 43 : $t.hashCode());
        Object $eDay = this.getEDay();
        result = result * 59 + ($eDay == null ? 43 : $eDay.hashCode());
        return result;
    }

    public String toString() {
        Double var10000 = this.getT();
        return "DemoData(t=" + var10000 + ", eDay=" + this.getEDay() + ")";
    }

    @JsonPOJOBuilder(
        withPrefix = "",
        buildMethodName = "build"
    )
    public static class DemoDataBuilder {
        private Double t;
        private Double eDay;

        DemoDataBuilder() {
        }

        public DemoData.DemoDataBuilder t(final Double t) {
            this.t = t;
            return this;
        }

        public DemoData.DemoDataBuilder eDay(final Double eDay) {
            this.eDay = eDay;
            return this;
        }

        public DemoData build() {
            return new DemoData(this.t, this.eDay);
        }

        public String toString() {
            return "DemoData.DemoDataBuilder(t=" + this.t + ", eDay=" + this.eDay + ")";
        }
    }
}

此時,Jackson會使用建造者方法來構建物件,寫入屬性,Json也可以正常解析了。

相關文章