背景
這周的某個(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 是這樣的:

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

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

然后我搜索了一下 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í)。