一次 lombok 默認(rèn)值為 null 的排查實(shí)踐

背景

這周的某個(gè)晚上,同事喊我過(guò)去看個(gè)問(wèn)題,大概是這樣的:為了滿足新的業(yè)務(wù)需求,對(duì)于A、B兩種不同的內(nèi)容,在頁(yè)面呈現(xiàn)上必須區(qū)分出兩套規(guī)則,一套是用戶可以進(jìn)行修改和刪除的,一套是用戶只能查看的。

很容易想到一種做法就是:VO(View Object) 新增 Boolean 字段,對(duì)于 A、B 兩種內(nèi)容,組裝 VO 的時(shí)候 A 的該字段設(shè)為 false,B 的該字段設(shè)為 true,通過(guò) MVC 的 model 和 view 交互時(shí)對(duì)它作個(gè)判斷,頁(yè)面的區(qū)分渲染就可以實(shí)現(xiàn)了。

但是,在測(cè)試的時(shí)候,他發(fā)現(xiàn)一個(gè)奇怪的現(xiàn)象,就是這個(gè)新增的字段值竟然是 null,以致于根本無(wú)法作判斷了。我們先看下 VO 的代碼:

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

可以看到,在 CommonVO 中新增了名為 readOnly 的字段,并通過(guò) lombok 注解 @Builder.Default 給它設(shè)置默認(rèn)值為 false,而且這個(gè) VO 類上也加了 lombok 注解,可以說(shuō)一應(yīng)俱全,乍一看沒(méi)有任何問(wèn)題,可為什么不行呢?

我們?cè)倏纯唇M裝 VO 的時(shí)候,是怎么實(shí)例化對(duì)象的:

CommonVO vo = new CommonVO();

哦,原來(lái)是用這種傳統(tǒng)的實(shí)例化方式啊,那 new 一個(gè)無(wú)參構(gòu)造函數(shù),默認(rèn)值就丟失了?

原因

我們直接編譯一下 java 文件,看看 CommonVO 的 class 文件里面的無(wú)參構(gòu)造函數(shù)是怎樣的:

public CommonVo() {
}

無(wú)參構(gòu)造函數(shù)體內(nèi)竟然沒(méi)有 this.readOnly = false; 這一行代碼,那顯而易見(jiàn)地,默認(rèn)值根本就不會(huì)生效嘛。

然后再往下看,肯定是有一個(gè) Builder 的構(gòu)建器,沒(méi)錯(cuò)在這里。為了劃重點(diǎn),我把 class 內(nèi)容拷貝成文本后加了三個(gè)注解,代碼如下:

public static class CommonVOBuilder {
    private Long id;
    private Integer status;
    private boolean readOnly$set;  // 重點(diǎn)關(guān)注①
    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;  // 重點(diǎn)關(guān)注②
        return this;
    }
    // 重點(diǎn)關(guān)注③
    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 + ")";
    }
}

可以看到,通過(guò) lombok 編譯后生成的 CommonVOBuilder 類會(huì)多出一個(gè) readOnly$set 字段,這個(gè)字段的作用就是用來(lái)判斷是否設(shè)置成默認(rèn)值。譬如,在對(duì)象實(shí)例化的時(shí)候,如果設(shè)置了 readOnly 的值為 true,那么readOnly$set 就會(huì)被設(shè)為 true,調(diào)用 build() 方法之后,就會(huì)有一個(gè)三目運(yùn)算符運(yùn)算來(lái)決定它應(yīng)該為 true 而不是默認(rèn)值 false。這里的 CommonVO.$default$readOnly() 方法體內(nèi)就一行代碼,返回默認(rèn)值:

private static Boolean $default$readOnly() {
    return false; // 這個(gè)false就是定義readOnly設(shè)置的默認(rèn)值
}

如果我們從 POJO 的定義加上 lombok 注解,到對(duì)象實(shí)例化都用 lombok 的同一套風(fēng)格來(lái)行事,肯定就不會(huì)出這岔子。

解決方案

那就直接把

CommonVO vo = new CommonVO();

改為

CommonVO vo = CommonVO().builder().build();

嗯,非常好!這是最規(guī)范的寫(xiě)法,lombok 官網(wǎng)也推薦。

但是現(xiàn)實(shí)情況是:由于歷史原因,項(xiàng)目中好多處都是直接 new 的形式來(lái)創(chuàng)建的,如果都按這種方式改,一是怕改漏了,二是怕改出問(wèn)題。

有沒(méi)有一種折中的辦法,默認(rèn)值無(wú)論是通過(guò) new 方式還是 Builder().build() 方式都能正常使用呢?

好吧,既然要這種騷操作,那就再來(lái)一波探索。

首先一個(gè)問(wèn)題就是:為什么 lombok 的無(wú)參構(gòu)造函數(shù)沒(méi)有幫我們?cè)O(shè)置默認(rèn)值?

我看了下項(xiàng)目的 pom.xml 里面 lombok 的 dependency 是這樣的:

image

沒(méi)指定 version,再往父依賴找:

image

原來(lái)依賴的是 spring boot,在這個(gè) pox 文件往上翻找查到具體版本:

image

然后我搜索了一下 maven 倉(cāng)庫(kù),目前最高的是 1.18.8,抱著嘗試的心態(tài)直接指定 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);
    }
}

結(jié)果:

CommonVO(id=null, status=null, readOnly=false)
CommonVO(id=null, status=null, readOnly=false)

OK,可行!然后出于好奇心,我想知道到底是哪個(gè)版本開(kāi)始支持的。多嘗試了幾個(gè),發(fā)現(xiàn) 1.18.2 這個(gè)版本修正了這個(gè)問(wèn)題,如果不信你可以把 lombok 的依賴指定為 1.18.2,再編譯看看 class 文件就知道了。

另外,lombok 的 GitHub 的 issues 在2017年就有人提出這個(gè)問(wèn)題,一年之后才得以修正。

如果不給 lombok 指定版本,還是依賴 spring boot 幫你指定,那必須把 spring boot 升級(jí)到 v2.1.0.M2 版本及以上才行,你可以在 spring boot 的 GitHub Releases 發(fā)版流水線的 Dependency upgrades 看到這個(gè)升級(jí)。

總結(jié)

總而言之,我們通過(guò)升級(jí) lombok 版本的方式解決了默認(rèn)值為 null 的問(wèn)題。

其實(shí) lombok 在幫我們減少 POJO 冗余編碼的同時(shí),也給我們帶來(lái)了一些困擾。比如首字母小寫(xiě)第二個(gè)字母大寫(xiě)的命名方式就會(huì)造成 Jackson 失敗問(wèn)題、低版本默認(rèn)值通過(guò) new 方式初始化會(huì)為 null 的問(wèn)題。

建議在編碼的時(shí)候,不要交叉著使用上面兩種實(shí)例化方式,這個(gè)必須要在團(tuán)隊(duì)中達(dá)成共識(shí)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容