mn create-app --jdk=17 \
example.micronaut.micronautguide \
--features=data-jdbc,postgres,liquibase \
--build=maven \
--lang=java \
Table of Contents
Micronaut Data and Java Records
Learn how to a leverage Java records for immutable configuration, Micronaut Data Mapped Entities and Projection DTOs
Authors: Sergio del Amo
Micronaut Version: 4.6.3
1. Getting Started
In this guide, we will create a Micronaut application written in Java.
You are going to use Record Classes in a Micronaut application.
Record classes, which are a special kind of class, help to model plain data aggregates with less ceremony than normal classes.
A record declaration specifies in a header a description of its contents; the appropriate accessors, constructor, equals, hashCode, and toString methods are created automatically. A record’s fields are final because the class is intended to serve as a simple "data carrier."
2. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE (e.g. IntelliJ IDEA)
-
JDK 21 or greater installed with
JAVA_HOME
configured appropriately
3. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
-
Download and unzip the source
Create an application using the Micronaut Command Line Interface or with Micronaut Launch.
To use Java records, specify JDK 17 when you create the application. |
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.
|
The previous command creates a Micronaut application with the default package example.micronaut
in a directory named micronautguide
.
If you use Micronaut Launch, select "Micronaut Application" as application type and add postgres , data-jdbc , and liquibase as features.
|
3.1. Immutable Configuration with Java Records
package example.micronaut;
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.annotation.NonNull;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
@ConfigurationProperties("vat") (1)
public record ValueAddedTaxConfiguration(
@NonNull @NotNull BigDecimal percentage) {
}
1 | The @ConfigurationProperties annotation takes the configuration prefix. |
Write a test:
package example.micronaut;
import io.micronaut.context.BeanContext;
import io.micronaut.context.annotation.Property;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Property(name = "vat.percentage", value = "21.0") (1)
@MicronautTest(startApplication = false) (2)
class ValueAddedTaxConfigurationTest {
@Inject
BeanContext beanContext;
@Test
void immutableConfigurationViaJavaRecords() {
assertTrue(beanContext.containsBean(ValueAddedTaxConfiguration.class));
assertEquals(new BigDecimal("21.0"),
beanContext.getBean(ValueAddedTaxConfiguration.class).percentage());
}
}
1 | Annotate the class with @Property to supply configuration to the test. |
2 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context. This test does not need the embedded server. Set startApplication to false to avoid starting it. |
3.2. Database Migration with Liquibase
We need a way to create the database schema. For that, we use Micronaut integration with Liquibase.
Add the following snippet to include the necessary dependencies:
<dependency>
<groupId>io.micronaut.liquibase</groupId>
<artifactId>micronaut-liquibase</artifactId>
<scope>compile</scope>
</dependency>
Configure the database migrations directory for Liquibase in application.yml
.
liquibase:
enabled: true
datasources:
default:
change-log: 'classpath:db/liquibase-changelog.xml'
Create the following files with the database schema creation and a book:
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<include file="changelog/01-create-books-schema.xml" relativeToChangelogFile="true"/>
<include file="changelog/02-insert-book.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="01" author="sdelamo">
<createTable tableName="book" remarks="A table to contain all books">
<column name="isbn" type="varchar(255)">
<constraints nullable="false" unique="true" primaryKey="true"/>
</column>
<column name="title" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="price" type="NUMERIC">
<constraints nullable="false"/>
</column>
<column name="about" type="LONGVARCHAR">
<constraints nullable="true"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="02" author="sdelamo">
<insert tableName="book">
<column name="isbn">0321601912</column>
<column name="title">Continuous Delivery</column>
<column name="price">39.99</column>
<column name="about">Winner of the 2011 Jolt Excellence Award! Getting software released to users is often a painful, risky, and time-consuming process. This groundbreaking new book sets out the principles and technical practices that enable rapid, incremental delivery of high quality, valuable new functionality to users.</column>
</insert>
</changeSet>
</databaseChangeLog>
During application startup, Liquibase executes the SQL file, creates the schema needed for the application and inserts one book.
3.3. Mapped Entities with Java Records
Create a Micronaut Data Mapped Entity
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
@MappedEntity (1)
public record Book(@Id @NonNull @NotBlank @Size(max = 255) String isbn, (2)
@NonNull @NotBlank @Size(max = 255) String title, (3)
@NonNull @NotNull BigDecimal price, (3)
@Nullable String about) { (4)
}
1 | Annotate the class with @MappedEntity to map the class to the table defined in the schema. |
2 | The primary key is annotated with @Id . It is an assigned primary key. |
3 | Use jakarta.validation.constraints Constraints to ensure the data matches your expectations. |
4 | Annotate about with @Nullable , since it is optional. |
3.4. Projections with Java Records
Create a record to project some data from the book
table. For example, exclude the about
field.
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
@Serdeable (1)
public record BookCard(@NonNull @NotBlank @Size(max = 255) String isbn, (2)
@NonNull @NotBlank @Size(max = 255) String title, (2)
@NonNull @NotNull BigDecimal price) {
}
1 | Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized. |
2 | Use jakarta.validation.constraints Constraints to ensure the data matches your expectations. |
Create a Repository, which uses the previous Java record as a DTO projection.
package example.micronaut;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
import java.util.List;
@JdbcRepository(dialect = Dialect.POSTGRES) (1)
public interface BookRepository extends CrudRepository<Book, String> { (2)
List<BookCard> find(); (3)
}
1 | @JdbcRepository with a specific dialect. |
2 | By extending CrudRepository you enable automatic generation of CRUD (Create, Read, Update, Delete) operations. |
3 | Micronaut Data supports reflection-free Data Transfer Object (DTO) projections if the return type is annotated with @Introspected . |
3.5. JSON serialization with Java Records
Create a Java record to represent a JSON response:
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.ReflectiveAccess;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
@Serdeable (1)
@ReflectiveAccess (2)
public record BookForSale(@NonNull @NotBlank String isbn, (3)
@NonNull @NotBlank String title,
@NonNull @NotNull BigDecimal price) { }
1 | Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized. |
2 | Jackson requires reflection to use Records. With @ReflectiveAccess the Micronaut framework will add the class to the GraalVM reflect-config.json file. |
3 | Use jakarta.validation.constraints Constraints to ensure the data matches your expectations. |
Create a Controller that uses the previous record:
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
@Controller("/books") (1)
class BookController {
private final BookRepository bookRepository;
private final ValueAddedTaxConfiguration valueAddedTaxConfiguration;
BookController(BookRepository bookRepository, (2)
ValueAddedTaxConfiguration valueAddedTaxConfiguration) {
this.bookRepository = bookRepository;
this.valueAddedTaxConfiguration = valueAddedTaxConfiguration;
}
@ExecuteOn(TaskExecutors.BLOCKING) (3)
@Get (4)
List<BookForSale> index() { (5)
return bookRepository.find()
.stream()
.map(bookCard -> new BookForSale(bookCard.isbn(),
bookCard.title(),
salePrice(bookCard)))
.collect(Collectors.toList());
}
@NonNull
private BigDecimal salePrice(@NonNull BookCard bookCard) {
return bookCard.price()
.add(bookCard.price()
.multiply(valueAddedTaxConfiguration.percentage()
.divide(new BigDecimal("100.0"), 2, RoundingMode.HALF_DOWN)))
.setScale(2, RoundingMode.HALF_DOWN);
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /books . |
2 | Use constructor injection to inject a bean of type BookRepository . |
3 | It is critical that any blocking I/O operations (such as fetching the data from the database) are offloaded to a separate thread pool that does not block the Event loop. |
4 | The @Get annotation maps the index method to an HTTP GET request on '/' . |
5 | Respond a Java Record and the application responds JSON representation of the object |
3.6. Create a Test
Create a test:
package example.micronaut;
import io.micronaut.context.annotation.Property;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Property(name = "vat.percentage", value = "21.0") (1)
@MicronautTest(transactional = false) (2)
class BookControllerTest {
int booksInsertedInDbByMigration = 1;
@Inject
@Client("/")
HttpClient httpClient; (3)
@Inject
BookRepository bookRepository;
@Test
void recordsUsedForJsonSerialization() {
String title = "Building Microservices";
String isbn = "1491950358";
(4)
String about = """
Distributed systems have become more fine-grained in the past 10 years, shifting from code-heavy monolithic applications to smaller, self-contained microservices. But developing these systems brings its own set of headaches. With lots of examples and practical advice, this book takes a holistic view of the topics that system architects and administrators must consider when building, managing, and evolving microservice architectures.
Microservice technologies are moving quickly. Author Sam Newman provides you with a firm grounding in the concepts while diving into current solutions for modeling, integrating, testing, deploying, and monitoring your own autonomous services. You’ll follow a fictional company throughout the book to learn how building a microservice architecture affects a single domain.
Discover how microservices allow you to align your system design with your organization’s goals
Learn options for integrating a service with the rest of your system
Take an incremental approach when splitting monolithic codebases
Deploy individual microservices through continuous integration
Examine the complexities of testing and monitoring distributed services
Manage security with user-to-service and service-to-service models
Understand the challenges of scaling microservice architectures
""";
Book b = new Book(isbn,
title,
new BigDecimal("38.15"),
about);
Book book = bookRepository.save(b);
assertEquals(booksInsertedInDbByMigration + 1, bookRepository.count());
BlockingHttpClient client = httpClient.toBlocking();
List<BookForSale> books = client.retrieve(HttpRequest.GET("/books"),
Argument.listOf(BookForSale.class)); (5)
assertNotNull(books);
assertEquals(booksInsertedInDbByMigration + 1, books.size());
assertEquals("Building Microservices", books.get(1).title());
assertEquals("1491950358", books.get(1).isbn());
assertEquals(new BigDecimal("46.16"), books.get(1).price());
bookRepository.delete(book);
}
}
1 | Annotate the class with @Property to supply configuration to the test. |
2 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. By default, each @Test method will be wrapped in a transaction that will be rolled back when the test finishes. This behaviour is is changed by setting transaction to false . |
3 | Inject the HttpClient bean and point it to the embedded server. |
4 | Multiline string |
5 | Micronaut HTTP Client simplifies binding a JSON array to a list of POJOs by using Argument::listOf . |
4. Test Resources
When the application is started locally — either under test or by running the application — resolution of the datasource URL is detected and the Test Resources service will start a local PostgreSQL docker container, and inject the properties required to use this as the datasource.
For more information, see the JDBC section of the Test Resources documentation.
5. Datasource configuration
Although the URL is configured automatically via Test Resources, we must configure the PostgreSQL driver and dialect in application.yml
:
datasources:
default:
driverClassName: org.postgresql.Driver (1)
dialect: POSTGRES (2)
schema-generate: NONE (3)
1 | Use PostgreSQL driver. |
2 | Configure the PostgreSQL dialect. |
3 | You handle database migrations via Liquibase |
6. Testing the Application
To run the tests:
./mvnw test
7. Running the application
Set up the environment variable to configure the VAT percentage.
Configure
export VAT_PERCENTAGE=20
To run the application, use the ./mvnw mn:run
command, which starts the application on port 8080.
You can run a cURL command to test the application:
curl http://localhost:8080/books
[{"isbn":"0321601912","title":"Continuous Delivery","price":47.99}]
8. Generate a Micronaut Application Native Executable with GraalVM
We will use GraalVM, an advanced JDK with ahead-of-time Native Image compilation, to generate a native executable of this Micronaut application.
Compiling Micronaut applications ahead of time with GraalVM significantly improves startup time and reduces the memory footprint of JVM-based applications.
Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.
|
8.1. GraalVM Installation
sdk install java 21.0.5-graal
For installation on Windows, or for a manual installation on Linux or Mac, see the GraalVM Getting Started documentation.
The previous command installs Oracle GraalVM, which is free to use in production and free to redistribute, at no cost, under the GraalVM Free Terms and Conditions.
Alternatively, you can use the GraalVM Community Edition:
sdk install java 21.0.2-graalce
8.2. Native Executable Generation
To generate a native executable using Maven, run:
./mvnw package -Dpackaging=native-image
The native executable is created in the target
directory and can be run with target/micronautguide
.
It is possible to customize the name of the native executable or pass additional build arguments using the Maven plugin for GraalVM Native Image building. Declare the plugin as following:
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.3</version>
<configuration>
<!-- <1> -->
<imageName>mn-graalvm-application</imageName> (1)
<buildArgs>
<!-- <2> -->
<buildArg>-Ob</buildArg>
</buildArgs>
</configuration>
</plugin>
1 | The native executable name will now be mn-graalvm-application . |
2 | It is possible to pass extra build arguments to native-image . For example, -Ob enables the quick build mode. |
Due to a bug with GraalVM and Java Records it is necessary to include the flag --report-unsupported-elements-at-runtime when building the native executable. Create the file native-image.properties :
|
Args = --report-unsupported-elements-at-runtime
You can run a cURL command to test the application:
curl http://localhost:8080/books
[{"isbn":"0321601912","title":"Continuous Delivery","price":47.99}]
You receive an empty array because there are no books in the database. You can create a Liquibase changelog to add seed data.
9. Next Steps
Explore more features with Micronaut Guides.
10. Help with the Micronaut Framework
The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.
11. License
All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…). |