在 Spring Boot 3 中使用 Java Record

Record 在 Java 14 中作为预览功能引入,并在 JDK 16 中成为标准功能。Record 是不可变数据类(data class)的简洁表示。

在使用 Record 之前,我们通常是这样创建不可变 class 的。

import java.util.Objects;

class Person {
    private final Long id;
    private final String name;

    public Person(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Person person = (Person) o;
        return Objects.equals(id, person.id) && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        int result = id != null ? id.hashCode() : 0;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Person{" + "id=" + id + ", name='" + name + '\'' + '}';
    }
}

虽然大多数情况下我们通常使用 IDE 生成或使用 Lombok 生成 equals()hashCode()toString(),但这会产生更多相同且枯燥的代码。同样的 Person class 可以写成如下的 Record

public record Person(Long id, String name){ }

就这样,这会为 Record 自动生成equals()hashCode()toString() 方法。但请注意,getter 并不遵循通常的 getId()getName() 模式。相反,它会生成 person.id()person.name() accessor 方法。

在 SpringBoot 3 中使用 Java Record

Spring Boot 3 于 2022 年 11 月 24 日发布,要求 Java 17 以上。让我们看看如何在 SpringBoot 中使用 Record。

绑定 Application Properties

如果你熟悉 SpringBoot application properties 与类的绑定,像这样:

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@ConfigurationProperties(prefix = "app")
@Validated
class ApplicationProperties {
    @Min(1)
    @Max(100)
    private int pageSize;
  
    public int getPageSize() {
        return pageSize;
    }
    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }
}

SpringBoot 2.2.0 引入了对 ConstructorBinding 的支持,可用于将 properties 绑定到不可变类。

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.validation.annotation.Validated;

@ConfigurationProperties(prefix = "app")
@Validated
public class ApplicationProperties {
    @Min(1)
    @Max(100)
    private final int pageSize;

    @ConstructorBinding
    public ApplicationProperties(int pageSize) {
        this.pageSize = pageSize;
    }
    public int getPageSize() {
        return pageSize;
    }
}

你很可能希望 ApplicationProperties对象是不可变的,Record 是一个不错的选择。因此,我们可以将 ApplicationProperties 创建为一个 Record,如下:

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@ConfigurationProperties(prefix = "app")
@Validated
public record ApplicationProperties(
        @Min(1)
        @Max(100)
        int pageSize
) {
}

这样做非常简洁,还能防止意外修改配置属性值。

绑定 Http 请求体/响应体

我们通常创建带有 setter 和 getter 的 DTO 类来绑定传入的 HTTP 请求体。

SpringBoot 默认使用 Jackson 库反序列化JSON请求体为Java对象,或者序列化Java对象为JSON响应体。Jackson 2.12 引入了对 Record 的支持。因此,我们可以使用 Record 绑定传入的请求体,并将 Record 作为响应返回。

下面是一个应用了 Bean Validation 约束的 Record:

import jakarta.validation.constraints.NotEmpty;
import java.time.Instant;

public record Bookmark(
        Long id,
        @NotEmpty(message = "Title is mandatory")
        String title,
        @NotEmpty(message = "Url is mandatory")
        String url,
        Instant createdAt) {
}

我们可以在 SpringMVC Controller 中使用 Bookmark record,如下所示:

import com.sivalabs.bookmarks.domain.Bookmark;
import com.sivalabs.bookmarks.domain.BookmarkService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.Instant;

@RestController
@RequestMapping("/api/bookmarks")
@RequiredArgsConstructor
public class BookmarkController {
    private final BookmarkService service;

    @PostMapping
    public ResponseEntity<Bookmark> save(@Valid @RequestBody Bookmark bookmark) {
        Bookmark savedBookmark = service.save(bookmark);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedBookmark);
    }
}

上面的代码将 JSON 请求体绑定到 Bookmark record,并将 Bookmark record 作为响应体返回,Jackson 将把响应体对象转换为 JSON 格式。

总结

Java record 非常有用,能以非常简洁的语法为不可变数据对象建模。然而,Record 并不是适用于所有情况的灵丹妙药。在某些情况下,普通 class 可能比 Record 更合适。


参考:https://www.sivalabs.in/using-java-records-with-spring-boot-3/