核心内容摘要
红桃17·c18:开启你的掌上数字新纪元
踩坑Lombok Builder和NoArgsConstructor一起用默认值居然失效了前几天维护一个老接口新服务对接的时候突然报空指针异常排查了半天最后发现居然是Lombok注解用错了导致的。
说真的Lombok这东西平时用着是真方便Data、Builder一套注解甩上去getter、setter、构造器全不用自己写省了好多代码。
但也正因为太方便有时候忽略了注解之间的搭配问题很容易踩坑。
这次踩的坑就是Builder、NoArgsConstructor、AllArgsConstructor和Data一起用导致DTO里的默认值失效进而引发了空指针。
今天就把这个坑整理出来看看有没有朋友和我一样也栽过这个跟头。
先上我最开始写的DTO代码看着其实没什么问题importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;DataBuilderNoArgsConstructorAllArgsConstructorpublicclassReqDto{// 给field1设置了默认值trueprivateBooleanfield1true;privateStringfield2;privateStringfield3;}接口里的判断逻辑也很简单就是用field1作为条件之一判断field2和field3不为空publicvoiddoBusiness(ReqDtoreqDto){// 这里报了空指针reqDto.getField1()居然是nullif(reqDto.getField1()reqDto.getField2()!nullreqDto.getField3()!null){// 执行业务逻辑System.out.println(业务逻辑执行成功);}}当时看到空指针报错我第一反应是新服务对接时没给field1传值。
但找对接的同事确认后他们说field1是可选参数没传的时候就用默认值。
我就纳闷了我明明给field1设置了默认值true就算没传值也应该是true才对怎么会是null呢后来我把DTO的class文件反编译了一下看完瞬间就明白了问题出在Lombok的Builder注解上。
先给大家看一下上面那套注解反编译后的DTO代码是什么样的关键部分截取publicclassReqDto{privateBooleanfield1true;privateStringfield2;privateStringfield3;// NoArgsConstructor生成的无参构造器publicReqDto(){}// AllArgsConstructor生成的全参构造器publicReqDto(Booleanfield1,Stringfield2,Stringfield
{this.field1field1;this.field2field2;this.field3field3;}// Builder生成的builder内部类和相关方法publicstaticReqDto.ReqDtoBuilderbuilder(){returnnewReqDto.ReqDtoBuilder();}publicstaticclassReqDtoBuilder{privateBooleanfield1;privateStringfield2;privateStringfield3;ReqDtoBuilder(){}publicReqDto.ReqDtoBuilderfield1(Booleanfield
{this.field1field1;returnthis;}publicReqDto.ReqDtoBuilderfield2(Stringfield
{this.field2field2;returnthis;}publicReqDto.ReqDtoBuilderfield3(Stringfield
{this.field3field3;returnthis;}publicReqDtobuild(){returnnewReqDto(this.field1,this.field2,this.field
;}}// 下面是Data生成的getter、setter方法省略...}重点看builder的build方法它调用的是全参构造器new ReqDto(this.field1, this.field2, this.field
。
而builder内部类里的field1是没有设置默认值的默认就是null。
当我们用builder创建对象又没有给field1赋值的时候builder里的field1就是null然后通过全参构造器传给DTO的field1直接覆盖了我们在DTO里设置的默认值true。
这就是问题的根源我们以为给field1设置了默认值就万事大吉但Builder注解生成的代码直接把这个默认值给“覆盖”掉了。
举个例子当我们用builder创建对象只传field2和field3不给field1传值ReqDtoreqDtoReqDto.builder().field2(test
.field3(test
.build();这时reqDto.getField1()的值不是我们预期的true而是null。
接口里用这个null去做判断自然就报空指针了。
找到问题之后解决办法就很简单了有两种常用的方式根据自己的场景选就行。
第一种方式给Builder的field设置默认值推荐不用改其他注解直接在Builder注解里给需要默认值的字段设置默认值这样builder创建对象时就算不赋值也会用我们设置的默认值。
importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;Data// 给field1设置默认值true覆盖builder内部的null默认值Builder(field1true)NoArgsConstructorAllArgsConstructorpublicclassReqDto{privateBooleanfield1true;privateStringfield2;privateStringfield3;}这样修改后再用builder创建对象不给field1赋值field1就会是true和我们预期的一致。
第二种方式不用AllArgsConstructor手动写全参构造器灵活度高如果不想用Builder的field默认值设置也可以去掉AllArgsConstructor注解自己手动写全参构造器在构造器里给field1设置默认值。
importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;DataBuilderNoArgsConstructor// 去掉AllArgsConstructor手动写全参构造器publicclassReqDto{privateBooleanfield1true;privateStringfield2;privateStringfield3;// 手动写全参构造器给field1设置默认值publicReqDto(Booleanfield1,Stringfield2,Stringfield
{this.field1field1null?true:field1;this.field2field2;this.field3field3;}}这种方式的好处是就算field1传了null也能通过构造器的判断把它设为true避免空指针。
这里还有个小细节很多人可能会忽略如果去掉AllArgsConstructor只保留Builder和NoArgsConstructorLombok会自动生成一个全参构造器吗答案是不会。
Builder注解本身不会生成全参构造器它只是依赖全参构造器来创建对象。
如果我们不手动写也不用AllArgsConstructor编译的时候就会报错提示找不到全参构造器。
再补充一个点为什么我们给DTO的field1设置了默认值还是会被覆盖因为Java的赋值顺序是先执行字段的默认值赋值再执行构造器里的赋值。
我们用builder创建对象时调用的是全参构造器构造器里的this.field1 field1builder里的null会覆盖掉字段本身的默认值true。
就像下面这段简单的代码执行完之后field1的值是null而不是truepublicclassTest{privateBooleanfield1true;publicTest(Booleanfield
{this.field1field1;}publicstaticvoidmain(String[]args){TesttestnewTest(null);System.out.println(test.field
;// 输出null}}道理是一样的Lombok生成的代码本质上也是这样的逻辑只是我们平时看不到而已。
最后再
总结一下这个坑用大白话讲就是当Builder和NoArgsConstructor、AllArgsConstructor一起使用时Builder生成的builder类其内部字段默认是nullbuild方法会调用全参构造器用builder里的null覆盖DTO字段的默认值导致默认值失效。
解决办法就两种要么用Builder(field 默认值)给字段设置默认值要么手动写全参构造器在构造器里处理默认值。
其实Lombok的坑还有不少但大多都是因为对注解的底层实现不了解盲目搭配使用导致的。
平时用的时候多留意一下注解之间的兼容性必要的时候反编译看一下生成的代码就能避免很多不必要的麻烦。
希望我这个踩坑经历能帮大家避开这个Lombok的小陷阱以后写代码少走点弯路