2023年Q3,我们团队一个DTO加字段的PR,平均 review 时间从4小时涨到11小时。不是业务变复杂了,是样板代码的债务终于到期。
Java 开发者都懂这种痛。纸上流程干净得像瀑布:加字段、改映射、跑测试、合并。真进代码库,你会发现那个字段像触手怪——构造函数、建造器、序列化器、校验注解,最后还有一个谁都不想点开的工具类。
我们的 DTO 层就这样,从"有点啰嗦"滑向"明显超重"。
从 Lombok 到 Records,不是追新,是还债
先说清楚:换 Records 不是为了在简历上写"掌握 Java 14+ 新特性"。是某天我盯着一行 @Builder.Default 看了五分钟,突然想不起它当初为什么存在。
Lombok 是个好拐杖。2017 年我们引入它时,Getter/Setter 的噪音确实消失了。但拐杖用久了,你会忘记腿本来该怎么走。
我们的 DTO 逐渐长成这样:Immutable 用 @Value,可变对象用 @Data,建造器模式套 @Builder,再加上 Jackson 的注解、Hibernate Validator 的注解、自定义的 @Mapper 注解。一个 8 字段的 DTO,文件行数轻松破 80。
更隐蔽的问题是"隐形耦合"。Lombok 生成的代码不参与编译期检查,IDE 里跳转不到,重构时全靠搜索。有次我们改了一个字段名,@Builder 生成的全参构造器没跟着变,测试全绿,生产环境序列化报错——因为 Jackson 用的还是旧的字段名映射。
Records 的吸引力在这里:它是语言级特性,不是字节码魔术。编译器看得见,IDE 跟得上,反射也认。
迁移不是搬家,是断舍离
我们没搞大爆炸重构。选了一条最脏的业务线试点——订单域的 37 个 DTO,覆盖创建、查询、导出三种场景。
第一个发现:Records 的紧凑是真实的。public record OrderDTO(String id, BigDecimal amount, Instant createdAt) {} 这一行,等效于 Lombok 时代的 15 行:类声明、@Value、4 个字段、4 个 Getter、全参构造器、equals/hashCode/toString。
但省行数不是重点。重点是这些行为现在由 JVM 保证,而不是一个注解处理器在编译期偷偷改写 AST。
第二个发现:建造器模式必须放弃。Records 的字段是 final 的,且必须在构造时初始化。我们原来的 @Builder 链式调用写得很爽,OrderDTO.builder().id("123").amount(new BigDecimal("99.00")).build(),现在得改成 new OrderDTO("123", new BigDecimal("99.00"), Instant.now())。
团队里有人抵触:"参数一多,构造器调用可读性太差。"我们试了两种妥协:小 DTO 直接裸构造,大 DTO 手写静态工厂方法,按业务语义命名参数。比如 OrderDTO.forNewCart("123", "99.00") 比裸 new 好懂,又不依赖字节码生成。
第三个发现:Jackson 的兼容性比预期顺滑。ObjectMapper 注册 JavaTimeModule 后,Records 的序列化/反序列化开箱即用。唯一要改的是字段命名策略——我们原来用 @JsonProperty 覆盖驼峰,现在统一在 ObjectMapper 配置层解决,DTO 本身保持干净。
那些没写在 Release Note 里的成本
迁移完 37 个 DTO,我们算了笔账。
正面:DTO 文件平均行数从 67 降到 12。代码审查时, reviewer 能一眼看完数据结构和校验规则,不用再展开 Lombok 生成的折叠代码。
负面:MapStruct 的映射代码要重写。原来 Lombok 的 @Builder 支持 targetBuilder 模式,Records 没有建造器,只能改用 @Mapping 注解直接指定构造参数。17 个映射接口,每个多花了 20 分钟调整。
还有一个长期成本:团队培训。 junior 开发问得最多的是"Records 能继承吗"(不能)、"能加 Setter 吗"(不能)、"那业务需求要改字段怎么办"(新建实例)。这些问题 Lombok 时代不存在,因为 @Data 什么都能干,也什么都敢干。
我们的选择是:把 Records 的约束当成设计审查。如果某个 DTO 真的需要可变字段,说明它可能不是纯数据载体,该拆成状态机或者移到领域层。
试点三个月后,订单域的生产 bug 率没明显变化,但 DTO 相关的代码审查评论数从平均每 PR 8.3 条降到 2.1 条。大部分争议从"这个字段该不该加 @NonNull"变成了"这个字段该不该存在"。
现在回头看 Lombok
我们没卸载 Lombok。日志用的 @Slf4j、异常类的 @StandardException 还在服役。只是 DTO 这个场景,Records 的"少即是多"更贴合我们现在的需求。
有个细节挺有意思:JDK 21 的 Record Patterns 和 Switch 模式匹配,让我们的 DTO 消费代码又短了一截。原来 if (dto instanceof OrderDTO) 配强制转型,现在直接 case OrderDTO(String id, var amount, _) -> ...,解构和判断一步完成。
这是语言特性叠代的复利。2017 年我们选 Lombok,是因为 Java 8 的语法表达能力不够。2024 年语言本身追上来了,拐杖就该放回储藏室。
你们团队的 DTO 现在是什么状态?是 Records 全量替换,还是 Lombok 继续扛主力,或者两者混用、边界模糊?
热门跟贴