一次 lombok 默認值爲 null 的排查實踐

背景

這周的某個晚上,同事喊我過去看個問題,大概是這樣的:爲了知足新的業務需求,對於A、B兩種不一樣的內容,在頁面呈現上必須區分出兩套規則,一套是用戶能夠進行修改和刪除的,一套是用戶只能查看的。java

很容易想到一種作法就是:VO(View Object) 新增 Boolean 字段,對於 A、B 兩種內容,組裝 VO 的時候 A 的該字段設爲 false,B 的該字段設爲 true,經過 MVC 的 model 和 view 交互時對它做個判斷,頁面的區分渲染就能夠實現了。spring

可是,在測試的時候,他發現一個奇怪的現象,就是這個新增的字段值居然是 null,以至於根本沒法做判斷了。咱們先看下 VO 的代碼:bash

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CommonVo {
    private Long id;
    private Integer status;
    @Builder.Default private Boolean readOnly = false; // 是否只讀
}    
複製代碼

能夠看到,在 CommonVO 中新增了名爲 readOnly 的字段,並經過 lombok 註解 @Builder.Default 給它設置默認值爲 false,並且這個 VO 類上也加了 lombok 註解,能夠說包羅萬象,乍一看沒有任何問題,可爲何不行呢?maven

咱們再看看組裝 VO 的時候,是怎麼實例化對象的:函數

CommonVO vo = new CommonVO();
複製代碼

哦,原來是用這種傳統的實例化方式啊,那 new 一個無參構造函數,默認值就丟失了?測試

緣由

咱們直接編譯一下 java 文件,看看 CommonVO 的 class 文件裏面的無參構造函數是怎樣的:ui

public CommonVo() {
}
複製代碼

無參構造函數體內居然沒有 this.readOnly = false; 這一行代碼,那顯而易見地,默認值根本就不會生效嘛。this

而後再往下看,確定是有一個 Builder 的構建器,沒錯在這裏。爲了劃重點,我把 class 內容拷貝成文本後加了三個註解,代碼以下:編碼

public static class CommonVOBuilder {
    private Long id;
    private Integer status;
    private boolean readOnly$set;  // 重點關注①
    private Boolean readOnly;

    CommonVOBuilder() {
    }

    public CommonVO.CommonVOBuilder id(final Long id) {
        this.id = id;
        return this;
    }

    public CommonVO.CommonVOBuilder status(final Integer status) {
        this.status = status;
        return this;
    }

    public CommonVO.CommonVOBuilder readOnly(final Boolean readOnly) {
        this.readOnly = readOnly;
        this.readOnly$set = true;  // 重點關注②
        return this;
    }
	// 重點關注③
    public CommonVO build() {
        return new CommonVO(this.id, this.status, this.readOnly$set ? this.readOnly : CommonVO.$default$readOnly());
    }

    public String toString() {
        return "CommonVO.CommonVOBuilder(id=" + this.id + ", status=" + this.status + ", readOnly=" + this.readOnly + ")";
    }
}
複製代碼

能夠看到,經過 lombok 編譯後生成的 CommonVOBuilder 類會多出一個 readOnly$set 字段,這個字段的做用就是用來判斷是否設置成默認值。譬如,在對象實例化的時候,若是設置了 readOnly 的值爲 true,那麼readOnly$set 就會被設爲 true,調用 build() 方法以後,就會有一個三目運算符運算來決定它應該爲 true 而不是默認值 false。這裏的 CommonVO.$default$readOnly() 方法體內就一行代碼,返回默認值:spa

private static Boolean $default$readOnly() {
    return false; // 這個false就是定義readOnly設置的默認值
}
複製代碼

若是咱們從 POJO 的定義加上 lombok 註解,到對象實例化都用 lombok 的同一套風格來行事,確定就不會出這岔子。

解決方案

那就直接把

CommonVO vo = new CommonVO();
複製代碼

改成

CommonVO vo = CommonVO().builder().build();
複製代碼

嗯,很是好!這是最規範的寫法,lombok 官網也推薦。

可是現實狀況是:因爲歷史緣由,項目中好多處都是直接 new 的形式來建立的,若是都按這種方式改,一是怕改漏了,二是怕改出問題。

有沒有一種折中的辦法,默認值不管是經過 new 方式仍是 Builder().build() 方式都能正常使用呢?

好吧,既然要這種騷操做,那就再來一波探索。

首先一個問題就是:爲何 lombok 的無參構造函數沒有幫咱們設置默認值?

我看了下項目的 pom.xml 裏面 lombok 的 dependency 是這樣的:

沒指定 version,再往父依賴找:

原來依賴的是 spring boot,在這個 pox 文件往上翻找查到具體版本:

而後我搜索了一下 maven 倉庫,目前最高的是 1.18.8,抱着嘗試的心態直接指定 lombok 的 version 爲最新版,編譯:

package com.example.demo.mock;
import com.example.demo.vo.CommonVO;
/**
 * @author Jessehuang
 */
public class LombokDefaultValTest {
    public static void main(String[] args) {
        CommonVO vo = new CommonVO();
        System.out.println(vo);
        CommonVO vo2 = CommonVO.builder().build();
        System.out.println(vo2);
    }
}
複製代碼

結果:

CommonVO(id=null, status=null, readOnly=false)
CommonVO(id=null, status=null, readOnly=false)
複製代碼

OK,可行!而後出於好奇心,我想知道究竟是哪一個版本開始支持的。多嘗試了幾個,發現 1.18.2 這個版本修正了這個問題,若是不信你能夠把 lombok 的依賴指定爲 1.18.2,再編譯看看 class 文件就知道了。

另外,lombok 的 GitHub 的 issues 在2017年就有人提出這個問題,一年以後才得以修正。

若是不給 lombok 指定版本,仍是依賴 spring boot 幫你指定,那必須把 spring boot 升級到 v2.1.0.M2 版本及以上才行,你能夠在 spring boot 的 GitHub Releases 發版流水線的 Dependency upgrades 看到這個升級。

總結

總而言之,咱們經過升級 lombok 版本的方式解決了默認值爲 null 的問題。

其實 lombok 在幫咱們減小 POJO 冗餘編碼的同時,也給咱們帶來了一些困擾。好比首字母小寫第二個字母大寫的命名方式就會形成 Jackson 失敗問題、低版本默認值經過 new 方式初始化會爲 null 的問題。

建議在編碼的時候,不要交叉着使用上面兩種實例化方式,這個必需要在團隊中達成共識。

相關文章
相關標籤/搜索