mn create-app example.micronaut.micronautguide \
--features=jax-rs,data-jdbc,mysql,flyway,serialization-jackson,validation,graalvm \
--build=gradle \
--lang=java \
--test=junit
Micronaut JAX-RS
Expose endpoints using JAX-RS annotations in a Micronaut application
Authors: Dan Hollingsworth, Sergio del Amo
Micronaut Version: 4.6.3
1. Getting Started
In this guide, we will create a Micronaut application written in Java.
By default, Micronaut users define their HTTP Routing using the Micronaut @Controller
annotation and other built-in Routing Annotations. However, Micronaut JAX-RS allows you to define your Micronaut endpoints with JAX-RS annotations.
What exactly is JAX-RS? JAX-RS is a POJO-based, annotation-driven framework for building web services that comply with RESTful principles. Imagine writing all the low level code to parse an HTTP request and the logic just to wire these requests to appropriate Java classes/methods. The beauty of the JAX-RS API is that it insulates developers from that complexity and allows them to concentrate on business logic. That’s precisely where the use of POJOs and annotations come into play! JAX-RS has annotations to bind specific URI patterns and HTTP operations to individual methods of your Java class.
Gupta, Abhishek. REST assured with JAX-RS: speak HTTP using Java
This application exposes some REST endpoints using JAX-RS annotations and stores data in a MySQL database using Micronaut Data JDBC.
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 -
Docker installed to run MySQL and to run tests using Testcontainers.
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
4. Writing the Application
Create an application using the Micronaut Command Line Interface or with Micronaut Launch.
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 jax-rs
, data-jdbc
, mysql
, flyway
, serialization-jackson
, validation
, and graalvm
features.
If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features, and apply those changes to your application. |
4.1. Data Source configuration
Define the datasource in src/main/resources/application.properties
.
datasources.default.db-type=mysql
datasources.default.dialect=MYSQL
datasources.default.driver-class-name=com.mysql.cj.jdbc.Driver
This way of defining the datasource properties enables us to externalize the configuration, for example for production environment, and also provide a default value for development. If the environment variables are not defined, the Micronaut framework will use the default values. |
4.2. Database Migration with Flyway
We need a way to create the database schema. For that, we use Micronaut integration with Flyway.
Flyway automates schema changes, significantly simplifying schema management tasks, such as migrating, rolling back, and reproducing in multiple environments.
Add the following snippet to include the necessary dependencies:
implementation("io.micronaut.flyway:micronaut-flyway")
We will enable Flyway in the Micronaut configuration file and configure it to perform migrations on one of the defined data sources.
(1)
flyway.datasources.default.enabled=true
1 | Enable Flyway for the default datasource. |
Configuring multiple data sources is as simple as enabling Flyway for each one. You can also specify directories that will be used for migrating each data source. Review the Micronaut Flyway documentation for additional details. |
Flyway migration will be automatically triggered before your Micronaut application starts. Flyway will read migration commands in the resources/db/migration/
directory, execute them if necessary, and verify that the configured data source is consistent with them.
Create the following migration files with the database schema creation:
DROP TABLE IF EXISTS pet;
CREATE TABLE pet (
id BIGINT NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
type varchar(255) check (type in ('DOG', 'CAT'))
);
During application startup, Flyway will execute the SQL file and create the schema needed for the application.
4.3. Domain
Add an enum:
package example.micronaut;
public enum PetType {
DOG,
CAT
}
Create an entity:
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
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;
@Serdeable (1)
@MappedEntity (2)
public class Pet {
@Id (3)
@GeneratedValue(GeneratedValue.Type.AUTO) (4)
private Long id;
@NonNull
@NotBlank
private String name;
@NonNull
@NotNull
private PetType type = PetType.DOG;
public Pet() {
}
public Pet(@NonNull String name) {
this.name = name;
}
public Pet(@NonNull String name, @NonNull PetType type) {
this.name = name;
this.type = type;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@NonNull
public String getName() {
return name;
}
public void setName(@NonNull String name) {
this.name = name;
}
@NonNull
public PetType getType() {
return type;
}
public void setType(@NonNull PetType type) {
this.type = type;
}
}
1 | Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized. |
2 | Annotate the class with @MappedEntity to map the class to the table defined in the schema. |
3 | Specifies the ID of an entity |
4 | Specifies that the property value is generated by the database and not included in inserts |
4.4. Service
Create a POJO NameDto
:
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotBlank;
@Serdeable (1)
public class NameDto {
@NonNull
@NotBlank
private final String name;
public NameDto(@NonNull String name) {
this.name = name;
}
@NonNull
public String getName() {
return name;
}
}
1 | Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized. |
Create a Repository:
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;
import java.util.Optional;
import io.micronaut.core.annotation.NonNull;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@JdbcRepository(dialect = Dialect.MYSQL) (1)
public interface PetRepository extends CrudRepository<Pet, Long> { (2)
@NonNull
List<NameDto> list(); (3)
@NonNull
Optional<Pet> findByName(@NonNull @NotBlank String name);
Pet save(@NonNull @NotBlank String name,
@NonNull @NotNull PetType type);
}
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 . |
4.5. JAX-RS
4.5.1. Dependencies
When you add a jax-rs
feature, the generated application includes the following dependencies:
annotationProcessor("io.micronaut.jaxrs:micronaut-jaxrs-processor")
implementation("io.micronaut.jaxrs:micronaut-jaxrs-server")
4.5.2. Resource
Create a POJO to encapsulate the HTTP Request body for a save request:
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@Serdeable
public class PetSave {
@NonNull
@NotBlank
private final String name;
@NonNull
@NotNull
private final PetType type;
public PetSave(@NonNull String name, @NonNull PetType type) {
this.name = name;
this.type = type;
}
@NonNull
public String getName() {
return name;
}
@NonNull
public PetType getType() {
return type;
}
}
Define an endpoint using JAX-RS.
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Status;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import java.util.List;
import java.util.Optional;
@Path("/pets") (1)
class PetResource {
private final PetRepository petRepository;
PetResource(PetRepository petRepository) { (2)
this.petRepository = petRepository;
}
@GET (3)
public List<NameDto> all() { (4)
return petRepository.list();
}
@GET (3)
@Path("/{name}") (5)
public Optional<Pet> byName(@PathParam("name") String petsName) { (6)
return petRepository.findByName(petsName);
}
@POST
@Status(HttpStatus.CREATED)
public void save(@NonNull @NotNull @Body PetSave petSave) {
petRepository.save(petSave.getName(), petSave.getType());
}
}
1 | A JAX-RS annotation to define the base URI for methods in this class. |
2 | Use constructor injection to inject a bean of type PetRepository . |
3 | A JAX-RS annotation to define an endpoint for HTTP Get requests. Mapped to the base URI /pets , since no @Path is provided. |
4 | The response is converted to a JSON array automatically. |
5 | Paths can be templated for path parameters, such as the pet’s name, here. |
6 | Associate the template placeholder name with the Java variable. |
4.6. Tests
Add a test for the resource:
package example.micronaut;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.http.uri.UriBuilder;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;
import org.junit.jupiter.api.function.Executable;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@MicronautTest(transactional = false) (1)
public class PetResourceTest {
@Inject
@Client("/")
HttpClient httpClient; (2)
@Inject
PetRepository repository;
@Test
void testAll() {
Pet dino = new Pet("Dino");
Pet bp = new Pet("Baby Puss");
bp.setType(PetType.CAT);
Pet hoppy = new Pet("Hoppy");
repository.saveAll(Arrays.asList(dino, bp, hoppy));
HttpResponse<List<NameDto>> response = httpClient.toBlocking()
.exchange(HttpRequest.GET("/pets"), Argument.listOf(NameDto.class));
assertEquals(HttpStatus.OK , response.status());
List<NameDto> petNames = response.body();
assertNotNull(petNames);
assertEquals(3, petNames.size());
repository.deleteAll();
}
@Test
void testGet() {
String name = "Dino";
Pet dino = new Pet(name);
Long id = repository.save(dino).getId();
URI uri = UriBuilder.of("/pets").path(name).build();
HttpResponse<Pet> response = httpClient.toBlocking()
.exchange(HttpRequest.GET(uri), Pet.class);
assertEquals(HttpStatus.OK , response.status());
Pet pet = response.body();
assertNotNull(pet);
assertNotNull(pet.getId());
assertNotNull(pet.getType());
assertEquals(PetType.DOG, pet.getType());
assertEquals(name, pet.getName());
repository.deleteById(id);
}
@Test
void testGetIfPetDoesNotExistsAPIReturnsNotFound() {
String name = "Dino";
Pet dino = new Pet(name);
Long id = repository.save(dino).getId();
URI uri = UriBuilder.of("/pets").path("Foo").build();
Executable e = () -> httpClient.toBlocking()
.exchange(HttpRequest.GET(uri), Pet.class);
HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, e);
assertEquals(HttpStatus.NOT_FOUND , thrown.getResponse().status()); (2)
repository.deleteById(id);
}
@Test
void testSave() {
String name = "Dino";
long oldCount = repository.count();
PetSave petSave = new PetSave(name, PetType.DOG);
HttpRequest<?> request = HttpRequest.POST("/pets", petSave);
HttpResponse<Pet> response = httpClient.toBlocking()
.exchange(request, Pet.class);
assertEquals(HttpStatus.CREATED , response.status());
long count = repository.count();
assertEquals(oldCount + 1, count);
repository.deleteAll();
}
}
1 | 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 . |
2 | Inject the HttpClient bean and point it to the embedded server. |
3 | Micronaut returns a 404 response if the resource method returns an empty optional. |
5. 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 MySQL docker container, and inject the properties required to use this as the datasource.
For more information, see the JDBC section or R2DBC section of the Test Resources documentation.
6. Testing the Application
To run the tests:
./gradlew test
Then open build/reports/tests/test/index.html
in a browser to see the results.
7. Running the Application
To run the application, use the ./gradlew run
command, which starts the application on port 8080.
You can execute the endpoints exposed by the application:
curl -id '{"name":"Chase", "type":"DOG"}' \
-H "Content-Type: application/json" \
-X POST http://localhost:8080/pets
HTTP/1.1 201 Created
...
curl -i localhost:8080/pets
HTTP/1.1 200 OK
...
[{"name":"Chase"}]
8. Generate a Micronaut Application Native Executable with GraalVM
We will use GraalVM, the polyglot embeddable virtual machine, to generate a native executable of our Micronaut application.
Compiling native executables ahead of time with GraalVM 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 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 Gradle, run:
./gradlew nativeCompile
The native executable is created in build/native/nativeCompile
directory and can be run with build/native/nativeCompile/micronautguide
.
It is possible to customize the name of the native executable or pass additional parameters to GraalVM:
graalvmNative {
binaries {
main {
imageName.set('mn-graalvm-application') (1)
buildArgs.add('--verbose') (2)
}
}
}
1 | The native executable name will now be mn-graalvm-application |
2 | It is possible to pass extra arguments to build the native executable |
You can execute the endpoints exposed by the native executable:
curl -id '{"name":"Chase", "type":"DOG"}' \
-H "Content-Type: application/json" \
-X POST http://localhost:8080/pets
HTTP/1.1 201 Created
...
curl -i localhost:8080/pets
HTTP/1.1 200 OK
...
[{"name":"Chase"}]
9. Configuration for production
When the application is run in a non-local environment, you will need to specify the datasource URL and credentials in a configuration that matches the specific environment. This can be achieved by adding a configuration specific to that environment like so:
Unresolved directive in micronaut-jaxrs-jdbc-gradle-java.adoc - include::build/code/micronaut-jaxrs-jdbc/micronaut-jaxrs-jdbc-gradle-java/src/main/resources/application-prod.yml[tag=prod-datasource]
And then run the application with the following environment variables:
-
MICRONAUT_ENVIRONMENTS=prod
-
DATASOURCES_DEFAULT_USERNAME=<username>
-
DATASOURCES_DEFAULT_PASSWORD=<password>
Instead of environment variables, you can also use Micronaut Distributed Configuration to pull these values from a secrets manager such as Hashicorp Vault. |
10. Next steps
Read more about:
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…). |