Java record承载请求对象时的校验与默认值处理
来源:17golang原创
时间:2026-09-20 13:23:51 439浏览 收藏
把 HTTP 请求直接映射成 Java record,最容易踩的坑不是语法,而是把“缺省值”“非法值”和“调用方仍能修改集合”混在一起。我的处理方式是:紧凑构造器只负责建立对象不变量,能安全修复的空值在这里规范化,业务场景默认值交给工厂方法,集合字段统一做不可变拷贝。
官方资料:https://docs.oracle.com/en/java/javase/22/language/records.html
record 的规范构造器会在组件写入前接住参数,因此请求对象可以在一个边界内完成非空校验、默认值处理和格式收敛;但它不是深度不可变容器,嵌套的可变对象仍要单独处理。
先把三类输入分开
请求 DTO 通常同时包含三类字段。用户必须提供但不能修复的值,例如订单号为空,应直接抛出参数异常;可以接受缺省的值,例如页码为空,可以规范化为 1;集合和嵌套对象则要考虑所有权,不能因为 record 自带 final 组件就认为内容不会改变。
| 输入情况 | 建议 | 原因 |
|---|---|---|
| 必填字符串为空 | 拒绝并给出字段名 | 避免错误延迟到业务层 |
| 可选页码为 null | 规范化为稳定默认值 | 调用方无需重复写默认逻辑 |
| List 由外部传入 | List.copyOf | 切断后续 add/remove 影响 |
把校验和默认值放进紧凑构造器
紧凑构造器省略了参数列表和字段赋值,适合集中表达输入边界。下面的默认页码是接口约定,不是 Java 的“无参构造器默认值”;record 不会自动提供传统意义上的无参构造器。
import java.util.List; import java.util.Objects; public record SearchRequest(String keyword, Integer page, Listfields) { public SearchRequest { // 必填字段无法修复,尽早拒绝并保留字段语义。 keyword = Objects.requireNonNull(keyword, "keyword"); if (keyword.isBlank()) { throw new IllegalArgumentException("keyword 不能为空"); } // 可选页码统一落到接口约定的起始页,避免业务层重复判断。 page = page == null ? 1 : page; if (page

这里不要在构造器里悄悄吞掉所有异常。空白关键字和小于 1 的页码属于调用契约错误,应让错误响应能定位字段;只有“未传页码”这种明确的缺省状态才适合自动补值。
集合字段要做防御性拷贝
record 的组件字段是 private final,但如果组件类型是 ArrayList,final 只限制引用重新指向,不能限制列表内部变化。List.copyOf 会生成不可变视图语义的副本,并且拒绝 null 元素;如果请求还包含可变的嵌套对象,则需要在嵌套类型自己的构造边界继续复制。
var source = new java.util.ArrayList(); source.add("title"); var request = new SearchRequest("java record", 1, source); // 构造后修改来源列表,不应改变 request.fields() 的内容。 source.add("price"); System.out.println(request.fields()); // 仍然只有 title // 不要把可变集合直接返回给下游继续修改。 // request.fields().add("id") 会抛出 UnsupportedOperationException。

业务默认值交给工厂方法
构造器默认值解决的是字段级缺省,业务默认值往往还依赖租户、用户权限或接口版本,这时放在静态工厂方法更清楚。构造器保持“给定参数必须满足不变量”,工厂方法负责把场景信息整理成这些参数。
public static SearchRequest firstPage(String keyword) {
// 场景方法表达“第一页查询”,构造器仍是唯一校验边界。
return new SearchRequest(keyword, 1, List.of("title", "summary"));
}
如果框架通过 JSON 反序列化调用 canonical constructor,构造器校验仍会生效;但字段缺失究竟变成 null 还是被框架提前拒绝,取决于绑定配置。不要只在控制器参数注解里写规则,却让其他入口绕过同一套不变量。
落地前的检查清单
- 必填字段是否用明确异常拒绝,而不是悄悄替换成空字符串?
- 默认值是否属于字段契约,还是依赖租户和权限的业务决策?
- 集合、数组、日期和嵌套对象是否仍然能从对象外部被修改?
- 序列化、单元测试和错误响应是否都经过同一个构造边界?
record 让请求对象更短,但不会替你决定哪些输入可修复。把不变量集中到紧凑构造器,再用工厂方法表达场景默认值,通常能同时得到更清晰的错误边界和更可靠的不可变语义。
常见问题
record 能不能写无参构造器?
不能把传统无参构造器当作 record 的默认入口。需要默认值时,使用带明确语义的静态工厂方法,或声明完整的 canonical constructor。
List.copyOf 是深拷贝吗?
不是。它切断集合容器本身的修改关系,但集合元素若是可变对象,元素内部状态仍可能被改变;这类嵌套对象要继续设计自己的不可变边界。
-
文章 · java教程 | 1星期前 | Java · 异常处理 · 资源管理 · java try-with-resources AutoCloseable close suppressed exception501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
232 收藏
-
265 收藏
-
451 收藏
-
451 收藏
-
378 收藏
-
365 收藏
-
346 收藏
-
118 收藏
-
383 收藏
-
306 收藏
-
文章 · java教程 | 16小时前 | Java教程 · CompletableFuture · Java HttpClient Java异步请求超时 CompletableFuture异常处理 HttpTimeoutException sendAsync异常读取278 收藏
-
448 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习