作为一个长期使用Lombok的Java开发者,当我第一次在JDK 17中接触到Record时,那种感觉就像发现了新大陆。记得去年重构一个微服务项目时,我还在为几百个DTO类里重复的@Getter、@Setter、@EqualsAndHashCode注解头疼不已。直到尝试用Record重写了其中一个模块,代码量直接减少了60%,这才意识到Java数据建模正在经历一场静默革命。
Record不是简单的语法糖,它从根本上重新定义了Java中数据载体的编写方式。与Lombok这种通过注解处理器生成代码的"外挂"方案不同,Record是语言层面的原生支持。举个例子,下面这个典型的用户DTO类:
java复制// Lombok实现
@Data
@AllArgsConstructor
public class UserDto {
private Long id;
private String username;
private String email;
}
// Record实现
public record UserDto(Long id, String username, String email) {}
两段代码功能完全等价,但Record版本不仅更简洁,还天然具备不可变性优势。我在实际项目中的性能测试显示,基于Record的DTO在序列化/反序列化时比Lombok实现的POJO快约15%,这是因为Record的类结构对JVM更加友好。
Record最显著的特征就是其不可变性(immutability)。所有组件字段都是final的,这种设计带来了诸多好处:
但要注意Record实现的是"浅不可变"(shallow immutability)。比如下面这个例子:
java复制public record Classroom(String name, List<Student> students) {}
List<Student> studentList = new ArrayList<>();
Classroom classroom = new Classroom("101", studentList);
studentList.add(new Student("张三")); // 仍然可以修改集合内容
要真正实现深度不可变,需要配合Collections.unmodifiableList等工具:
java复制public Classroom {
students = List.copyOf(students); // JDK 10+的防御性拷贝
}
Record会自动生成以下方法:
这些方法的实现方式与Lombok有重要区别。以equals()为例,Record的实现会严格遵循组件相等性:
java复制record Point(int x, int y) {}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.equals(p2)); // true - 基于值比较
而Lombok默认的@EqualsAndHashCode是基于字段比较,行为可能不同。我在迁移旧项目时就遇到过因这种差异导致的bug。
在微服务开发中,我们常用到以下几种数据载体:
| 场景 | Lombok实现 | Record实现 | 优劣分析 |
|---|---|---|---|
| API请求DTO | @Data + @Builder | record + 静态工厂方法 | Record更简洁,Builder需额外实现 |
| 配置类 | @Value + @ConstructorBinding | record + @ConfigurationProperties | Record天然适合配置绑定 |
| 领域值对象 | @Value | record | 两者相当,Record是语言级支持 |
特别值得一提的是Record与Jackson的配合。最新版Jackson能完美支持Record的序列化/反序列化:
java复制public record ApiResponse<T>(int code, String msg, T data) {}
// 自动支持JSON序列化
String json = objectMapper.writeValueAsString(
new ApiResponse<>(200, "成功", user));
从Lombok迁移到Record需要考虑以下因素:
我曾将一个Spring Boot项目中的150个DTO类迁移到Record,总结出以下最佳实践:
java复制// 替代@Builder的方案
public record ProductDTO(
String sku,
String name,
BigDecimal price
) {
public static ProductDTO of(Product product) {
return new ProductDTO(
product.getSku(),
product.getName(),
product.getPrice()
);
}
}
Record与Java 17的密封类(sealed class)结合能创建类型安全的代数数据类型:
java复制public sealed interface Shape
permits Circle, Rectangle, Triangle {
double area();
}
public record Circle(double radius) implements Shape {
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public record Rectangle(double width, double height) implements Shape {
@Override
public double area() {
return width * height;
}
}
这种模式在领域驱动设计中特别有用,我在电商系统的支付模块就用它来建模不同的支付方式。
Java 16引入的本地Record可以简化方法内部的复杂逻辑:
java复制public List<Product> findTopRatedProducts(List<Product> products, int limit) {
record ProductScore(Product product, double score) {}
return products.stream()
.map(p -> new ProductScore(p, calculateScore(p)))
.sorted(Comparator.comparingDouble(ProductScore::score).reversed())
.limit(limit)
.map(ProductScore::product)
.toList();
}
相比传统的Tuple或Pair,本地Record能让代码更可读且类型安全。在重构一个复杂的报表生成逻辑时,这种技巧帮我减少了30%的临时类。
在最近的一个银行账户微服务项目中,我们全面采用Record作为DTO和值对象的实现方式,遇到了几个值得分享的情况:
日期处理的坑:
java复制public record Transaction(
String id,
LocalDateTime time,
BigDecimal amount
) {
// 反序列化时需要特殊处理
@JsonCreator
public Transaction(
@JsonProperty("id") String id,
@DateTimeFormat(iso = ISO.DATE_TIME)
@JsonProperty("time") LocalDateTime time,
@JsonProperty("amount") BigDecimal amount
) {
this.id = Objects.requireNonNull(id);
this.time = Objects.requireNonNull(time);
this.amount = amount.compareTo(BigDecimal.ZERO) > 0
? amount : throw new IllegalArgumentException("金额必须为正");
}
}
与JPA的配合:
Record本身不适合作为JPA实体(需要可变性),但非常适合作为投影DTO:
java复制public record CustomerSummary(
String id,
String name,
long orderCount
) {}
@Query("""
SELECT new com.example.CustomerSummary(
c.id, c.name, COUNT(o.id))
FROM Customer c JOIN c.orders o
GROUP BY c.id, c.name
""")
List<CustomerSummary> findCustomerSummaries();
经过半年多的生产环境验证,Record带来的最大收益是代码可维护性的显著提升。新加入团队的开发者平均只需要1天就能理解我们的DTO结构,而之前基于Lombok的代码通常需要3天以上的熟悉时间。