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