Micronaut Tutorial: Server Application
In this part of my tutorial series on the Micronaut framework we are going to create a simple HTTP server-side application running on Netty.
Join the DZone community and get the full member experience.
Join For FreeIn this part of my tutorial series on the Micronaut framework we are going to create a simple HTTP server-side application running on Netty. We have already discussed the most interesting core features of Micronaut like beans, scopes, and unit testing in the first part of this series. For more details you may refer to my article Micronaut Tutorial: Beans and Scopes.
Assuming that we have a basic knowledge of the core mechanisms of Micronaut, we may proceed to the key part of that framework and discuss how to build simple microservice application exposing a REST API over HTTP.
Embedded Server
First, we need to include a dependency in our pom.xml
that's responsible for running an embedded server during the application startup. By default, Micronaut starts on Netty server, so we only need to include the following dependency:
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
</dependency>
Assuming, we have the following main class defined, we only need to run it:
public class MainApp {
public static void main(String[] args) {
Micronaut.run(MainApp.class);
}
}
By default, the Netty server runs on port 8080
. You may override it to force the server to run on a specific port by setting the following property in your application.yml
or bootstrap.yml
. You can also set the value of this property to -1
to run the server on randomly generated port.
micronaut:
server:
port: 8100
Creating a Web Application
If you are already familiar with Spring Boot you should not have any problems with building a simple REST server-side application using Micronaut. The approach is almost identical. We just have to create a controller class and annotate it with @Controller
. Micronaut supports all HTTP method types. You will probably use: @Get
, @Post
, @Delete
, @Put
or @Patch
. Here's our sample controller class that implements methods for adding new Person
object, finding all persons or a single person by id:
@Controller("/persons")
public class PersonController {
List<Person> persons = new ArrayList<>();
@Post
public Person add(Person person) {
person.setId(persons.size() + 1);
persons.add(person);
return person;
}
@Get("/{id}")
public Optional<Person> findById(Integer id) {
return persons.stream()
.filter(it -> it.getId().equals(id))
.findFirst();
}
@Get
public List<Person> findAll() {
return persons;
}
}
Request variables are resolved automatically and bind to the variable with the same name. Micronaut populates methods arguments from URI variables like /{variableName}
and GET query parameters like ?paramName=paramValue
. If the request contains a JSON body you should annotate it with @Body
. Our sample controller is very simple. It does not perform any input data validation. Let's change it.
Validation
To be able to perform HTTP request validation, we should first include the following dependencies in our pom.xml
:
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut.configuration</groupId>
<artifactId>micronaut-hibernate-validator</artifactId>
</dependency>
Validation in Micronaut is based on JSR-380, also known as Bean Validation 2.0. We can use javax.validation
annotations such as @NotNull
, @Min
or @Max
. Micronaut uses implementation provided by Hibernate Validator, so even if don't use any JPA in your project, you have to include micronaut-hibernate-validator
to your dependencies. After that we may add a validation to our model class using some javax.validation
annotations. Here's Person
model class with validation. All the fields are required: firstName
and lastName
cannot be blank, id
cannot be greater than 10000
, age
cannot be lower than 0
.
public class Person {
@Max(10000)
private Integer id;
@NotBlank
private String firstName;
@NotBlank
private String lastName;
@PositiveOrZero
private int age;
@NotNull
private Gender gender;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Gender getGender() {
return gender;
}
public void setGender(Gender gender) {
this.gender = gender;
}
}
Now, we need to modify the code of our controller. First, it needs to be annotated with @Validated
. Also the@Body
parameter of the POST method should be annotated with @Valid
. The REST method argument may also be validated using JSR-380 annotation. Alternatively, we may configure validation using URI templates. The annotation @Get("/{id:4}")
indicates that a variable can contain 4 characters max (is lower than 10000) or a query parameter is optional as shown here: @Get("{?max,offset}")
.
Here's the current implementation of our controller. Besides validation, I have also implemented pagination for findAll
based on offset and limit optional parameters:
@Controller("/persons")
@Validated
public class PersonController {
List<Person> persons = new ArrayList<>();
@Post
public Person add(@Body @Valid Person person) {
person.setId(persons.size() + 1);
persons.add(person);
return person;
}
@Get("/{id:4}")
public Optional<Person> findById(@NotNull Integer id) {
return persons.stream()
.filter(it -> it.getId().equals(id))
.findFirst();
}
@Get("{?max,offset}")
public List<Person> findAll(@Nullable Integer max, @Nullable Integer offset) {
return persons.stream()
.skip(offset == null ? 0 : offset)
.limit(max == null ? 10000 : max)
.collect(Collectors.toList());
}
}
Since we have finished the implementation of our controller, it is a right time to test it.
Testing With an Embedded Server
We have already discussed testing with Micronaut in the first part of this tutorial series. The only difference in comparison to those tests is a necessity of running an embedded server and call an endpoint via HTTP. To do that we have to include the dependency with Micronaut HTTP client:
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-client</artifactId>
<scope>test</scope>
</dependency>
We should also inject an instance of an embedded server in order to be able to detect its address (for example if a port number is generated automatically):
@MicronautTest
public class PersonControllerTests {
@Inject
EmbeddedServer server;
// tests implementation ...
}
We are building a Micronaut HTTP Client programmatically by calling static method create
. It is also possible to obtain a reference to HttpClient
by annotating it with @Client
.
The following test implementation is based on JUnit 5. I have provided the positive test for all the exposed endpoints and one negative scenario with not valid input data ( age
field lower than zero). The Micronaut HTTP client can be used in both asynchronous non-blocking mode and synchronous blocking mode. In that case we force it to work in blocking mode.
@MicronautTest
public class PersonControllerTests {
@Inject
EmbeddedServer server;
@Test
public void testAdd() throws MalformedURLException {
HttpClient client = HttpClient.create(new URL("http://" + server.getHost() + ":" + server.getPort()));
Person person = new Person();
person.setFirstName("John");
person.setLastName("Smith");
person.setAge(33);
person.setGender(Gender.MALE);
person = client.toBlocking().retrieve(HttpRequest.POST("/persons", person), Person.class);
Assertions.assertNotNull(person);
Assertions.assertEquals(Integer.valueOf(1), person.getId());
}
@Test
public void testAddNotValid() throws MalformedURLException {
HttpClient client = HttpClient.create(new URL("http://" + server.getHost() + ":" + server.getPort()));
final Person person = new Person();
person.setFirstName("John");
person.setLastName("Smith");
person.setAge(-1);
person.setGender(Gender.MALE);
Assertions.assertThrows(HttpClientResponseException.class,
() -> client.toBlocking().retrieve(HttpRequest.POST("/persons", person), Person.class),
"person.age: must be greater than or equal to 0");
}
@Test
public void testFindById() throws MalformedURLException {
HttpClient client = HttpClient.create(new URL("http://" + server.getHost() + ":" + server.getPort()));
Person person = client.toBlocking().retrieve(HttpRequest.GET("/persons/1"), Person.class);
Assertions.assertNotNull(person);
}
@Test
public void testFindAll() throws MalformedURLException {
HttpClient client = HttpClient.create(new URL("http://" + server.getHost() + ":" + server.getPort()));
Person[] persons = client.toBlocking().retrieve(HttpRequest.GET("/persons"), Person[].class);
Assertions.assertEquals(1, persons.length);
}
}
We have already built a simple web application that exposes some methods over a REST API, validates input data, and includes JUnit API tests. Now, we may discuss some more advanced, interesting Micronaut features. First of them is built-in support for API versioning.
API Versioning
Since 1.1, Micronaut has supported API versioning via a dedicated @Version
annotation. To test this feature, we will add a new version of the findAll
method to our controller class. The new version of this method requires to set input parameters max
and offset
, which were optional for the first version of the method.
@Version("1")
@Get("{?max,offset}")
public List<Person> findAll(@Nullable Integer max, @Nullable Integer offset) {
return persons.stream()
.skip(offset == null ? 0 : offset)
.limit(max == null ? 10000 : max)
.collect(Collectors.toList());
}
@Version("2")
@Get("?max,offset")
public List<Person> findAllV2(@NotNull Integer max, @NotNull Integer offset) {
return persons.stream()
.skip(offset == null ? 0 : offset)
.limit(max == null ? 10000 : max)
.collect(Collectors.toList());
}
The versioning feature is not enabled by default. To do that, you need to set property micronaut.router.versioning.enabled
to true
in application.yml
. We will also set default version to 1
, which is compatible with tests created in the previous section that does not use versioning feature:
micronaut:
router:
versioning:
enabled: true
default-version: 1
Micronaut automatic versioning is supported by a declarative HTTP client. To create such a client we need to define an interface that contains a signature of the target server-side method, and is annotated with @Client
. Here's declarative client interface responsible only for communicating with version 2
of findAll
method:
@Client("/persons")
public interface PersonClient {
@Version("2")
@Get("?max,offset")
List<Person> findAllV2(Integer max, Integer offset);
}
The PersonClient
declared above may be injected into the test and used for calling API method exposed by server-side application:
@Inject
PersonClient client;
@Test
public void testFindAllV2() {
List<Person> persons = client.findAllV2(10, 0);
Assertions.assertEquals(1, persons.size());
}
API Documentation With Swagger
Micronaut provides built-in support for generating Open API/Swagger YAML documentation at compilation time. We can customize produced documentation using standard Swagger annotations. To enable this support for our application we should add the following swagger-annotations
dependency to pom.xml
, and enable annotation processing for micronaut-openapi
module inside Maven compiler plugin configuration:
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>${jdk.version}</source>
<target>${jdk.version}</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-java</artifactId>
<version>${micronaut.version}</version>
</path>
<path>
<groupId>io.micronaut.configuration</groupId>
<artifactId>micronaut-openapi</artifactId>
<version>${micronaut.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
...
</plugin>
We have to include some basic information to the generated Swagger YAML like application name, description, version number, or author name using the @OpenAPIDefinition
annotation:
@OpenAPIDefinition(
info = @Info(
title = "Sample Application",
version = "1.0",
description = "Sample API",
contact = @Contact(url = "https://piotrminkowski.wordpress.com", name = "Piotr Mińkowski", email = "piotr.minkowski@gmail.com")
)
)
public class MainApp {
public static void main(String[] args) {
Micronaut.run(MainApp.class);
}
}
Micronaut generates a Swagger file based on the title and version fields inside the @Info
annotation. In that case, our YAML definition file is available under the name sample-application-1.0.yml
, and will be generated to the META-INF/swagger
directory. We can expose it outside application using HTTP endpoint. Here's the appropriate configuration provided inside application.yml
file.
micronaut:
static-resources:
swagger:
paths: classpath:META-INF/swagger
mapping: /swagger/**
Assuming our application is running on port 8100 Swagger definition is available under the path http://localhost:8100/swagger/sample-application-1.0.yml
. You can call this endpoint and copy the response to any Swagger editor as shown below.
Management and Monitoring Endpoints
Micronaut provides some built-in HTTP endpoints used for management and monitoring. To enable them for the application we first need to include the following dependency:
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-management</artifactId>
</dependency>
There are no endpoints exposed by default outside the application. If you would like to expose them all you should set the endpoints.all.enabled
property to true
. Alternatively, you can enable or disable the single endpoint just by using its id instead of all
in the name of property. Also, some of the built-in endpoints require authentication, and some not. You may enable/disable it for all endpoints using property endpoints.all.enabled
. The following configuration inside application.yaml
enables all built-in endpoints except stop
endpoints using for graceful shutdown of application, and disables authentication for all the enabled endpoints:
endpoints:
all:
enabled: true
sensitive: false
stop:
enabled: false
You may use one of the following:
GET /beans
- returns information about the loaded bean definitions.GET /info
- returns static information from the state of the application.GET /health
- exposes "healthcheck."POST /refresh
- it is refresh the application state, all the beans annotated with@Refreshable
will be reloaded.GET /routes
- returns information about URIs exposed by the application.GET /logger
- returns information about the available loggers.GET /caches
- returns information about the caches.POST /stop
- it shuts down the application server.
Summary
In this tutorial, we have learned how to:
- Build a simple application that exposes some HTTP endpoints.
- Validate input data inside a controller.
- Test our controller with JUnit 5 on an embedded Netty server using a Micronaut HTTP client.
- Use built-in API versioning.
- Generate Swagger API documentation automatically.
- Using built-in management and monitoring endpoints.
The repository for this series can be found here: https://github.com/piomin/sample-micronaut-applications.git.
Published at DZone with permission of Piotr Mińkowski, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments