news 2026/6/10 13:01:25

踩坑!Lombok @Builder和@NoArgsConstructor一起用,默认值居然失效了

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
踩坑!Lombok @Builder和@NoArgsConstructor一起用,默认值居然失效了

踩坑!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的小陷阱,以后写代码少走点弯路~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 12:26:32

人工智能毕业设计新颖的方向帮助

1 引言 毕业设计是大家学习生涯的最重要的里程碑,它不仅是对四年所学知识的综合运用,更是展示个人技术能力和创新思维的重要过程。选择一个合适的毕业设计题目至关重要,它应该既能体现你的专业能力,又能满足实际应用需求&#xf…

作者头像 李华
网站建设 2026/5/29 3:39:25

为什么越来越多前端选择 XinServer 做后端?

为什么越来越多前端选择 XinServer 做后端? 最近跟几个做前端的朋友聊天,发现一个挺有意思的现象:以前他们一提到要搞个带后端的小项目就头疼,现在却一个个自己就能把后台接口、用户管理这些事儿给搞定了。问他们怎么突然变“全栈…

作者头像 李华
网站建设 2026/6/10 12:05:40

以死亡为边界条件,倒推生命优先级的知识体系

以死亡为边界条件,进行生命优先级的倒推,是哲学与实践的终极结合。它并非关于死亡本身,而是利用“有限性”这一最根本的现实,作为校准生命方向的终极工具。 核心理念:死亡不是终点,而是最严厉的人生编辑 将…

作者头像 李华
网站建设 2026/5/30 17:04:16

AI教材编写新突破!低查重AI写教材工具,高效打造专业教材!

在写教材的过程中遇到的问题及 AI 工具的作用 在写教材的过程中,总是能精确踩到“慢节奏”的各种坑。框架和材料虽然已准备齐全,但内容书写却总是卡壳——一句话反复修改半个小时,依旧觉得不够合适;章节之间的过渡语句&#xff0…

作者头像 李华
网站建设 2026/6/9 16:37:28

确定性随机下的隐语义引擎:矩阵分解组件的深度设计与工程实践

好的,收到您的需求。我将以随机种子 1769990400070 为灵感起点(暗示一种对确定性与随机性交织的思考),围绕“矩阵分解组件”这一选题,为您撰写一篇兼具深度、新颖性和实用性的技术文章。文章将避开常见的“推荐系统入门…

作者头像 李华