Java Record真香现场:把DTO代码量砍半,但小心这五个坑

2026-09-01 0 358

上个月把项目里一个老模块的数据类从Lombok换成了Java原生的Record,本来只是试试水,结果代码量直接少了一半。当然也踩了几个不太明显的坑,尤其是和Hibernate、MyBatis配合的时候。今天拿一个实际开发中常见的用户注册场景,把Record怎么用、怎么避开坑说清楚。

说说为什么受够了Lombok

以前写一个DTO,你得加@Data@Builder@AllArgsConstructor这些注解。类本身可能就三四个字段,但代码看着一点都不清爽。最关键的是Lombok在编译期搞的“魔法”,遇到不同版本的JDK或者插件冲突,排查起来想骂人。而Java 16正式发布了Record,它就是为“只存数据”的类而生的,不用写一堆样板代码,构造函数、equals、hashCode、toString都自动生成。

举个例子,注册接口需要一个请求对象:用户名、手机号、密码。以前这么写:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RegisterRequest {
    private String username;
    private String phone;
    private String password;
}

现在用Record,一行就完事:

public record RegisterRequest(String username, String phone, String password) {}

看着是不是舒服多了?Record的字段默认是private final的,只有getter(像username()这样),没有setter。这种不可变性在数据传输时特别安全,不怕哪里不小心改掉了。

完整的案例:把注册接口改成Record

我们的Controller原来接收一个实体类,然后手动调用getter取字段:

@PostMapping("/register")
public Result register(@RequestBody RegisterRequest request) {
    // 取字段的方法以前是 request.getUsername()
    String username = request.getUsername();
    String phone = request.getPhone();
    String password = request.getPassword();
    // 调用业务层
    userService.register(username, phone, password);
}

改成Record之后,取字段的方法变成了request.username(),其他逻辑不用动。Idea会帮你自动替换,所以整体改造并不麻烦。

构造器校验参数,替代一切手动if

Record最爽的一点,是可以在官方推荐的“紧凑构造器”里做参数校验。格式非常简洁:

public record RegisterRequest(
        String username,
        String phone,
        String password
) {
    public RegisterRequest {
        if (username == null || username.isBlank()) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        if (phone.length() != 11) {
            throw new IllegalArgumentException("手机号格式不对");
        }
        if (password.length() < 6) {
            throw new IllegalArgumentException("密码至少6位");
        }
    }
}

注意,紧凑构造器里没有写this.username = username之类的赋值语句,因为Record会自动给你赋值,你只需要写校验逻辑。这样参数在创建对象时就被“锁死”了,不符合要求的对象根本创建不出来,业务层就不用再重复校验了。

嵌套Record:组装复杂的返回结构

很多时候我们需要在接口里嵌套返回一些数据。比如注册成功后要返回用户信息+token,用Record嵌套起来特别清晰:

public record RegisterResponse(
        UserInfo user,
        String token
) {
    public record UserInfo(
            Long id,
            String username,
            String phone,
            String avatar
    ) {}
}

在业务层构建这个对象时,不需要写构造器,直接这样:

RegisterResponse.UserInfo info = new RegisterResponse.UserInfo(
        user.getId(),
        user.getUsername(),
        user.getPhone(),
        user.getAvatar()
);
return new RegisterResponse(info, token);

代码是变短了,但刚开始我有个担心:Jackson能不能正常把Record序列化成JSON?后来实测Spring Boot 3.x自带的Jackson完全支持Record,不用额外配置。如果你用的还是Spring Boot 2.x,可能会遇到一点小问题,升级Jackson版本或者加一个jackson-databind-nullable即可。

和数据访问层打交道时遇到的坑

坑一:MyBatis-Plus 对Record的支持有点差

一开始我想直接把Record作为实体类映射数据库表,结果MyBatis-Plus玩不转。它默认利用无参构造函数创建对象,然后反射字段赋值。但Record没有无参构造器,所以直接报错。试了几个办法,最后放弃了,还是用传统POJO做数据库映射,只在接口出入参上用了Record。说实话,DTO层用Record够了,实体类保持原样更稳妥。

坑二:Hibernate的懒加载会报错

如果直接把Record用做JPA的实体类,绝对要出问题。Hibernate需要为实体类生成代理对象,但Record是final的,无法继承。所以一旦你用了@Entity标注在一个Record上,启动时就会直接抛异常。网上有人做了一些扩展,但生产环境建议别这么搞,老老实实用普通类做实体。

坑三:IDE自动生成的方法不一定有注释

Record会自动生成toString(),里边的输出格式是RegisterRequest[username=x, phone=y, password=z]。打印日志时可能会把敏感信息(比如密码)打出来。为了避免这种情况,我选择在Record里重写toString,只打印脱敏后的信息:

@Override
public String toString() {
    return "RegisterRequest{" +
            "username='" + username + ''' +
            ", phone='" + phone + ''' +
            ", password='***'" +
            '}';
}

虽然代码长了点,但日志安全很重要。

坑四:无法继承Record,也不好实现某些框架的接口

Record隐含地继承java.lang.Record,所以它不能再继承其他类。如果某个场景你需要让DTO继承一个父类(比如分页查询对象),Record就不合适了。另外,有些框架要求实现某些接口(比如Spring的AttributeModel),Record也可以实现接口,只是不能继承。

坑五:Builder和Record天然不兼容

Lombok的@Builder不能用在Record上。如果你习惯了构建者模式,刚开始会觉得有点憋屈。不过Record的构造器本来就列出了所有字段,参数一多可能会太啰嗦。作为折中,我有时候会在Record内部定义静态工厂方法,比如:

public static RegisterRequest of(String username, String phone, String password) {
    return new RegisterRequest(username, phone, password);
}

但为了简化,直接在构造器里传所有参数也够用了。

和JSON反序列化有关的小惊喜

Jackson在反序列化JSON到Record时,会自动使用构造器,同时会调用紧凑构造器里的校验逻辑。也就是说客户端传来的参数如果不符合要求,直接在这里就报错了。这是好事,不过我担心错误信息不够友好。后来我用了Spring的@ControllerAdvice统一捕获IllegalArgumentException并返回自定义消息,这样前端就能看到“手机号格式不对”这样的提示,而不是一堆堆栈信息。

总结一下什么情况下用Record

我个人的习惯是:接口的DTO、VO、查询参数对象,用Record非常舒服,因为它不可变、语义清晰、精简。但是数据库实体类、需要复杂继承体系的对象、需要用到MyBatis-Plus的LambdaUpdateWrapper的对象,还是继续用传统类。Record不是要完全替代所有POJO,它只是提供了一种更简洁的写法。

如果你还没用过Record,我建议在你项目里拿一个简单的接口试试:把请求参数类改成Record,然后让Controller层去接收,感受一下。等熟悉了之后再逐步扩大使用范围。踩坑的事我已经帮你踩过了,你避开上面那五个坑就行。

Java Record真香现场:把DTO代码量砍半,但小心这五个坑
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 java Java Record真香现场:把DTO代码量砍半,但小心这五个坑 https://www.taomawang.com/server/java/2679.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务