Using Java Records with Spring Boot

In this post, we’ll explore how Java records can be leveraged within a Spring Boot application to enhance its efficiency and readability. We will create a Spring Boot application and perform CRUD operations using the H2 database.

What are Java Records? 

Java records are a type of class designed for holding immutable data. They automatically provide implementations for methods like equals(), hashCode(), and toString(), significantly reducing boilerplate code. This makes them ideal for creating Data Transfer Objects (DTOs), entities, and other model classes in a Spring Boot application. 

Example of Java Record 

Let’s consider a simple example of a Java Record to represent a Person.

public record Person(String name, int age) {}

In this example, Person is a record with two fields: name and age. Java automatically provides the following for this record: 

A public constructor: Person(String name, int age) 

Public getter methods: name() and age() 

Implementations of equals(), hashCode(), and toString()

Using Getter Methods

One of the key features of records is that they provide implicit getter methods for accessing the fields. These methods are named after the fields themselves.

Here's how you can create an instance of Person and use its getter methods:

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);

        // Using getter methods
        String name = person.name();
        int age = person.age();

        System.out.println("Name: " + name);
        System.out.println("Age: " + age);
    }
}

Output:

Name: Alice
Age: 30

Key Advantages of Using Records in Spring Boot 

Reduced Boilerplate: Records automatically generate the constructor, getters, equals(), hashCode(), and toString() methods, significantly reducing the need for boilerplate code. 

Immutability: Records create immutable data structures by default, making them ideal for use cases where data integrity is critical. 

Clarity and Transparency: The intent of a record is clear – it is solely a carrier for its data. This transparency makes the code easier to read and maintain. 

Ease of Use: Developers can define a record in a single line of code, making it much simpler and more efficient to create data-holding classes.

Setting Up the Environment 

To begin, ensure that you have JDK 16 or higher and Spring Boot 3.0. You can use Spring Initializr to set up your project with dependencies like Spring Web, Spring Data JPA, and H2 Database. 

Project Structure 

Our Spring Boot project will consist of the following layers: 

Controller Layer: Handles HTTP requests and responses. 

Service Layer: Contains business logic. 

Repository Layer: Manages data persistence. 

Database: H2 in-memory database for simplicity. 

We'll create a simple application to manage User entities. 

Step 1: Define the Record for Domain 

First, let's define a User record which will be our domain entity.

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public record User(@Id Long id, String name, String email) {}

Here, we use Java Records to define the entity, making our entity definition concise and immutable.

Step 2: Repository Layer

Next, we create a repository interface for data access:

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {}

Spring Data JPA will provide the implementation, and we get basic CRUD operations out of the box.

Step 3: Service Layer

The UserService contains the business logic and interacts with the UserRepository. Now, let's define the service layer:

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User createUser(User user) {
        return userRepository.save(user);
    }

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    public User getUserById(Long id) {
        return userRepository.findById(id).orElseThrow();
    }

    public User updateUser(Long id, User userDetails) {
        User user = userRepository.findById(id).orElseThrow();
        User updatedUser = new User(user.id(), userDetails.name(), userDetails.email());
        return userRepository.save(updatedUser);
    }

    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}

This service class uses the UserRepository to perform CRUD operations.

Step 4: Controller Layer

In the controller layer, we expose our CRUD operations as HTTP endpoints:

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User createUser(@RequestBody User user) {
        return userService.createUser(user);
    }

    @GetMapping
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.getUserById(id);
    }

    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody User user) {
        return userService.updateUser(id, user);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
    }
}

The UserController will handle HTTP requests and delegate CRUD operations to the UserService.

Step 5: REST Client Using WebClient

To test the CRUD REST APIs of our Spring Boot application using WebClient from Spring Boot, we'll write a client class that performs HTTP requests to our UserController. WebClient is a non-blocking, reactive client for performing HTTP requests with a fluent, functional API. 

First, ensure that spring-boot-starter-webflux dependency is included in your pom.xml to use WebClient. Here's a complete code to use WebClient to test the CRUD operations:

import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

public class RestClient {
    private final WebClient webClient;

    public RestClient() {
        this.webClient = WebClient.create("http://localhost:8080");
    }

    public void createUser() {
        User user = new User(1L, "John Doe", "[email protected]");
        User createdUser = webClient.post()
                .uri("/users")
                .body(Mono.just(user), User.class)
                .retrieve()
                .bodyToMono(User.class)
                .block();
        System.out.println("Created User: " + createdUser);
    }

    public void getUser(Long id) {
        User user = webClient.get()
                .uri("/users/" + id)
                .retrieve()
                .bodyToMono(User.class)
                .block();
        System.out.println("User: " + user);
    }

    public void updateUser(Long id, User updatedUser) {
        User user = webClient.put()
                .uri("/users/" + id)
                .body(Mono.just(updatedUser), User.class)
                .retrieve()
                .bodyToMono(User.class)
                .block();
        System.out.println("Updated User: " + user);
    }

    public void deleteUser(Long id) {
        webClient.delete()
                .uri("/users/" + id)
                .retrieve()
                .bodyToMono(Void.class)
                .block();
        System.out.println("Deleted User with ID: " + id);
    }

    public static void main(String[] args) {
        RestClient client = new RestClient();

        // Test create user
        client.createUser();

        // Test get user
        client.getUser(1L);

        // Test update user
        User updatedUser = new User(1L, "Jane Doe", "[email protected]");
        client.updateUser(1L, updatedUser);

        // Test delete user
        client.deleteUser(1L);
    }

    // User record for client-side representation
    private record User(Long id, String name, String email) {}
}

Output

Assuming the server is running and the REST endpoints are correctly set up, executing the RestClient class would produce output similar to the following:

Created User: User[id=1, name=John Doe, [email protected]]
User: User[id=1, name=John Doe, [email protected]]
Updated User: User[id=1, name=Jane Doe, [email protected]]
Deleted User with ID: 1

Comments