Table of Contents

Micronaut Framework

Natively Cloud Native

Version: 4.3.11

1 Introduction

The Micronaut Framework is a modern, JVM-based, full stack Java framework designed for building modular, easily testable JVM applications with support for Java, Kotlin, and Groovy.

The Micronaut framework was originally created by a team who had also worked on the Grails framework. The Micronaut framework takes inspiration from lessons learned over the years building real-world applications from monoliths to microservices using Spring, Spring Boot and the Grails framework. The core team continues to develop and maintain the Micronaut project through the support of the Micronaut Foundation.

The Micronaut framework aims to provide all the tools necessary to build JVM applications including:

  • Dependency Injection and Inversion of Control (IoC)

  • Aspect Oriented Programming (AOP)

  • Sensible Defaults and Auto-Configuration

With the Micronaut framework you can build Message-Driven Applications, Command Line Applications, HTTP Servers and more whilst for Microservices in particular Micronaut also provides:

  • Distributed Configuration

  • Service Discovery

  • HTTP Routing

  • Client-Side Load Balancing

At the same time, the Micronaut framework aims to avoid the downsides of frameworks like Spring, Spring Boot and Grails by providing:

  • Fast startup time

  • Reduced memory footprint

  • Minimal use of reflection

  • Minimal use of proxies

  • No runtime bytecode generation

  • Easy Unit Testing

Historically, frameworks such as Spring and Grails were not designed to run in scenarios such as serverless functions, Android apps, or low memory footprint microservices. In contrast, the Micronaut framework is designed to be suitable for all of these scenarios.

This goal is achieved through the use of Java’s annotation processors, which are usable on any JVM language that supports them, as well as an HTTP Server (with several runtimes Netty, Jetty, Tomcat, Undertow…​) and an HTTP Client (with several runtimes Netty, Java HTTP Client, …​). To provide a similar programming model to Spring and Grails, these annotation processors precompile the necessary metadata to perform DI, define AOP proxies and configure your application to run in a low-memory environment.

Many APIs in the Micronaut framework are heavily inspired by Spring and Grails. This is by design, and helps bring developers up to speed quickly.

1.1 What's New?

4.2.0

Kotlin base version updated to 1.9.20

Kotlin has been updated to 1.9.20, which may cause issues when compiling or linking to Kotlin libraries.

4.1.0

4.0.0

Core Changes

Java 17 Baseline

Micronaut 4 now requires a minimum of Java 17 for building and running applications.

GraalVM 23 support, GraalVM Metadata Repository and Runtime Initialization

The GraalVM Metadata Repository in Micronaut’s Gradle and Maven plugins is now enabled by default and Micronaut has been altered to by default primarily initialize at runtime to ensure consistency in behaviour between JIT and Native applications.

Kotlin 1.8

Micronaut framework 4.0 supports Kotlin 1.8

Experimental Support for Kotlin Symbol Processing (KSP)

Micronaut framework has offered support for Kotlin via KAPT. With version 4.0, Micronaut framework supports Kotlin also via Kotlin Symbol Processing (KSP) API.

Apache Groovy 4.0

Micronaut framework 4.x supports Apache Groovy 4.0.

Improved Modularity

The core of Micronaut has been further refactored to improve modularity and reduce the footprint of a Micronaut application, including:

  • Third-party dependencies on SnakeYAML and Jackson Databind are now optional and can be removed if other implementations are present.

  • The runtime and compiler code has been split, allowing the removal of the re-packaging of ASM and Caffeine and reduction of the runtime footprint.

  • The built-in Validation, Retry, Service Discovery, HTTP Session and WebSocket features have been split into separate modules allowing removal of this functionality if not needed.

Completed javax to jakarta Migration

The remaining functionality depending on the javax specification has been migrated to jakarta including the validation module (for jakarta.validation) and support for Hibernate 6 (for jakarta.persistence).

Expression Language

A new fully compilation time, type-safe and reflection-free Expression Language has been added to the framework which unlocks a number of new possibilities (like conditional job scheduling). It is expected that submodules will adopt the new EL over time to add features and capabilities.

Injection of Maps

It is now possible to inject a java.util.Map of beans where the key is the bean name. The name of the bean is derived from the qualifier or (if not present) the simple name of the class.

Arbitrary Nesting of Configuration Properties

With Micronaut 4 it is now possible to arbitrarily nest @ConfigurationProperties and @EachProperty annotations allowing for more dynamic configuration possibilities.

Improved Error Messages for Missing Configuration

When a bean is not present due to missing configuration (such as a bean that uses @EachProperty) error messages have been improved to display the configuration that is required to activate the bean.

Improved Error Messages for Missing Beans

When a bean annotated with @EachProperty or @Bean is not found due to missing configuration an error is thrown showing the configuration prefix necessary to resolve the issue.

Tracking of Disabled Beans

Beans that are disabled via Bean Requirements are now tracked and an appropriate error thrown if a bean has been disabled.

The disabled beans are also now visible via the Beans Endpoint in the Management module aiding in understanding the state of your application configuration.

HTTP Changes

Initial Support for Virtual Threads (Loom)

Preview support for Virtual Threads has been added. When using JDK 19 or above with preview features enabled you can off load processing to a virtual thread pool.

Experimental Support for HTTP/3

Experimental Support for HTTP/3 via the Netty incubator project has been added.

Experimental Support for io_uring

Experimental support for io_uring via the Netty incubator project has been added.

Rewritten HTTP layer

The HTTP layer has been rewritten to improve performance and reduce the presence of reactive stack frames if reactive is not used (such as with Virtual threads).

Annotation-Based HTTP Filters

Context Propagation API

Micronaut Framework 4 introduces a new Context Propagation API, which aims to simplify reactor instrumentation, avoid thread-local usage, and integrate idiomatically with Kotlin Coroutines.

1.2 Upgrading your Micronaut Application

Upgrading between Micronaut Framework versions

Check Micronaut Upgrade documentation to help you upgrade your Micronaut applications.

Breaking Changes

Review the section on Breaking Changes and update your affected application code.

2 Quick Start

The following sections walk you through a Quick Start on how to use the Micronaut framework to set up a basic "Hello World" application.

Before getting started ensure you have a Java 8 or higher JDK installed, and it is recommended that you use a suitable IDE such as IntelliJ IDEA.

2.1 Install the CLI

The Micronaut CLI is an optional but convenient way to create Micronaut applications. The best way to install Micronaut CLI on Unix systems is with SDKMAN which greatly simplifies installing and managing multiple Micronaut versions.

To see all available installation methods, check the Micronaut Starter documentation.

2.2 Creating a Server Application

Using the Micronaut CLI you can create a new Micronaut application in either Groovy, Java, or Kotlin (the default is Java).

The following command creates a new "Hello World" server application in Java with a Gradle build:

Applications generated via our CLI include Gradle or Maven wrappers, so it is not even necessary to have Gradle or Maven installed on your machine to begin running the applications. Simply use the mvnw or gradlew command, as explained further below.
$ mn create-app hello-world
Supply --build maven to create a Maven-based build instead

If you don’t have the CLI installed then you can also create the same application by visiting Micronaut Launch and clicking the "Generate Project" button or by using the following curl command on Unix systems:

curl https://launch.micronaut.io/hello-world.zip -o hello-world.zip
unzip hello-world.zip
cd hello-world
Add ?build=maven to the URL passed to curl to generate a Maven project.

The previous steps created a new Java application in a directory called hello-world featuring a Gradle build. You can run the application with ./gradlew run:

$ ./gradlew run
> Task :run
[main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 540ms. Server Running: http://localhost:28933

If you have created a Maven-based project, use ./mvnw mn:run instead.

For Windows the ./ before commands is not needed

By default, the Micronaut HTTP server is configured to run on port 8080. See the section Running Server on a Specific Port for more options.

To create a service that responds to "Hello World" you first need a controller. The following is an example:

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/hello") // (1)
public class HelloController {

    @Get(produces = MediaType.TEXT_PLAIN) // (2)
    public String index() {
        return "Hello World"; // (3)
    }
}
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller('/hello') // (1)
class HelloController {

    @Get(produces = MediaType.TEXT_PLAIN) // (2)
    String index() {
        'Hello World' // (3)
    }
}
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/hello") // (1)
class HelloController {

    @Get(produces = [MediaType.TEXT_PLAIN]) // (2)
    fun index(): String {
        return "Hello World" // (3)
    }
}
1 The @Controller annotation defines the class as a controller mapped to the path /hello
2 The @Get annotation maps the index method to all requests that use an HTTP GET
3 A String "Hello World" is returned as the response

If you use Java, place the previous file in src/main/java/hello/world.
If you use Groovy, place the previous file in src/main/groovy/hello/world.
If you use Kotlin, place the previous file in src/main/kotlin/hello/world.

If you start the application and send a GET request to the /hello URI, the text "Hello World" is returned:

$ curl http://localhost:8080/hello
Hello World
See the guide for Creating your First Micronaut Application to learn more.

2.3 Setting up an IDE

The application created in the previous section contains a main class located in src/main/java that looks like the following:

import io.micronaut.runtime.Micronaut;

public class Application {

    public static void main(String[] args) {
        Micronaut.run(Application.class);
    }
}
import io.micronaut.runtime.Micronaut

class Application {

    static void main(String... args) {
        Micronaut.run Application
    }
}
import io.micronaut.runtime.Micronaut

object Application {

    @JvmStatic
    fun main(args: Array<String>) {
        Micronaut.run(Application.javaClass)
    }
}

This is the class that is run when starting the application via Gradle/Maven or via deployment. You can also run the main class directly within your IDE.

2.3.1 IntelliJ IDEA

To import a Micronaut project into IntelliJ IDEA, open the build.gradle or pom.xml file and follow the instructions to import the project.

For IntelliJ IDEA, if you plan to use the IntelliJ compiler, enable annotation processing under "Build, Execution, Deployment → Compiler → Annotation Processors" by ticking the "Enable annotation processing" checkbox:

Intellij Settings

Once you have enabled annotation processing in IntelliJ you can run the application and tests directly within the IDE without the need of an external build tool such as Gradle or Maven.

2.3.2 Eclipse

To use Eclipse IDE, it is recommended you import your Micronaut project into Eclipse using either Gradle BuildShip for Gradle or M2Eclipse for Maven.

The Micronaut framework requires Eclipse IDE 4.9 or higher

Eclipse and Gradle

Once you have set up Eclipse 4.9 or higher with Gradle BuildShip, first run the gradle eclipse task from the root of your project, then import the project by selecting File → Import and choosing Gradle → Existing Gradle Project and navigating to the root directory of your project (where the build.gradle file is located).

Eclipse and Maven

For Eclipse 4.9 and above with Maven you need the following Eclipse plugins:

Once these are installed, import the project by selecting File → Import and choosing Maven → Existing Maven Project and navigating to the root directory of your project (where the pom.xml file is located).

Then enable annotation processing by opening Eclipse → Preferences and navigating to Maven → Annotation Processing and selecting the option Automatically configure JDT APT.

2.3.3 Apache NetBeans

Apache NetBeans can open Maven and Gradle projects out of the box.

Make sure that the Java Web and EE feature is enabled at Tools → Plugins → Installed, in order to have additional support for Micronaut, like code completion for configuration and data elements.

NetBeans Plugins

2.3.4 Visual Studio Code

The Micronaut framework can be set up within Visual Studio Code in one of two ways.

Option 1) GraalVM Extension Pack for Java

The prefered way is using the GraalVM Extension Pack for Java which ships with an Apache NetBeans Language server.

It is not possible to have both the official Java Extension Pack and the GraalVM Extension Pack for Java installed at the same time so if you prefer the former, skip this section and go to Option 2)

The GraalVM Tools for Java are preferred since they delegate to the build system for running applications and tests which means there is no additional setup or differences with regard to how javac is configured for annotation processing when compared to the Java Extension Pack which is based on the Eclipse compiler.

The GraalVM Extension Pack also includes the GraalVM Tools for Micronaut extension which features:

  • An application creation wizard

  • Code completion for YAML configuration

  • Pallet commands to build, deploy, create Native Images etc.

Option 2) Red Hat/Microsoft Java Extension Pack

First install the Java Extension Pack.

You can also optionally install STS to enable code completion for application.yml.

If you use Gradle, prior to opening the project in VSC run the following command from a terminal window:

./gradlew eclipse
If you don’t run the above command beforehand then annotation processing will not be configured correctly and the application will not work.

Once the extension pack is installed just type code . in any project directory and the project will be automatically set up.

For macOS, you need to install the code command by following these instructions.

2.4 Creating a Client

As mentioned previously, the Micronaut framework includes both an HTTP server and an HTTP client. A low-level HTTP client is provided which you can use to test the HelloController created in the previous section.

Testing Hello World
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.runtime.server.EmbeddedServer;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import jakarta.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest
public class HelloControllerSpec {

    @Inject
    EmbeddedServer server; // (1)

    @Inject
    @Client("/")
    HttpClient client; // (2)

    @Test
    void testHelloWorldResponse() {
        String response = client.toBlocking() // (3)
                .retrieve(HttpRequest.GET("/hello"));
        assertEquals("Hello World", response); // (4)
    }
}
Testing Hello World
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification

import jakarta.inject.Inject

@MicronautTest
class HelloControllerSpec extends Specification {

    @Inject
    EmbeddedServer embeddedServer // (1)

    @Inject
    @Client("/")
    HttpClient client // (2)

    void "test hello world response"() {
        expect:
            client.toBlocking() // (3)
                    .retrieve(HttpRequest.GET('/hello')) == "Hello World" // (4)
    }
}
Testing Hello World
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import jakarta.inject.Inject

@MicronautTest
class HelloControllerSpec {

    @Inject
    lateinit var server: EmbeddedServer // (1)

    @Inject
    @field:Client("/")
    lateinit var client: HttpClient // (2)

    @Test
    fun testHelloWorldResponse() {
        val rsp: String = client.toBlocking() // (3)
                .retrieve("/hello")
        assertEquals("Hello World", rsp) // (4)
    }
}
1 The EmbeddedServer is configured as a shared test field
2 A HttpClient instance shared field is also defined
3 The test uses the toBlocking() method to make a blocking call
4 The retrieve method returns the controller response as a String

In addition to a low-level client, the Micronaut framework features a declarative, compile-time HTTP client, powered by the Client annotation.

To create a client, create an interface annotated with @Client, for example:

src/main/java/hello/world/HelloClient.java
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import org.reactivestreams.Publisher;
import io.micronaut.core.async.annotation.SingleResult;

@Client("/hello") // (1)
public interface HelloClient {

    @Get(consumes = MediaType.TEXT_PLAIN) // (2)
    @SingleResult
    Publisher<String> hello(); // (3)
}
src/main/java/hello/world/HelloClient.java
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import org.reactivestreams.Publisher
import io.micronaut.core.async.annotation.SingleResult

@Client("/hello") // (1)
interface HelloClient {

    @Get(consumes = MediaType.TEXT_PLAIN) // (2)
    @SingleResult
    Publisher<String> hello() // (3)
}
src/main/java/hello/world/HelloClient.java
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import io.micronaut.core.async.annotation.SingleResult
import org.reactivestreams.Publisher

@Client("/hello") // (1)
interface HelloClient {

    @Get(consumes = [MediaType.TEXT_PLAIN]) // (2)
    @SingleResult
    fun hello(): Publisher<String>  // (3)
}
1 The @Client annotation is used with a value that is a relative path to the current server
2 The same @Get annotation used on the server is used to define the client mapping
3 A Publisher annotated with SingleResult is returned with the value read from the server

To test the HelloClient, retrieve it from the ApplicationContext associated with the server:

Testing HelloClient
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import jakarta.inject.Inject;
import reactor.core.publisher.Mono;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest // (1)
public class HelloClientSpec {

    @Inject
    HelloClient client; // (2)

    @Test
    public void testHelloWorldResponse() {
        assertEquals("Hello World", Mono.from(client.hello()).block());// (3)
    }
}
Testing HelloClient
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import reactor.core.publisher.Mono
import spock.lang.Specification

import jakarta.inject.Inject

@MicronautTest // (1)
class HelloClientSpec extends Specification {

    @Inject HelloClient client // (2)

    void "test hello world response"() {
        expect:
        Mono.from(client.hello()).block() == "Hello World" // (3)
    }

}
Testing HelloClient
import io.micronaut.context.annotation.Property
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import jakarta.inject.Inject
import reactor.core.publisher.Mono


@MicronautTest // (1)
class HelloClientSpec {

    @Inject
    lateinit var client: HelloClient // (2)

    @Test
    fun testHelloWorldResponse() {
        assertEquals("Hello World", Mono.from(client.hello()).block())// (3)
    }
}
1 The @MicronautTest annotation defines the test
2 The HelloClient is injected from the ApplicationContext
3 The client is invoked using the Project Reactor Mono::block method

The Client annotation produces an implementation automatically for you at compile time without the using proxies or runtime reflection.

The Client annotation is very flexible. See the section on the Micronaut HTTP Client for more information.

2.5 Deploying the Application

To deploy a Micronaut application you create an executable JAR file by running ./gradlew assemble or ./mvnw package.

The constructed JAR file can then be executed with java -jar. For example:

$ java -jar build/libs/hello-world-0.1-all.jar

if building with Gradle, or

$ java -jar target/hello-world.jar

if building with Maven.

The executable JAR can be run locally, or deployed to a virtual machine or managed Cloud service that supports executable JARs.

To publish a layered application to a Docker container registry, configure your Docker image name in build.gradle for Gradle:

dockerBuild {
    images = ["[REPO_URL]/[NAMESPACE]/my-image:$project.version"]
}

Then use dockerPush to push a built image of the application:

$ ./gradlew dockerPush

For Maven, define the following plugin in your POM:

<plugin>
  <groupId>com.google.cloud.tools</groupId>
  <artifactId>jib-maven-plugin</artifactId>
  <configuration>
    <to>
      <image>docker.io/my-company/my-image:${project.version}</image>
    </to>
  </configuration>
</plugin>

Then invoke the deploy lifecycle phase specifying the packaging type as either docker or docker-native:

$ ./mvnw deploy -Dpackaging=docker

Deployment Guides

See the following guides to learn more about deploying Micronaut Applications:

3 Inversion of Control

Unlike other frameworks which rely on runtime reflection and proxies, the Micronaut framework uses compile time data to implement dependency injection.

This is a similar approach taken by tools such as Google Dagger, which is designed primarily with Android in mind. Micronaut, on the other hand, is designed for building server-side microservices and provides many of the same tools and utilities as other frameworks but without using reflection or caching excessive amounts of reflection metadata.

The goals of the Micronaut IoC container are summarized as:

  • Use reflection as a last resort

  • Avoid proxies

  • Optimize start-up time

  • Reduce memory footprint

  • Provide clear, understandable error handling

Note that the IoC part of the Micronaut framework can be used completely independently of Micronaut for whatever application type you wish to build.

To do so, configure your build to include the micronaut-inject-java dependency as an annotation processor.

The easiest way to do this is with Micronaut’s Gradle or Maven plugins. For example with Gradle:

Configuring Gradle
plugins {
    id 'io.micronaut.library' version '1.3.2' (1)
}

version "0.1"
group "com.example"

repositories {
    mavenCentral()
}

micronaut {
    version = "4.3.11" (2)
}
1 Define the Micronaut Library plugin
2 Specify the Micronaut framework version to use

The entry point for IoC is then the ApplicationContext interface, which includes a run method. The following example demonstrates using it:

Running the ApplicationContext
try (ApplicationContext context = ApplicationContext.run()) { (1)
    MyBean myBean = context.getBean(MyBean.class); (2)
    // do something with your bean
}
1 Run the ApplicationContext
2 Retrieve a bean from the ApplicationContext
The example uses Java try-with-resources syntax to ensure the ApplicationContext is cleanly shutdown when the application exits.

3.1 Defining Beans

A bean is an object whose lifecycle is managed by the Micronaut IoC container. That lifecycle may include creation, execution, and destruction. Micronaut implements the JSR-330 (jakarta.inject) - Dependency Injection for Java specification, hence to use Micronaut you simply use the annotations provided by jakarta.inject.

The following is a simple example:

public interface Engine { // (1)
    int getCylinders();
    String start();
}

@Singleton// (2)
public class V8Engine implements Engine {
    private int cylinders = 8;

    @Override
    public String start() {
        return "Starting V8";
    }

    @Override
    public int getCylinders() {
        return cylinders;
    }

    public void setCylinders(int cylinders) {
        this.cylinders = cylinders;
    }
}

@Singleton
public class Vehicle {
    private final Engine engine;

    public Vehicle(Engine engine) {// (3)
        this.engine = engine;
    }

    public String start() {
        return engine.start();
    }
}
interface Engine { // (1)
    int getCylinders()
    String start()
}

@Singleton // (2)
class V8Engine implements Engine {
    int cylinders = 8

    @Override
    String start() {
        "Starting V8"
    }
}

@Singleton
class Vehicle {
    final Engine engine

    Vehicle(Engine engine) { // (3)
        this.engine = engine
    }

    String start() {
        engine.start()
    }
}
interface Engine {
    // (1)
    val cylinders: Int

    fun start(): String
}

@Singleton// (2)
class V8Engine : Engine {

    override var cylinders = 8

    override fun start(): String {
        return "Starting V8"
    }
}

@Singleton
class Vehicle(private val engine: Engine) { // (3)
    fun start(): String {
        return engine.start()
    }
}
1 A common Engine interface is defined
2 A V8Engine implementation is defined and marked with Singleton scope
3 The Engine is injected via constructor injection

To perform dependency injection, run the BeanContext using the run() method and lookup a bean using getBean(Class), as per the following example:

final ApplicationContext context = ApplicationContext.run();
Vehicle vehicle = context.getBean(Vehicle.class);
System.out.println(vehicle.start());
ApplicationContext context = ApplicationContext.run()
Vehicle vehicle = context.getBean(Vehicle)
println vehicle.start()
val context = ApplicationContext.run()
val vehicle = context.getBean(Vehicle::class.java)
println(vehicle.start())

The Micronaut framework automatically discovers dependency injection metadata on the classpath and wires the beans together according to injection points you define.

The Micronaut framework supports the following types of dependency injection:

  • Constructor injection (must be one public constructor or a single constructor annotated with @Inject)

  • Field injection

  • JavaBean property injection

  • Method parameter injection

Classes or particular fields, methods can be excluded by adding an annotation @Vetoed
See the guide for Micronaut Dependency Injection Types to learn more.

3.2 How Does it Work?

At this point, you may be wondering how Micronaut framework performs the above dependency injection without requiring reflection.

The key is a set of AST transformations (for Groovy) and annotation processors (for Java) that generate classes that implement the BeanDefinition interface.

Micronaut framework uses the ASM bytecode library to generate classes, and because Micronaut knows ahead of time the injection points, there is no need to scan all methods, fields, constructors, etc. at runtime like other frameworks such as Spring do.

Also, since reflection is not used when constructing the bean, the JVM can inline and optimize the code far better, resulting in better runtime performance and reduced memory consumption. This is particularly important for non-singleton scopes where application performance depends on bean creation performance.

In addition, with Micronaut framework your application startup time and memory consumption are not affected by the size of your codebase in the same way as with a framework that uses reflection. Reflection-based IoC frameworks load and cache reflection data for every single field, method, and constructor in your code. Thus, as your code grows in size so do your memory requirements, whilst with Micronaut this is not the case.

3.3 The BeanContext

The BeanContext is a container object for all your bean definitions (it also implements BeanDefinitionRegistry).

It is also the point of initialization for Micronaut. Generally speaking however, you don’t interact directly with the BeanContext API and can simply use jakarta.inject annotations and the annotations in the io.micronaut.context.annotation package for your dependency injection needs.

3.4 Injectable Container Types

In addition to being able to inject beans, Micronaut framework natively supports injecting the following types:

Table 1. Injectable Container Types
Type Description Example

java.util.Optional

An Optional of a bean. empty() is injected if the bean doesn’t exist

Optional<Engine>

java.util.Collection

An Collection or subtype of Collection (e.g. List, Set, etc.)

Collection<Engine>

java.util.Map

An Map or subtype of Map (e.g. LinkedHashMap, TreeMap, etc.) where the key is the qualifier

Map<String, Engine>

java.util.stream.Stream

A lazy Stream of beans

Stream<Engine>

Array

A native array of beans of a given type

Engine[]

Provider

A jakarta.inject.Provider if a circular dependency requires it, or to instantiate a prototype for each get call.

Provider<Engine>

BeanProvider

A io.micronaut.context.BeanProvider if a circular dependency requires it or to instantiate a prototype for each get call.

BeanProvider<Engine>

There are 3 different provider types supported, however the BeanProvider is the one we suggest to use.

When injecting a java.lang.Collection, or java.util.stream.Stream, Array of beans into a bean matching the injection type, then the owning bean will not be a member of the injected collection. A common pattern demonstrating this is aggregation. For example:

@Singleton
class AggregateEngine implements Engine {
  @Inject
  List<Engine> engines;

  @Override
  public void start() {
    engines.forEach(Engine::start);
  }

  ...
}

In this example, the injected member variable engines will not contain an instance of AggregateEngine

A prototype bean will have one instance created per place the bean is injected. When a prototype bean is injected as a provider, each call to get() creates a new instance.

Collection Ordering

When injecting a collection of beans, they are not ordered by default. Implement the Ordered interface to inject an ordered collection. If the requested bean type does not implement Ordered, Micronaut framework searches for the @Order annotation on beans.

The @Order annotation is especially useful for ordering beans created by factories where the bean type is a class in a third-party library. In this example, both LowRateLimit and HighRateLimit implement the RateLimit interface.

Factory with @Order
import io.micronaut.context.annotation.Factory;
import io.micronaut.core.annotation.Order;

import jakarta.inject.Singleton;
import java.time.Duration;

@Factory
public class RateLimitsFactory {

    @Singleton
    @Order(20)
    LowRateLimit rateLimit2() {
        return new LowRateLimit(Duration.ofMinutes(50), 100);
    }

    @Singleton
    @Order(10)
    HighRateLimit rateLimit1() {
        return new HighRateLimit(Duration.ofMinutes(50), 1000);
    }
}
Factory with @Order
import io.micronaut.context.annotation.Factory
import io.micronaut.core.annotation.Order

import jakarta.inject.Singleton
import java.time.Duration

@Factory
class RateLimitsFactory {

    @Singleton
    @Order(20)
    LowRateLimit rateLimit2() {
        new LowRateLimit(Duration.ofMinutes(50), 100);
    }

    @Singleton
    @Order(10)
    HighRateLimit rateLimit1() {
        new HighRateLimit(Duration.ofMinutes(50), 1000);
    }
}
Factory with @Order
import io.micronaut.context.annotation.Factory
import io.micronaut.core.annotation.Order
import java.time.Duration
import jakarta.inject.Singleton

@Factory
class RateLimitsFactory {

    @Singleton
    @Order(20)
    fun rateLimit2(): LowRateLimit {
        return LowRateLimit(Duration.ofMinutes(50), 100)
    }

    @Singleton
    @Order(10)
    fun rateLimit1(): HighRateLimit {
        return HighRateLimit(Duration.ofMinutes(50), 1000)
    }
}

When a collection of RateLimit beans are requested from the context, they are returned in ascending order based on the value in the annotation.

Injecting a Bean by Order

When injecting a single instance of a bean the @Order annotation can also be used to define which bean has the highest precedence and hence should be injected.

The Ordered interface is not taken into account when selecting a single instance as this would require instantiating the bean to resolve the order.

3.5 Bean Qualifiers

If you have multiple possible implementations for a given interface to inject, you need to use a qualifier.

Once again Micronaut framework leverages JSR-330 and the Qualifier and Named annotations to support this use case.

Qualifying By Name

To qualify by name, use the Named annotation. For example, consider the following classes:

public interface Engine { // (1)
    int getCylinders();
    String start();
}

@Singleton
public class V6Engine implements Engine {  // (2)
    @Override
    public String start() {
        return "Starting V6";
    }

    @Override
    public int getCylinders() {
        return 6;
    }
}

@Singleton
public class V8Engine implements Engine {  // (3)
    @Override
    public String start() {
        return "Starting V8";
    }

    @Override
    public int getCylinders() {
        return 8;
    }

}

@Singleton
public class Vehicle {
    private final Engine engine;

    @Inject
    public Vehicle(@Named("v8") Engine engine) {// (4)
        this.engine = engine;
    }

    public String start() {
        return engine.start();// (5)
    }
}
interface Engine { // (1)
    int getCylinders()
    String start()
}

@Singleton
class V6Engine implements Engine { // (2)
    int cylinders = 6

    @Override
    String start() {
        "Starting V6"
    }
}

@Singleton
class V8Engine implements Engine { // (3)
    int cylinders = 8

    @Override
    String start() {
        "Starting V8"
    }
}

@Singleton
class Vehicle {
    final Engine engine

    @Inject Vehicle(@Named('v8') Engine engine) { // (4)
        this.engine = engine
    }

    String start() {
        engine.start() // (5)
    }
}
interface Engine { // (1)
    val cylinders: Int
    fun start(): String
}

@Singleton
class V6Engine : Engine {  // (2)

    override var cylinders: Int = 6

    override fun start(): String {
        return "Starting V6"
    }
}

@Singleton
class V8Engine : Engine {

    override var cylinders: Int = 8

    override fun start(): String {
        return "Starting V8"
    }

}

@Singleton
class Vehicle @Inject
constructor(@param:Named("v8") private val engine: Engine) { // (4)

    fun start(): String {
        return engine.start() // (5)
    }
}
1 The Engine interface defines the common contract
2 The V6Engine class is the first implementation
3 The V8Engine class is the second implementation
4 The jakarta.inject.Named annotation indicates that the V8Engine implementation is required
5 Calling the start method prints: "Starting V8"

Micronaut framework is capable of injecting V8Engine in the previous example, because:

@Named qualifier value (v8) + type being injected simple name (Engine) == (case-insensitive) == The simple name of a bean of type Engine (V8Engine)

You can also declare @Named at the class level of a bean to explicitly define the name of the bean.

Qualifying By Annotation

In addition to being able to qualify by name, you can build your own qualifiers using the Qualifier annotation. For example, consider the following annotation:

import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Qualifier
@Retention(RUNTIME)
public @interface V8 {
}
import jakarta.inject.Qualifier
import java.lang.annotation.Retention

import static java.lang.annotation.RetentionPolicy.RUNTIME

@Qualifier
@Retention(RUNTIME)
@interface V8 {
}
import jakarta.inject.Qualifier
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy.RUNTIME

@Qualifier
@Retention(RUNTIME)
annotation class V8

The above annotation is itself annotated with the @Qualifier annotation to designate it as a qualifier. You can then use the annotation at any injection point in your code. For example:

@Inject Vehicle(@V8 Engine engine) {
    this.engine = engine;
}
@Inject Vehicle(@V8 Engine engine) {
    this.engine = engine
}
@Inject constructor(@V8 val engine: Engine) {

Qualifying By Annotation Members

Since Micronaut framework 3.0, annotation qualifiers can also use annotation members to resolve the correct bean to inject. For example, consider the following annotation:

import io.micronaut.context.annotation.NonBinding;
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Qualifier // (1)
@Retention(RUNTIME)
public @interface Cylinders {
    int value();

    @NonBinding // (2)
    String description() default "";
}
import io.micronaut.context.annotation.NonBinding
import jakarta.inject.Qualifier
import java.lang.annotation.Retention

import static java.lang.annotation.RetentionPolicy.RUNTIME

@Qualifier // (1)
@Retention(RUNTIME)
@interface Cylinders {
    int value();

    @NonBinding // (2)
    String description() default "";
}
import io.micronaut.context.annotation.NonBinding
import jakarta.inject.Qualifier
import kotlin.annotation.Retention

@Qualifier // (1)
@Retention(AnnotationRetention.RUNTIME)
annotation class Cylinders(
    val value: Int,
    @get:NonBinding // (2)
    val description: String = ""
)
1 The @Cylinders annotation is meta-annotated with @Qualifier
2 The annotation has two members. The @NonBinding annotation is used to exclude the description member from being considered during dependency resolution.

You can then use the @Cylinders annotation on any bean and the members that are not annotated with @NonBinding are considered during dependency resolution:

@Singleton
@Cylinders(value = 6, description = "6-cylinder V6 engine")  // (1)
public class V6Engine implements Engine { // (2)

    @Override
    public int getCylinders() {
        return 6;
    }

    @Override
    public String start() {
        return "Starting V6";
    }
}
@Singleton
@Cylinders(value = 6, description = "6-cylinder V6 engine")  // (1)
class V6Engine implements Engine { // (2)

    @Override
    int getCylinders() {
        return 6
    }

    @Override
    String start() {
        return "Starting V6"
    }
}
@Singleton
@Cylinders(value = 6, description = "6-cylinder V6 engine") // (1)
class V6Engine : Engine { // (2)
    // (2)
    override val cylinders: Int
        get() = 6

    override fun start(): String {
        return "Starting V6"
    }
}
1 Here the value member is set to 6 for the V6Engine type
2 The class implements an Engine interface
@Singleton
@Cylinders(value = 8, description = "8-cylinder V8 engine") // (1)
public class V8Engine implements Engine { // (2)
    @Override
    public int getCylinders() {
        return 8;
    }

    @Override
    public String start() {
        return "Starting V8";
    }
}
@Singleton
@Cylinders(value = 8, description = "8-cylinder V8 engine") // (1)
class V8Engine implements Engine { // (2)
    @Override
    int getCylinders() {
        return 8
    }

    @Override
    String start() {
        return "Starting V8"
    }
}
@Singleton
@Cylinders(value = 8, description = "8-cylinder V8 engine") // (1)
class V8Engine : Engine { // (2)
    override val cylinders: Int
        get() = 8

    override fun start(): String {
        return "Starting V8"
    }
}
1 Here the value member is set to 8 for the V8Engine type
2 The class implements an Engine interface

You can then use the @Cylinders qualifier on any injection point to select the correct bean to inject. For example:

@Inject Vehicle(@Cylinders(8) Engine engine) {
    this.engine = engine;
}
@Inject Vehicle(@Cylinders(8) Engine engine) {
    this.engine = engine
}
@Singleton
class Vehicle(@param:Cylinders(8) val engine: Engine) {
    fun start(): String {
        return engine.start()
    }
}

Qualifying by Generic Type Arguments

Since Micronaut framework 3.0, it is possible to select which bean to inject based on the generic type arguments of the class or interface. Consider the following example:

public interface CylinderProvider {
    int getCylinders();
}
interface CylinderProvider {
    int getCylinders()
}
interface CylinderProvider {
    val cylinders: Int
}

The CylinderProvider interface provides the number of cylinders.

public interface Engine<T extends CylinderProvider> { // (1)
    default int getCylinders() {
        return getCylinderProvider().getCylinders();
    }

    default String start() {
        return "Starting " + getCylinderProvider().getClass().getSimpleName();
    }

    T getCylinderProvider();
}
interface Engine<T extends CylinderProvider> { // (1)
    default int getCylinders() { cylinderProvider.cylinders }

    default String start() { "Starting ${cylinderProvider.class.simpleName}" }

    T getCylinderProvider()
}
interface Engine<T : CylinderProvider> { // (1)
    val cylinders: Int
        get() = cylinderProvider.cylinders

    fun start(): String {
        return "Starting ${cylinderProvider.javaClass.simpleName}"
    }

    val cylinderProvider: T
}
1 The engine class defines a generic type argument <T> that must be an instance of CylinderProvider

You can define implementations of the Engine interface with different generic type arguments. For example for a V6 engine:

public class V6 implements CylinderProvider {
    @Override
    public int getCylinders() {
        return 6;
    }
}
class V6 implements CylinderProvider {
    @Override
    int getCylinders() { 6 }
}
class V6 : CylinderProvider {
    override val cylinders: Int = 6
}

The above defines a V6 class that implements the CylinderProvider interface.

@Singleton
public class V6Engine implements Engine<V6> {  // (1)
    @Override
    public V6 getCylinderProvider() {
        return new V6();
    }
}
@Singleton
class V6Engine implements Engine<V6> {  // (1)
    @Override
    V6 getCylinderProvider() { new V6() }
}
@Singleton
class V6Engine : Engine<V6> { // (1)
    override val cylinderProvider: V6
        get() = V6()
}
1 The V6Engine implements Engine providing V6 as a generic type parameter

And a V8 engine:

public class V8 implements CylinderProvider {
    @Override
    public int getCylinders() {
        return 8;
    }
}
class V8 implements CylinderProvider {
    @Override
    int getCylinders() { 8 }
}
class V8 : CylinderProvider {
    override val cylinders: Int = 8
}

The above defines a V8 class that implements the CylinderProvider interface.

@Singleton
public class V8Engine implements Engine<V8> {  // (1)
    @Override
    public V8 getCylinderProvider() {
        return new V8();
    }
}
@Singleton
class V8Engine implements Engine<V8> {  // (1)
    @Override
    V8 getCylinderProvider() { new V8() }
}
@Singleton
class V8Engine : Engine<V8> { // (1)
    override val cylinderProvider: V8
        get() = V8()
}
1 The V8Engine implements Engine providing V8 as a generic type parameter

You can then use the generic arguments when defining the injection point and Micronaut framework will pick the correct bean to inject based on the specific generic type arguments:

@Inject
public Vehicle(Engine<V8> engine) {
    this.engine = engine;
}
@Inject
Vehicle(Engine<V8> engine) {
    this.engine = engine
}
@Singleton
class Vehicle(val engine: Engine<V8>) {

In the above example the V8Engine bean is injected.

Primary and Secondary Beans

Primary is a qualifier that indicates that a bean is the primary bean to be selected in the case of multiple interface implementations.

Consider the following example:

public interface ColorPicker {
    String color();
}
interface ColorPicker {
    String color()
}
interface ColorPicker {
    fun color(): String
}

ColorPicker is implemented by these classes:

The Primary Bean
import io.micronaut.context.annotation.Primary;
import jakarta.inject.Singleton;

@Primary
@Singleton
class Green implements ColorPicker {

    @Override
    public String color() {
        return "green";
    }
}
The Primary Bean
import io.micronaut.context.annotation.Primary
import jakarta.inject.Singleton

@Primary
@Singleton
class Green implements ColorPicker {

    @Override
    String color() {
        return "green"
    }
}
The Primary Bean
import io.micronaut.context.annotation.Primary
import jakarta.inject.Singleton

@Primary
@Singleton
class Green: ColorPicker {
    override fun color(): String {
        return "green"
    }
}

The Green bean class implements ColorPicker and is annotated with @Primary.

Another Bean of the Same Type
import jakarta.inject.Singleton;

@Singleton
public class Blue implements ColorPicker {

    @Override
    public String color() {
        return "blue";
    }
}
Another Bean of the Same Type
import jakarta.inject.Singleton

@Singleton
class Blue implements ColorPicker {

    @Override
    String color() {
        return "blue"
    }
}
Another Bean of the Same Type
import jakarta.inject.Singleton

@Singleton
class Blue: ColorPicker {
    override fun color(): String {
        return "blue"
    }
}

The Blue bean class also implements ColorPicker and hence you have two possible candidates when injecting the ColorPicker interface. Since Green is the primary, it will always be favoured.

@Controller("/testPrimary")
public class TestController {

    protected final ColorPicker colorPicker;

    public TestController(ColorPicker colorPicker) { // (1)
        this.colorPicker = colorPicker;
    }

    @Get
    public String index() {
        return colorPicker.color();
    }
}
@Controller("/test")
class TestController {

    protected final ColorPicker colorPicker

    TestController(ColorPicker colorPicker) { // (1)
        this.colorPicker = colorPicker
    }

    @Get
    String index() {
        colorPicker.color()
    }
}
@Controller("/test")
class TestController(val colorPicker: ColorPicker) { // (1)

    @Get
    fun index(): String {
        return colorPicker.color()
    }
}
1 Although there are two ColorPicker beans, Green gets injected due to the @Primary annotation.

If multiple possible candidates are present and no @Primary is defined a NonUniqueBeanException is thrown.

In addition to @Primary, there is also a Secondary annotation which causes the opposite effect and allows de-prioritizing a bean.

See the guide for Micronaut Patterns - Composite to learn more.

Injecting Any Bean

If you are not particular about which bean gets injected then you can use the @Any qualifier which will inject the first available bean, for example:

Injecting Any Instance
@Inject @Any
Engine engine;
Injecting Any Instance
@Inject @Any
Engine engine
Injecting Any Instance
@Inject
@field:Any
lateinit var engine: Engine

The @Any qualifier is typically used in conjunction with the BeanProvider interface to allow more dynamic use cases. For example the following Vehicle implementation will start the Engine if the bean is present:

Using BeanProvider with Any
import io.micronaut.context.BeanProvider;
import io.micronaut.context.annotation.Any;
import jakarta.inject.Singleton;

@Singleton
public class Vehicle {
    final BeanProvider<Engine> engineProvider;

    public Vehicle(@Any BeanProvider<Engine> engineProvider) { // (1)
        this.engineProvider = engineProvider;
    }
    void start() {
        engineProvider.ifPresent(Engine::start); // (2)
    }
}
Using BeanProvider with Any
import io.micronaut.context.BeanProvider
import io.micronaut.context.annotation.Any
import jakarta.inject.Singleton

@Singleton
class Vehicle {
    final BeanProvider<Engine> engineProvider

    Vehicle(@Any BeanProvider<Engine> engineProvider) { // (1)
        this.engineProvider = engineProvider
    }
    void start() {
        engineProvider.ifPresent(Engine::start) // (2)
    }
}
Using BeanProvider with Any
import io.micronaut.context.BeanProvider
import io.micronaut.context.annotation.Any
import jakarta.inject.Singleton

@Singleton
class Vehicle(@param:Any val engineProvider: BeanProvider<Engine>) { // (1)
    fun start() {
        engineProvider.ifPresent { it.start() } // (2)
    }
    fun startAll() {
        if (engineProvider.isPresent) { // (1)
            engineProvider.forEach { it.start() } // (2)
        }
}
1 Use @Any to inject the BeanProvider
2 Call the start method if the underlying bean is present using the ifPresent method

If there are multiple beans you can also adapt the behaviour. The following example starts all the engines installed in the Vehicle if any are present:

Using BeanProvider with Any
void startAll() {
    if (engineProvider.isPresent()) { // (1)
        engineProvider.stream().forEach(Engine::start); // (2)
    }
}
Using BeanProvider with Any
void startAll() {
    if (engineProvider.isPresent()) { // (1)
        engineProvider.each {it.start() } // (2)
    }
}
Using BeanProvider with Any
fun startAll() {
    if (engineProvider.isPresent) { // (1)
        engineProvider.forEach { it.start() } // (2)
    }
1 Check if any beans present
2 If so iterate over each one via the stream().forEach(..) method, starting the engines

3.6 Limiting Injectable Types

By default, when you annotate a bean with a scope such as @Singleton the bean class and all interfaces it implements and super classes it extends from become injectable via @Inject.

Consider the following example from the previous section on defining beans:

@Singleton
public class V8Engine implements Engine {  // (3)
    @Override
    public String start() {
        return "Starting V8";
    }

    @Override
    public int getCylinders() {
        return 8;
    }

}
@Singleton
class V8Engine implements Engine { // (3)
    int cylinders = 8

    @Override
    String start() {
        "Starting V8"
    }
}
@Singleton
class V8Engine : Engine {

    override var cylinders: Int = 8

    override fun start(): String {
        return "Starting V8"
    }

}

In the above case other classes in your application can choose to either inject the interface Engine or the concrete implementation V8Engine.

If this is undesirable you can use the typed member of the @Bean annotation to limit the exposed types. For example:

@Singleton
@Bean(typed = Engine.class) // (1)
public class V8Engine implements Engine {  // (2)
    @Override
    public String start() {
        return "Starting V8";
    }

    @Override
    public int getCylinders() {
        return 8;
    }
}
@Singleton
@Bean(typed = Engine) // (1)
class V8Engine implements Engine {  // (2)
    @Override
    String start() { "Starting V8" }

    @Override
    int getCylinders() { 8 }
}
@Singleton
@Bean(typed = [Engine::class]) // (1)
class V8Engine : Engine { // (2)
    override fun start(): String {
        return "Starting V8"
    }

    override val cylinders: Int = 8
}
1 @Bean(typed=..) is used to only allow injection the interface Engine and not the concrete type
2 The class must implement the class or interface defined by typed otherwise a compilation error will occur

The following test demonstrates the behaviour of typed using programmatic lookup and the BeanContext API:

@MicronautTest
public class EngineSpec {
    @Inject
    BeanContext beanContext;

    @Test
    public void testEngine() {
        assertThrows(NoSuchBeanException.class, () ->
                beanContext.getBean(V8Engine.class) // (1)
        );
        final Engine engine = beanContext.getBean(Engine.class); // (2)
        assertTrue(engine instanceof V8Engine);
    }
}
class EngineSpec extends Specification {
    @Shared @AutoCleanup
    ApplicationContext beanContext = ApplicationContext.run()

    void 'test engine'() {
        when:'the class is looked up'
        beanContext.getBean(V8Engine) // (1)

        then:'a no such bean exception is thrown'
        thrown(NoSuchBeanException)

        and:'it is possible to lookup by the typed interface'
        beanContext.getBean(Engine) instanceof V8Engine // (2)
    }
}
@MicronautTest
class EngineSpec {
    @Inject
    lateinit var beanContext: BeanContext

    @Test
    fun testEngine() {
        assertThrows(NoSuchBeanException::class.java) {
            beanContext.getBean(V8Engine::class.java) // (1)
        }

        val engine = beanContext.getBean(Engine::class.java) // (2)
        assertTrue(engine is V8Engine)
    }
}
1 Trying to lookup V8Engine throws a NoSuchBeanException
2 Whilst looking up the Engine interface succeeds

3.7 Scopes

Micronaut framework features an extensible bean scoping mechanism based on JSR-330. The following default scopes are supported:

3.7.1 Built-In Scopes

Table 1. Micronaut Built-in Scopes
Type Description

@Singleton

Singleton scope indicates only one instance of the bean will exist

@Context

Context scope indicates that the bean will be created at the same time as the ApplicationContext (eager initialization)

@Prototype

Prototype scope indicates that a new instance of the bean is created each time it is injected

@Infrastructure

Infrastructure scope represents a bean that cannot be overridden or replaced using @Replaces because it is critical to the functioning of the system.

@ThreadLocal

@ThreadLocal scope is a custom scope that associates a bean per thread via a ThreadLocal

@Refreshable

@Refreshable scope is a custom scope that allows a bean’s state to be refreshed via the /refresh endpoint.

@RequestScope

@RequestScope scope is a custom scope that indicates a new instance of the bean is created and associated with each HTTP request

The @Prototype annotation is a synonym for @Bean because the default scope is prototype.

Additional scopes can be added by defining a @Singleton bean that implements the CustomScope interface.

Note that when starting an ApplicationContext, by default @Singleton-scoped beans are created lazily and on-demand. This is by design to optimize startup time.

If this presents a problem for your use case you have the option of using the @Context annotation which binds the lifecycle of your object to the lifecycle of the ApplicationContext. In other words when the ApplicationContext is started your bean will be created.

Alternatively, annotate any @Singleton-scoped bean with @Parallel which allows parallel initialization of your bean without impacting overall startup time.

If your bean fails to initialize in parallel, the application will be automatically shut down.

3.7.1.1 Eager Initialization of Singletons

Eager initialization of @Singleton beans maybe desirable in certain scenarios, such as on AWS Lambda where more CPU resources are assigned to Lambda construction than execution.

You can specify whether to eagerly initialize @Singleton-scoped beans using the ApplicationContextBuilder interface:

Enabling Eager Initialization of Singletons
public class Application {

    public static void main(String[] args) {
        Micronaut.build(args)
            .eagerInitSingletons(true) (1)
            .mainClass(Application.class)
            .start();
    }
}
1 Setting eager init to true initializes all singletons

When you use Micronaut framework in environments such as Serverless Functions, you will not have an Application class, and instead you extend a Micronaut-provided class. In those cases, Micronaut provides methods which you can override to enhance the ApplicationContextBuilder

Override of newApplicationContextBuilder()
public class MyFunctionHandler extends MicronautRequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
...
    @Nonnull
    @Override
    protected ApplicationContextBuilder newApplicationContextBuilder() {
        ApplicationContextBuilder builder = super.newApplicationContextBuilder();
        builder.eagerInitSingletons(true);
        return builder;
    }
    ...
}

@ConfigurationReader beans such as @EachProperty or @ConfigurationProperties are singleton beans. To eagerly init configuration but keep other @Singleton-scoped bean creation lazy, use eagerInitConfiguration:

Enabling Eager Initialization of Configuration
public class Application {

    public static void main(String[] args) {
        Micronaut.build(args)
            .eagerInitConfiguration(true) (1)
            .mainClass(Application.class)
            .start();
    }
}
1 Setting eager init to true initializes all configuration reader beans.

3.7.2 Refreshable Scope

The Refreshable scope is a custom scope that allows a bean’s state to be refreshed via:

The following example illustrates @Refreshable scope behavior.

@Refreshable // (1)
static class WeatherService {
    private String forecast;

    @PostConstruct
    public void init() {
        forecast = "Scattered Clouds " + new SimpleDateFormat("dd/MMM/yy HH:mm:ss.SSS").format(new Date());// (2)
    }

    public String latestForecast() {
        return forecast;
    }
}
@Refreshable // (1)
static class WeatherService {

    String forecast

    @PostConstruct
    void init() {
        forecast = "Scattered Clouds ${new SimpleDateFormat("dd/MMM/yy HH:mm:ss.SSS").format(new Date())}" // (2)
    }

    String latestForecast() {
        return forecast
    }
}
@Refreshable // (1)
open class WeatherService {
    private var forecast: String? = null

    @PostConstruct
    open fun init() {
        forecast = "Scattered Clouds " + SimpleDateFormat("dd/MMM/yy HH:mm:ss.SSS").format(Date())// (2)
    }

    open fun latestForecast(): String? {
        return forecast
    }
}
1 The WeatherService is annotated with @Refreshable scope which stores an instance until a refresh event is triggered
2 The value of the forecast property is set to a fixed value when the bean is created and won’t change until the bean is refreshed

If you invoke latestForecast() twice, you will see identical responses such as "Scattered Clouds 01/Feb/18 10:29.199".

When the /refresh endpoint is invoked or a RefreshEvent is published, the instance is invalidated and a new instance is created the next time the object is requested. For example:

applicationContext.publishEvent(new RefreshEvent());
applicationContext.publishEvent(new RefreshEvent())
applicationContext.publishEvent(RefreshEvent())

3.7.3 Scopes on Meta Annotations

Scopes can be defined on meta annotations that you can then apply to your classes. Consider the following example meta annotation:

Driver.java Annotation
import io.micronaut.context.annotation.Requires;

import jakarta.inject.Singleton;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Requires(classes = Car.class) // (1)
@Singleton // (2)
@Documented
@Retention(RUNTIME)
public @interface Driver {
}
Driver.java Annotation
import io.micronaut.context.annotation.Requires

import jakarta.inject.Singleton
import java.lang.annotation.Documented
import java.lang.annotation.Retention

import static java.lang.annotation.RetentionPolicy.RUNTIME

@Requires(classes = Car.class) // (1)
@Singleton // (2)
@Documented
@Retention(RUNTIME)
@interface Driver {
}
Driver.java Annotation
import io.micronaut.context.annotation.Requires
import jakarta.inject.Singleton
import kotlin.annotation.AnnotationRetention.RUNTIME

@Requires(classes = [Car::class]) // (1)
@Singleton // (2)
@MustBeDocumented
@Retention(RUNTIME)
annotation class Driver
1 The scope declares a requirement on a Car class using Requires
2 The annotation is declared as @Singleton

In the example above the @Singleton annotation is applied to the @Driver annotation which results in every class that is annotated with @Driver being regarded as singleton.

Note that in this case it is not possible to alter the scope when the annotation is applied. For example, the following will not override the scope declared by @Driver and is invalid:

Declaring Another Scope
@Driver
@Prototype
class Foo {}

For the scope to be overridable, instead use the DefaultScope annotation on @Driver which allows a default scope to be specified if none other is present:

Using @DefaultScope
@Requires(classes = Car.class)
@DefaultScope(Singleton.class) (1)
@Documented
@Retention(RUNTIME)
public @interface Driver {
}
@Requires(classes = Car.class)
@DefaultScope(Singleton.class) (1)
@Documented
@Retention(RUNTIME)
@interface Driver {
}
@Requires(classes = [Car::class])
@DefaultScope(Singleton::class) (1)
@Documented
@Retention(RUNTIME)
annotation class Driver
1 DefaultScope declares the scope to use if none is specified

3.8 Bean Factories

In many cases, you may want to make available as a bean a class that is not part of your codebase such as those provided by third-party libraries. In this case, you cannot annotate the compiled class. Instead, implement a @Factory.

A factory is a class annotated with the Factory annotation that provides one or more methods annotated with a bean scope annotation. Which annotation you use depends on what scope you want the bean to be in. See the section on bean scopes for more information.

The factory has the default scope singleton and will be destroyed with the context. If you want to dispose the factory after it produces a bean, use @Prototype scope.

The return types of methods annotated with a bean scope annotation are the bean types. This is best illustrated by an example:

@Singleton
class CrankShaft {
}

class V8Engine implements Engine {
    private final int cylinders = 8;
    private final CrankShaft crankShaft;

    public V8Engine(CrankShaft crankShaft) {
        this.crankShaft = crankShaft;
    }

    @Override
    public String start() {
        return "Starting V8";
    }
}

@Factory
class EngineFactory {

    @Singleton
    Engine v8Engine(CrankShaft crankShaft) {
        return new V8Engine(crankShaft);
    }
}
@Singleton
class CrankShaft {
}

class V8Engine implements Engine {
    final int cylinders = 8
    final CrankShaft crankShaft

    V8Engine(CrankShaft crankShaft) {
        this.crankShaft = crankShaft
    }

    @Override
    String start() {
        "Starting V8"
    }
}

@Factory
class EngineFactory {

    @Singleton
    Engine v8Engine(CrankShaft crankShaft) {
        new V8Engine(crankShaft)
    }
}
@Singleton
internal class CrankShaft

internal class V8Engine(private val crankShaft: CrankShaft) : Engine {
    private val cylinders = 8

    override fun start(): String {
        return "Starting V8"
    }
}

@Factory
internal class EngineFactory {

    @Singleton
    fun v8Engine(crankShaft: CrankShaft): Engine {
        return V8Engine(crankShaft)
    }
}

In this case, a V8Engine is created by the EngineFactory class' v8Engine method. Note that you can inject parameters into the method, and they will be resolved as beans. The resulting V8Engine bean will be a singleton.

A factory can have multiple methods annotated with bean scope annotations, each one returning a distinct bean type.

If you take this approach you should not invoke other bean methods internally within the class. Instead, inject the types via parameters.
To allow the resulting bean to participate in the application context shutdown process, annotate the method with @Bean and set the preDestroy argument to the name of the method to be called to close the bean.

Beans from Fields

With Micronaut framework 3.0 or above it is also possible to produce beans from fields by declaring the @Bean annotation on a field.

Whilst generally this approach should be discouraged in favour for factory methods, which provide more flexibility it does simplify testing code. For example with bean fields you can easily produce mocks in your test code:

import io.micronaut.context.annotation.*;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest
public class VehicleMockSpec {
    @Requires(beans = VehicleMockSpec.class)
    @Bean @Replaces(Engine.class)
    Engine mockEngine = () -> "Mock Started"; // (1)

    @Inject Vehicle vehicle; // (2)

    @Test
    void testStartEngine() {
        final String result = vehicle.start();
        assertEquals("Mock Started", result); // (3)
    }
}
import io.micronaut.context.annotation.*
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification
import jakarta.inject.Inject

@MicronautTest
class VehicleMockSpec extends Specification {
    @Requires(beans=VehicleMockSpec.class)
    @Bean @Replaces(Engine.class)
    Engine mockEngine = {-> "Mock Started" } as Engine  // (1)

    @Inject Vehicle vehicle // (2)

    void "test start engine"() {
        given:
        final String result = vehicle.start()

        expect:
        result == "Mock Started" // (3)
    }
}
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Replaces
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import jakarta.inject.Inject

@MicronautTest
class VehicleMockSpec {
    @get:Bean
    @get:Replaces(Engine::class)
    val mockEngine: Engine = object : Engine { // (1)
        override fun start(): String {
            return "Mock Started"
        }
    }

    @Inject
    lateinit var vehicle : Vehicle // (2)

    @Test
    fun testStartEngine() {
        val result = vehicle.start()
        Assertions.assertEquals("Mock Started", result) // (3)
    }
}
1 A bean is defined from a field that replaces the existing Engine.
2 The Vehicle is injected.
3 The code asserts that the mock implementation is called.

Note that only public or package protected fields are supported on non-primitive types. If the field is static, private, or protected a compilation error will occur.

If the bean method/field includes a scope or a qualifier any scope or qualifiers from the type will be omitted.
Qualifiers from the factory instance aren’t inherited to the beans.

Primitive Beans and Arrays

Since Micronaut framework 3.1 it is possible to define and inject primitive types and array types from factories.

For example:

import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import jakarta.inject.Named;

@Factory
class CylinderFactory {
    @Bean
    @Named("V8") // (1)
    final int v8 = 8;

    @Bean
    @Named("V6") // (1)
    final int v6 = 6;
}
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import jakarta.inject.Named

@Factory
class CylinderFactory {
    @Bean
    @Named("V8") // (1)
    final int v8 = 8

    @Bean
    @Named("V6") // (1)
    final int v6 = 6
}
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import jakarta.inject.Named

@Factory
class CylinderFactory {
    @get:Bean
    @get:Named("V8") // (1)
    val v8 = 8

    @get:Bean
    @get:Named("V6") // (1)
    val v6 = 6
}
1 Two primitive integer beans are defined with different names

Primitive beans can be injected like any other bean:

import jakarta.inject.Named;
import jakarta.inject.Singleton;

@Singleton
public class V8Engine {
    private final int cylinders;

    public V8Engine(@Named("V8") int cylinders) { // (1)
        this.cylinders = cylinders;
    }

    public int getCylinders() {
        return cylinders;
    }
}
import jakarta.inject.Named
import jakarta.inject.Singleton

@Singleton
class V8Engine {
    private final int cylinders

    V8Engine(@Named("V8") int cylinders) { // (1)
        this.cylinders = cylinders
    }

    int getCylinders() {
        return cylinders
    }
}
import jakarta.inject.Named
import jakarta.inject.Singleton

@Singleton
class V8Engine(
    @param:Named("V8") val cylinders: Int // (1)
)

Note that primitive beans and primitive array beans have the following limitations:

  • AOP advice cannot be applied to primitives or wrapper types

  • Due to the above custom scopes that proxy are not supported

  • The @Bean(preDestroy=..) member is not supported

Programmatically Disabling Beans

Factory methods can throw DisabledBeanException to conditionally disable beans. Using @Requires should always be the preferred method to conditionally create beans; throwing an exception in a factory method should only be done if using @Requires is not possible.

For example:

public interface Engine {
    Integer getCylinders();
}

@EachProperty("engines")
public class EngineConfiguration implements Toggleable {

    private boolean enabled = true;
    private Integer cylinders;

    @NotNull
    public Integer getCylinders() {
        return cylinders;
    }

    public void setCylinders(Integer cylinders) {
        this.cylinders = cylinders;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

}

@Factory
public class EngineFactory {

    @EachBean(EngineConfiguration.class)
    public Engine buildEngine(EngineConfiguration engineConfiguration) {
        if (engineConfiguration.isEnabled()) {
            return engineConfiguration::getCylinders;
        } else {
            throw new DisabledBeanException("Engine configuration disabled");
        }
    }
}
interface Engine {
    Integer getCylinders()
}

@EachProperty("engines")
class EngineConfiguration implements Toggleable {
    boolean enabled = true
    @NotNull
    Integer cylinders
}

@Factory
class EngineFactory {

    @EachBean(EngineConfiguration)
    Engine buildEngine(EngineConfiguration engineConfiguration) {
        if (engineConfiguration.enabled) {
            (Engine) { -> engineConfiguration.cylinders }
        } else {
            throw new DisabledBeanException("Engine configuration disabled")
        }
    }
}
interface Engine {
    fun getCylinders(): Int
}

@EachProperty("engines")
class EngineConfiguration : Toggleable {

    var enabled = true

    @NotNull
    val cylinders: Int? = null

    override fun isEnabled(): Boolean {
        return enabled
    }
}

@Factory
class EngineFactory {

    @EachBean(EngineConfiguration::class)
    fun buildEngine(engineConfiguration: EngineConfiguration): Engine? {
        return if (engineConfiguration.isEnabled) {
            object : Engine {
                override fun getCylinders(): Int {
                    return engineConfiguration.cylinders!!
                }
            }
        } else {
            throw DisabledBeanException("Engine configuration disabled")
        }
    }
}

Injection Point

A common use case with factories is to take advantage of annotation metadata from the point at which an object is injected such that behaviour can be modified based on said metadata.

Consider an annotation such as the following:

@Documented
@Retention(RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Cylinders {
    int value() default 8;
}
@Documented
@Retention(RUNTIME)
@Target(ElementType.PARAMETER)
@interface Cylinders {
    int value() default 8
}
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Cylinders(val value: Int = 8)

The above annotation could be used to customize the type of engine we want to inject into a vehicle at the point at which the injection point is defined:

@Singleton
class Vehicle {

    private final Engine engine;

    Vehicle(@Cylinders(6) Engine engine) {
        this.engine = engine;
    }

    String start() {
        return engine.start();
    }
}
@Singleton
class Vehicle {

    private final Engine engine

    Vehicle(@Cylinders(6) Engine engine) {
        this.engine = engine
    }

    String start() {
        return engine.start()
    }
}
@Singleton
internal class Vehicle(@param:Cylinders(6) private val engine: Engine) {
    fun start(): String {
        return engine.start()
    }
}

The above Vehicle class specifies an annotation value of @Cylinders(6) indicating an Engine of six cylinders is required.

To implement this use case, define a factory that accepts the InjectionPoint instance to analyze the defined annotation values:

@Factory
class EngineFactory {

    @Prototype
    Engine v8Engine(InjectionPoint<?> injectionPoint, CrankShaft crankShaft) { // (1)
        final int cylinders = injectionPoint
                .getAnnotationMetadata()
                .intValue(Cylinders.class).orElse(8); // (2)
        switch (cylinders) { // (3)
            case 6:
                return new V6Engine(crankShaft);
            case 8:
                return new V8Engine(crankShaft);
            default:
                throw new IllegalArgumentException("Unsupported number of cylinders specified: " + cylinders);
        }
    }
}
@Factory
class EngineFactory {

    @Prototype
    Engine v8Engine(InjectionPoint<?> injectionPoint, CrankShaft crankShaft) { // (1)
        final int cylinders = injectionPoint
                .getAnnotationMetadata()
                .intValue(Cylinders.class).orElse(8) // (2)
        switch (cylinders) { // (3)
            case 6:
                return new V6Engine(crankShaft)
            case 8:
                return new V8Engine(crankShaft)
            default:
                throw new IllegalArgumentException("Unsupported number of cylinders specified: $cylinders")
        }
    }
}
@Factory
internal class EngineFactory {

    @Prototype
    fun v8Engine(injectionPoint: InjectionPoint<*>, crankShaft: CrankShaft): Engine { // (1)
        val cylinders = injectionPoint
                .annotationMetadata
                .intValue(Cylinders::class.java).orElse(8) // (2)
        return when (cylinders) { // (3)
            6 -> V6Engine(crankShaft)
            8 -> V8Engine(crankShaft)
            else -> throw IllegalArgumentException("Unsupported number of cylinders specified: $cylinders")
        }
    }
}
1 The factory method defines a parameter of type InjectionPoint.
2 The annotation metadata is used to obtain the value of the @Cylinder annotation
3 The value is used to construct an engine, throwing an exception if an engine cannot be constructed.
It is important to note that the factory is declared as @Prototype scope so the method is invoked for each injection point. If the V8Engine and V6Engine types are required to be singletons, the factory should use a Map to ensure the objects are only constructed once.

3.9 Conditional Beans

At times, you may want a bean to load conditionally based on various potential factors including the classpath, the configuration, the presence of other beans, etc.

The Requires annotation provides the ability to define one or many conditions on a bean.

Consider the following example:

Using @Requires
@Singleton
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public class JdbcBookService implements BookService {

    DataSource dataSource;

    public JdbcBookService(DataSource dataSource) {
        this.dataSource = dataSource;
    }
Using @Requires
@Singleton
@Requires(beans = DataSource)
@Requires(property = "datasource.url")
class JdbcBookService implements BookService {

    DataSource dataSource
Using @Requires
@Singleton
@Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url"))
class JdbcBookService(internal var dataSource: DataSource) : BookService {

The above bean defines two requirements. The first indicates that a DataSource bean must be present for the bean to load. The second requirement ensures that the datasource.url property is set before loading the JdbcBookService bean.

Kotlin currently does not support repeatable annotations. Use the @Requirements annotation when multiple requires are needed. For example, @Requirements(Requires(…​), Requires(…​)). See https://youtrack.jetbrains.com/issue/KT-12794 to track this feature.

If multiple beans require the same combination of requirements, you can define a meta-annotation with the requirements:

Using a @Requires meta-annotation
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PACKAGE, ElementType.TYPE})
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public @interface RequiresJdbc {
}
Using a @Requires meta-annotation
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PACKAGE, ElementType.TYPE])
@Requires(beans = DataSource)
@Requires(property = "datasource.url")
@interface RequiresJdbc {
}
Using a @Requires meta-annotation
@Documented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE)
@Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url"))
annotation class RequiresJdbc

In the above example the RequiresJdbc annotation can be used on the JdbcBookService instead:

Using a meta-annotation
@RequiresJdbc
public class JdbcBookService implements BookService {
    ...
}

If you have multiple beans that need to fulfill a given requirement before loading, you may want to consider a bean configuration group, as explained in the next section.

Configuration Requirements

The @Requires annotation is very flexible and can be used for a variety of use cases. The following table summarizes some possibilities:

Table 1. Using @Requires
Requirement Example

Require the presence of one or more classes

@Requires(classes=javax.servlet.Servlet)

Require the absence of one or more classes

@Requires(missing=javax.servlet.Servlet)

Require the presence one or more beans

@Requires(beans=javax.sql.DataSource)

Require the absence of one or more beans

@Requires(missingBeans=javax.sql.DataSource)

Require the environment to be applied

@Requires(env="test")

Require the environment to not be applied

@Requires(notEnv="test")

Require the presence of another configuration package

@Requires(configuration="foo.bar")

Require the absence of another configuration package

@Requires(missingConfigurations="foo.bar")

Require particular SDK version

@Requires(sdk=Sdk.JAVA, value="1.8")

Requires classes annotated with the given annotations to be available to the application via package scanning

@Requires(entities=javax.persistence.Entity)

Require a property with an optional value

@Requires(property="data-source.url")

Require a property to not be part of the configuration

@Requires(missingProperty="data-source.url")

Require the presence of one or more files in the file system

@Requires(resources="file:/path/to/file")

Require the presence of one or more classpath resources

@Requires(resources="classpath:myFile.properties")

Require the current operating system to be in the list

@Requires(os={Requires.Family.WINDOWS})

Require the current operating system to not be in the list

@Requires(notOs={Requires.Family.WINDOWS})

Requires bean to be present in case no beanProperty specified

@Requires(bean=Config.class)

Requires the specified property of bean to be present

@Requires(bean=Config.class, beanProperty="enabled")

Additional Notes on Property Requirements.

Adding a requirement on a property has some additional functionality. You can require the property to be a certain value, not be a certain value, and use a default in those checks if it is not set.

@Requires(property="foo") (1)
@Requires(property="foo", value="John") (2)
@Requires(property="foo", value="John", defaultValue="John") (3)
@Requires(property="foo", notEquals="Sally") (4)
1 Requires the property to be set
2 Requires the property to be "John"
3 Requires the property to be "John" or not set
4 Requires the property to not be "Sally" or not set

Referencing bean properties in @Requires.

You can also reference other beans properties in @Requires to conditionally load beans. Similar to property requirements, you can specify required value or set the value bean property should not be equal to using notEquals annotation member. For the bean property to be checked, the bean of type specified in bean annotation member should be present within context, otherwise conditional bean will not be loaded.

@Requires(bean=Config.class, beanProperty="foo") (1)
@Requires(bean=Config.class, beanProperty="foo", value="John") (2)
@Requires(bean=Config.class, beanProperty="foo", notEquals="Sally") (3)
1 Requires the "foo" property on Config bean to be set
2 Requires the "foo" property on Config bean to be "John"
3 Requires the "foo" property on Config bean to not be "Sally" or not set

Specified bean property is accessed through respective getter method whose presence and availability will be checked at compilation time.

Note that bean property is considered to be present in case it’s value is not null. Keep in mind that primitive properties are initialized with default values such as false for boolean and 0 for int, so they are considered to be set even if no value is explicitly specified for them.

Debugging Conditional Beans

If you have multiple conditions and complex requirements it may become difficult to understand why a particular bean has not been loaded.

To help resolve issues with conditional beans you can enable debug logging for the io.micronaut.context.condition package which will log the reasons why beans were not loaded.

logback.xml
<logger name="io.micronaut.context.condition" level="DEBUG"/>

Consult the logging chapter for details howto setup logging.

3.10 Bean Replacement

One significant difference between Micronaut’s Dependency Injection system and Spring’s is the way beans are replaced.

In a Spring application, beans have names and are overridden by creating a bean with the same name, regardless of the type of the bean. Spring also has the notion of bean registration order, hence in Spring Boot you have @AutoConfigureBefore and @AutoConfigureAfter annotations that control how beans override each other.

This strategy leads to problems that are difficult to debug, for example:

  • Bean loading order changes, leading to unexpected results

  • A bean with the same name overrides another bean with a different type

To avoid these problems, Micronaut’s DI has no concept of bean names or load order. Beans have a type and a Qualifier. You cannot override a bean of a completely different type with another.

A useful benefit of Spring’s approach is that it allows overriding existing beans to customize behaviour. To support the same ability, Micronaut’s DI provides an explicit @Replaces annotation, which integrates nicely with support for Conditional Beans and clearly documents and expresses the intention of the developer.

Any existing bean can be replaced by another bean that declares @Replaces. For example, consider the following class:

JdbcBookService
@Singleton
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public class JdbcBookService implements BookService {

    DataSource dataSource;

    public JdbcBookService(DataSource dataSource) {
        this.dataSource = dataSource;
    }
JdbcBookService
@Singleton
@Requires(beans = DataSource)
@Requires(property = "datasource.url")
class JdbcBookService implements BookService {

    DataSource dataSource
JdbcBookService
@Singleton
@Requirements(Requires(beans = [DataSource::class]), Requires(property = "datasource.url"))
class JdbcBookService(internal var dataSource: DataSource) : BookService {

You can define a class in src/test/java that replaces this class just for your tests:

Using @Replaces
@Replaces(JdbcBookService.class) // (1)
@Singleton
public class MockBookService implements BookService {

    Map<String, Book> bookMap = new LinkedHashMap<>();

    @Override
    public Book findBook(String title) {
        return bookMap.get(title);
    }
}
Using @Replaces
@Replaces(JdbcBookService.class) // (1)
@Singleton
class MockBookService implements BookService {

    Map<String, Book> bookMap = [:]

    @Override
    Book findBook(String title) {
        bookMap.get(title)
    }
}
Using @Replaces
@Replaces(JdbcBookService::class) // (1)
@Singleton
class MockBookService : BookService {

    var bookMap: Map<String, Book> = LinkedHashMap()

    override fun findBook(title: String): Book? {
        return bookMap[title]
    }
}
1 The MockBookService declares that it replaces JdbcBookService

Factory Replacement

The @Replaces annotation also supports a factory argument. That argument allows the replacement of factory beans in their entirety or specific types created by the factory.

For example, it may be desired to replace all or part of the given factory class:

BookFactory
@Factory
public class BookFactory {

    @Singleton
    Book novel() {
        return new Book("A Great Novel");
    }

    @Singleton
    TextBook textBook() {
        return new TextBook("Learning 101");
    }
}
BookFactory
@Factory
class BookFactory {

    @Singleton
    Book novel() {
        new Book('A Great Novel')
    }

    @Singleton
    TextBook textBook() {
        new TextBook('Learning 101')
    }
}
BookFactory
@Factory
class BookFactory {

    @Singleton
    internal fun novel(): Book {
        return Book("A Great Novel")
    }

    @Singleton
    internal fun textBook(): TextBook {
        return TextBook("Learning 101")
    }
}
To replace a factory entirely, your factory methods must match the return types of all methods in the replaced factory.

In this example, BookFactory#textBook() is not replaced because this factory does not have a factory method that returns a TextBook.

CustomBookFactory
@Factory
@Replaces(factory = BookFactory.class)
public class CustomBookFactory {

    @Singleton
    Book otherNovel() {
        return new Book("An OK Novel");
    }
}
CustomBookFactory
@Factory
@Replaces(factory = BookFactory)
class CustomBookFactory {

    @Singleton
    Book otherNovel() {
        new Book('An OK Novel')
    }
}
CustomBookFactory
@Factory
@Replaces(factory = BookFactory::class)
class CustomBookFactory {

    @Singleton
    internal fun otherNovel(): Book {
        return Book("An OK Novel")
    }
}

To replace one or more factory methods but retain the rest, apply the @Replaces annotation on the method(s) and denote the factory to apply to.

TextBookFactory
@Factory
public class TextBookFactory {

    @Singleton
    @Replaces(value = TextBook.class, factory = BookFactory.class)
    TextBook textBook() {
        return new TextBook("Learning 305");
    }
}
TextBookFactory
@Factory
class TextBookFactory {

    @Singleton
    @Replaces(value = TextBook, factory = BookFactory)
    TextBook textBook() {
        new TextBook('Learning 305')
    }
}
TextBookFactory
@Factory
class TextBookFactory {

    @Singleton
    @Replaces(value = TextBook::class, factory = BookFactory::class)
    internal fun textBook(): TextBook {
        return TextBook("Learning 305")
    }
}

The BookFactory#novel() method will not be replaced because the TextBook class is defined in the annotation.

Default Implementation

When exposing an API, it may be desirable to not expose the default implementation of an interface as part of the public API. Doing so prevents users from being able to replace the implementation because they will not be able to reference the class. The solution is to annotate the interface with DefaultImplementation to indicate which implementation to replace if a user creates a bean that @Replaces(YourInterface.class).

For example consider:

A public API contract

import io.micronaut.context.annotation.DefaultImplementation;

@DefaultImplementation(DefaultResponseStrategy.class)
public interface ResponseStrategy {
}
import io.micronaut.context.annotation.DefaultImplementation

@DefaultImplementation(DefaultResponseStrategy)
interface ResponseStrategy {
}
import io.micronaut.context.annotation.DefaultImplementation

@DefaultImplementation(DefaultResponseStrategy::class)
interface ResponseStrategy

The default implementation

import jakarta.inject.Singleton;

@Singleton
class DefaultResponseStrategy implements ResponseStrategy {

}
import jakarta.inject.Singleton

@Singleton
class DefaultResponseStrategy implements ResponseStrategy {

}
import jakarta.inject.Singleton

@Singleton
internal class DefaultResponseStrategy : ResponseStrategy

The custom implementation

import io.micronaut.context.annotation.Replaces;
import jakarta.inject.Singleton;

@Singleton
@Replaces(ResponseStrategy.class)
public class CustomResponseStrategy implements ResponseStrategy {

}
import io.micronaut.context.annotation.Replaces
import jakarta.inject.Singleton

@Singleton
@Replaces(ResponseStrategy)
class CustomResponseStrategy implements ResponseStrategy {

}
import io.micronaut.context.annotation.Replaces
import jakarta.inject.Singleton

@Singleton
@Replaces(ResponseStrategy::class)
class CustomResponseStrategy : ResponseStrategy

In the above example, the CustomResponseStrategy replaces the DefaultResponseStrategy because the DefaultImplementation annotation points to the DefaultResponseStrategy.

3.11 Bean Configurations

A bean @Configuration is a grouping of multiple bean definitions within a package.

The @Configuration annotation is applied at the package level and informs the Micronaut framework that the beans defined with the package form a logical grouping.

The @Configuration annotation is typically applied to package-info classes. For example:

package-info.groovy
@Configuration
package my.package

import io.micronaut.context.annotation.Configuration

Where this grouping becomes useful is when the bean configuration is made conditional via the @Requires annotation. For example:

package-info.groovy
@Configuration
@Requires(beans = javax.sql.DataSource)
package my.package

In the above example, all bean definitions within the annotated package are only loaded and made available if a javax.sql.DataSource bean is present. This lets you implement conditional autoconfiguration of bean definitions.

Java and Kotlin also support this functionality via package-info.java. Kotlin does not support a package-info.kt as of version 1.3.

3.12 Life-Cycle Methods

When The Bean Is Constructed

To invoke a method when the bean is constructed, use the jakarta.annotation.PostConstruct annotation:

import jakarta.annotation.PostConstruct; // (1)
import jakarta.inject.Singleton;

@Singleton
public class V8Engine implements Engine {

    private int cylinders = 8;
    private boolean initialized = false; // (2)

    @Override
    public String start() {
        if (!initialized) {
            throw new IllegalStateException("Engine not initialized!");
        }

        return "Starting V8";
    }

    @Override
    public int getCylinders() {
        return cylinders;
    }

    public boolean isInitialized() {
        return initialized;
    }

    @PostConstruct // (3)
    public void initialize() {
        initialized = true;
    }
}
import jakarta.annotation.PostConstruct // (1)
import jakarta.inject.Singleton

@Singleton
class V8Engine implements Engine {

    int cylinders = 8
    boolean initialized = false // (2)

    @Override
    String start() {
        if (!initialized) {
            throw new IllegalStateException("Engine not initialized!")
        }

        return "Starting V8"
    }

    @PostConstruct // (3)
    void initialize() {
        initialized = true
    }
}
import jakarta.annotation.PostConstruct
import jakarta.inject.Singleton

@Singleton
class V8Engine : Engine {

    override val cylinders = 8

    var initialized = false
        private set // (2)

    override fun start(): String {
        check(initialized) { "Engine not initialized!" }

        return "Starting V8"
    }

    @PostConstruct // (3)
    fun initialize() {
        initialized = true
    }
}
1 The PostConstruct annotation is imported
2 A field is defined that requires initialization
3 A method is annotated with @PostConstruct and will be invoked once the object is constructed and fully injected.

To manage when a bean is constructed, see the section on bean scopes.

When The Bean Is Destroyed

To invoke a method when the bean is destroyed, use the jakarta.annotation.PreDestroy annotation:

import jakarta.annotation.PreDestroy; // (1)
import jakarta.inject.Singleton;
import java.util.concurrent.atomic.AtomicBoolean;

@Singleton
public class PreDestroyBean implements AutoCloseable {

    AtomicBoolean stopped = new AtomicBoolean(false);

    @PreDestroy // (2)
    @Override
    public void close() throws Exception {
        stopped.compareAndSet(false, true);
    }
}
import jakarta.annotation.PreDestroy // (1)
import jakarta.inject.Singleton
import java.util.concurrent.atomic.AtomicBoolean

@Singleton
class PreDestroyBean implements AutoCloseable {

    AtomicBoolean stopped = new AtomicBoolean(false)

    @PreDestroy // (2)
    @Override
    void close() throws Exception {
        stopped.compareAndSet(false, true)
    }
}
import jakarta.annotation.PreDestroy // (1)
import jakarta.inject.Singleton
import java.util.concurrent.atomic.AtomicBoolean

@Singleton
class PreDestroyBean : AutoCloseable {

    internal var stopped = AtomicBoolean(false)

    @PreDestroy // (2)
    @Throws(Exception::class)
    override fun close() {
        stopped.compareAndSet(false, true)
    }
}
1 The PreDestroy annotation is imported
2 A method is annotated with @PreDestroy and will be invoked when the context is closed.

For factory beans, the preDestroy value in the Bean annotation tells Micronaut framework which method to invoke.

import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;

import jakarta.inject.Singleton;

@Factory
public class ConnectionFactory {

    @Bean(preDestroy = "stop") // (1)
    @Singleton
    public Connection connection() {
        return new Connection();
    }
}
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory

import jakarta.inject.Singleton

@Factory
class ConnectionFactory {

    @Bean(preDestroy = "stop") // (1)
    @Singleton
    Connection connection() {
        new Connection()
    }
}
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory

import jakarta.inject.Singleton

@Factory
class ConnectionFactory {

    @Bean(preDestroy = "stop") // (1)
    @Singleton
    fun connection(): Connection {
        return Connection()
    }
}

import java.util.concurrent.atomic.AtomicBoolean;

public class Connection {

    AtomicBoolean stopped = new AtomicBoolean(false);

    public void stop() { // (2)
        stopped.compareAndSet(false, true);
    }

}
import java.util.concurrent.atomic.AtomicBoolean

class Connection {

    AtomicBoolean stopped = new AtomicBoolean(false)

    void stop() { // (2)
        stopped.compareAndSet(false, true)
    }

}
import java.util.concurrent.atomic.AtomicBoolean

class Connection {

    internal var stopped = AtomicBoolean(false)

    fun stop() { // (2)
        stopped.compareAndSet(false, true)
    }

}
1 The preDestroy value is set on the annotation
2 The annotation value matches the method name
Simply implementing the Closeable or AutoCloseable interface is not enough for a bean to be closed with the context. One of the above methods must be used.

Dependent Beans

Dependent beans are the beans used in the construction of your bean. If the dependent bean’s scope is @Prototype or unknown, it will be destroyed along with your instance.

3.13 Context Events

The Micronaut framework supports a general event system through the context. The ApplicationEventPublisher API publishes events and the ApplicationEventListener API is used to listen to events. The event system is not limited to events that Micronaut publishes and supports custom events created by users. Context Events require Micronaut Context dependency:

implementation("io.micronaut:micronaut-context")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-context</artifactId>
</dependency>

micronaut-context is a transitive dependency of micronaut-http. If you use a Micronaut HTTP runtime, your project already includes the Micronaut-context dependency.

Publishing Events

The ApplicationEventPublisher API supports events of any type, although all events that the Micronaut framework publishes extend ApplicationEvent.

To publish an event, use dependency injection to obtain an instance of ApplicationEventPublisher where the generic type is the type of event and invoke the publishEvent method with your event object.

"Publishing an Event
public class SampleEvent {
    private String message = "Something happened";

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

import io.micronaut.context.event.ApplicationEventPublisher;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

@Singleton
public class SampleEventEmitterBean {

    @Inject
    ApplicationEventPublisher<SampleEvent> eventPublisher;

    public void publishSampleEvent() {
        eventPublisher.publishEvent(new SampleEvent());
    }

}
"Publishing an Event
class SampleEvent {
    String message = "Something happened"
}

import io.micronaut.context.event.ApplicationEventPublisher
import jakarta.inject.Inject
import jakarta.inject.Singleton

@Singleton
class SampleEventEmitterBean {

    @Inject
    ApplicationEventPublisher<SampleEvent> eventPublisher

    void publishSampleEvent() {
        eventPublisher.publishEvent(new SampleEvent())
    }

}
"Publishing an Event
data class SampleEvent(val message: String = "Something happened")

import io.micronaut.context.event.ApplicationEventPublisher
import jakarta.inject.Inject
import jakarta.inject.Singleton

@Singleton
class SampleEventEmitterBean {

    @Inject
    internal var eventPublisher: ApplicationEventPublisher<SampleEvent>? = null

    fun publishSampleEvent() {
        eventPublisher!!.publishEvent(SampleEvent())
    }

}
Publishing an event is synchronous by default! The publishEvent method will not return until all listeners have been executed. Move this work off to a thread pool if it is time-intensive.

Listening for Events

To listen to an event, register a bean that implements ApplicationEventListener where the generic type is the type of event.

Listening for Events with ApplicationEventListener
import io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.docs.context.events.SampleEvent;
import jakarta.inject.Singleton;

@Singleton
public class SampleEventListener implements ApplicationEventListener<SampleEvent> {
    private int invocationCounter = 0;

    @Override
    public void onApplicationEvent(SampleEvent event) {
        invocationCounter++;
    }

    public int getInvocationCounter() {
        return invocationCounter;
    }
}


import io.micronaut.context.ApplicationContext;
import io.micronaut.docs.context.events.SampleEventEmitterBean;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class SampleEventListenerSpec {

    @Test
    void testEventListenerIsNotified() {
        try (ApplicationContext context = ApplicationContext.run()) {
            SampleEventEmitterBean emitter = context.getBean(SampleEventEmitterBean.class);
            SampleEventListener listener = context.getBean(SampleEventListener.class);
            assertEquals(0, listener.getInvocationCounter());
            emitter.publishSampleEvent();
            assertEquals(1, listener.getInvocationCounter());
        }
    }
}
Listening for Events with ApplicationEventListener
import io.micronaut.context.event.ApplicationEventListener
import io.micronaut.docs.context.events.SampleEvent
import jakarta.inject.Singleton

@Singleton
class SampleEventListener implements ApplicationEventListener<SampleEvent> {
    int invocationCounter = 0

    @Override
    void onApplicationEvent(SampleEvent event) {
        invocationCounter++
    }
}

import io.micronaut.context.ApplicationContext
import io.micronaut.docs.context.events.SampleEventEmitterBean
import spock.lang.Specification

class SampleEventListenerSpec extends Specification {

    void "test event listener is notified"() {
        given:
        ApplicationContext context = ApplicationContext.run()
        SampleEventEmitterBean emitter = context.getBean(SampleEventEmitterBean)
        SampleEventListener listener = context.getBean(SampleEventListener)

        expect:
        listener.invocationCounter == 0

        when:
        emitter.publishSampleEvent()

        then:
        listener.invocationCounter == 1

        cleanup:
        context.close()
    }
}
Listening for Events with ApplicationEventListener
import io.micronaut.context.event.ApplicationEventListener
import io.micronaut.docs.context.events.SampleEvent
import jakarta.inject.Singleton

@Singleton
class SampleEventListener : ApplicationEventListener<SampleEvent> {
    var invocationCounter = 0

    override fun onApplicationEvent(event: SampleEvent) {
        invocationCounter++
    }
}

import io.kotest.matchers.shouldBe
import io.kotest.core.spec.style.AnnotationSpec
import io.micronaut.context.ApplicationContext
import io.micronaut.docs.context.events.SampleEventEmitterBean

class SampleEventListenerSpec : AnnotationSpec() {

    @Test
    fun testEventListenerWasNotified() {
        val context = ApplicationContext.run()
        val emitter = context.getBean(SampleEventEmitterBean::class.java)
        val listener = context.getBean(SampleEventListener::class.java)
        listener.invocationCounter.shouldBe(0)
        emitter.publishSampleEvent()
        listener.invocationCounter.shouldBe(1)

        context.close()
    }
}
The supports method can be overridden to further clarify events to be processed.

Alternatively, use the @EventListener annotation if you do not wish to implement an interface or utilize one of the built-in events like StartupEvent and ShutdownEvent:

Listening for Events with @EventListener
import io.micronaut.docs.context.events.SampleEvent;
import io.micronaut.context.event.StartupEvent;
import io.micronaut.context.event.ShutdownEvent;
import io.micronaut.runtime.event.annotation.EventListener;

@Singleton
public class SampleEventListener {
    private int invocationCounter = 0;

    @EventListener
    public void onSampleEvent(SampleEvent event) {
        invocationCounter++;
    }

    @EventListener
    public void onStartupEvent(StartupEvent event) {
        // startup logic here
    }

    @EventListener
    public void onShutdownEvent(ShutdownEvent event) {
        // shutdown logic here
    }

    public int getInvocationCounter() {
        return invocationCounter;
    }
}
Listening for Events with @EventListener
import io.micronaut.docs.context.events.SampleEvent
import io.micronaut.context.event.StartupEvent
import io.micronaut.context.event.ShutdownEvent
import io.micronaut.runtime.event.annotation.EventListener

@Singleton
class SampleEventListener {
    int invocationCounter = 0

    @EventListener
    void onSampleEvent(SampleEvent event) {
        invocationCounter++
    }

    @EventListener
    void onStartupEvent(StartupEvent event) {
        // startup logic here
    }

    @EventListener
    void onShutdownEvent(ShutdownEvent event) {
        // shutdown logic here
    }
}
Listening for Events with @EventListener
import io.micronaut.docs.context.events.SampleEvent
import io.micronaut.context.event.StartupEvent
import io.micronaut.context.event.ShutdownEvent
import io.micronaut.runtime.event.annotation.EventListener

@Singleton
class SampleEventListener {
    var invocationCounter = 0

    @EventListener
    internal fun onSampleEvent(event: SampleEvent) {
        invocationCounter++
    }

    @EventListener
    internal fun onStartupEvent(event: StartupEvent) {
        // startup logic here
    }

    @EventListener
    internal fun onShutdownEvent(event: ShutdownEvent) {
        // shutdown logic here
    }
}

If your listener performs work that might take a while, use the @Async annotation to run the operation on a separate thread:

Asynchronously listening for Events with @EventListener
import io.micronaut.docs.context.events.SampleEvent;
import io.micronaut.runtime.event.annotation.EventListener;
import io.micronaut.scheduling.annotation.Async;

@Singleton
public class SampleEventListener {
    private AtomicInteger invocationCounter = new AtomicInteger(0);

    @EventListener
    @Async
    public void onSampleEvent(SampleEvent event) {
        invocationCounter.getAndIncrement();
    }

    public int getInvocationCounter() {
        return invocationCounter.get();
    }
}


import io.micronaut.context.ApplicationContext;
import io.micronaut.docs.context.events.SampleEventEmitterBean;
import org.junit.jupiter.api.Test;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;

class SampleEventListenerSpec {

    @Test
    void testEventListenerIsNotified() {
        try (ApplicationContext context = ApplicationContext.run()) {
            SampleEventEmitterBean emitter = context.getBean(SampleEventEmitterBean.class);
            SampleEventListener listener = context.getBean(SampleEventListener.class);
            assertEquals(0, listener.getInvocationCounter());
            emitter.publishSampleEvent();
            await().atMost(5, SECONDS).until(listener::getInvocationCounter, equalTo(1));
        }
    }
}
Asynchronously listening for Events with @EventListener
import io.micronaut.docs.context.events.SampleEvent
import io.micronaut.runtime.event.annotation.EventListener
import io.micronaut.scheduling.annotation.Async

@Singleton
class SampleEventListener {
    AtomicInteger invocationCounter = new AtomicInteger(0)

    @EventListener
    @Async
    void onSampleEvent(SampleEvent event) {
        invocationCounter.getAndIncrement()
    }
}

import io.micronaut.context.ApplicationContext
import io.micronaut.docs.context.events.SampleEventEmitterBean
import spock.lang.Specification
import spock.util.concurrent.PollingConditions

class SampleEventListenerSpec extends Specification {

    void "test event listener is notified"() {
        given:
        def context = ApplicationContext.run()
        def emitter = context.getBean(SampleEventEmitterBean)
        def listener = context.getBean(SampleEventListener)

        expect:
        listener.invocationCounter.get() == 0

        when:
        emitter.publishSampleEvent()

        then:
        new PollingConditions(timeout: 5).eventually {
            listener.invocationCounter.get() == 1
        }

        cleanup:
        context.close()
    }
}
Asynchronously listening for Events with @EventListener
import io.micronaut.docs.context.events.SampleEvent
import io.micronaut.runtime.event.annotation.EventListener
import io.micronaut.scheduling.annotation.Async
import java.util.concurrent.atomic.AtomicInteger

@Singleton
open class SampleEventListener {

    var invocationCounter = AtomicInteger(0)

    @EventListener
    @Async
    open fun onSampleEvent(event: SampleEvent) {
        println("Incrementing invocation counter...")
        invocationCounter.getAndIncrement()
    }
}

import io.kotest.assertions.timing.eventually
import io.kotest.matchers.shouldBe
import io.kotest.core.spec.style.AnnotationSpec
import io.micronaut.context.ApplicationContext
import io.micronaut.docs.context.events.SampleEventEmitterBean
import org.opentest4j.AssertionFailedError
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
import kotlin.time.toDuration

@ExperimentalTime
class SampleEventListenerSpec : AnnotationSpec() {

    @Test
    suspend fun testEventListenerWasNotified() {
        val context = ApplicationContext.run()
        val emitter = context.getBean(SampleEventEmitterBean::class.java)
        val listener = context.getBean(SampleEventListener::class.java)
        listener.invocationCounter.get().shouldBe(0)
        emitter.publishSampleEvent()

        eventually(5.toDuration(DurationUnit.SECONDS), AssertionFailedError::class) {
            println("Current value of counter: " + listener.invocationCounter.get())
            listener.invocationCounter.get().shouldBe(1)
        }

        context.close()
    }
}

The event listener by default runs on the scheduled executor. You can configure this thread pool as required in your configuration file (e.g application.yml):

Configuring Scheduled Task Thread Pool
micronaut.executors.scheduled.type=scheduled
micronaut.executors.scheduled.core-pool-size=30
micronaut:
  executors:
    scheduled:
      type: scheduled
      core-pool-size: 30
[micronaut]
  [micronaut.executors]
    [micronaut.executors.scheduled]
      type="scheduled"
      core-pool-size=30
micronaut {
  executors {
    scheduled {
      type = "scheduled"
      corePoolSize = 30
    }
  }
}
{
  micronaut {
    executors {
      scheduled {
        type = "scheduled"
        core-pool-size = 30
      }
    }
  }
}
{
  "micronaut": {
    "executors": {
      "scheduled": {
        "type": "scheduled",
        "core-pool-size": 30
      }
    }
  }
}

3.14 Bean Events

You can hook into the creation of beans using one of the following interfaces:

  • BeanInitializedEventListener - allows modifying or replacing a bean after properties have been set but prior to @PostConstruct event hooks.

  • BeanCreatedEventListener - allows modifying or replacing a bean after the bean is fully initialized and all @PostConstruct hooks called.

The BeanInitializedEventListener interface is commonly used in combination with Factory beans. Consider the following example:

public class V8Engine implements Engine {
    private final int cylinders = 8;
    private double rodLength; // (1)

    public V8Engine(double rodLength) {
        this.rodLength = rodLength;
    }

    @Override
    public String start() {
        return "Starting V" + getCylinders() + " [rodLength=" + getRodLength() + ']';
    }

    @Override
    public final int getCylinders() {
        return cylinders;
    }

    public double getRodLength() {
        return rodLength;
    }

    public void setRodLength(double rodLength) {
        this.rodLength = rodLength;
    }
}

@Factory
public class EngineFactory {

    private V8Engine engine;
    private double rodLength = 5.7;

    @PostConstruct
    public void initialize() {
        engine = new V8Engine(rodLength); // (2)
    }

    @Singleton
    public Engine v8Engine() {
        return engine;// (3)
    }

    public void setRodLength(double rodLength) {
        this.rodLength = rodLength;
    }
}

@Singleton
public class EngineInitializer implements BeanInitializedEventListener<EngineFactory> { // (4)
    @Override
    public EngineFactory onInitialized(BeanInitializingEvent<EngineFactory> event) {
        EngineFactory engineFactory = event.getBean();
        engineFactory.setRodLength(6.6);// (5)
        return engineFactory;
    }
}
class V8Engine implements Engine {
    final int cylinders = 8
    double rodLength // (1)

    @Override
    String start() {
        return "Starting V$cylinders [rodLength=$rodLength]"
    }
}

@Factory
class EngineFactory {
    private V8Engine engine
    double rodLength = 5.7

    @PostConstruct
    void initialize() {
        engine = new V8Engine(rodLength: rodLength) // (2)
    }

    @Singleton
    Engine v8Engine() {
        return engine // (3)
    }
}

@Singleton
class EngineInitializer implements BeanInitializedEventListener<EngineFactory> { // (4)
    @Override
    EngineFactory onInitialized(BeanInitializingEvent<EngineFactory> event) {
        EngineFactory engineFactory = event.bean
        engineFactory.rodLength = 6.6 // (5)
        return engineFactory
    }
}
class V8Engine(var rodLength: Double) : Engine {  // (1)

    override val cylinders = 8

    override fun start(): String {
        return "Starting V$cylinders [rodLength=$rodLength]"
    }
}

@Factory
class EngineFactory {

    private var engine: V8Engine? = null
    private var rodLength = 5.7

    @PostConstruct
    fun initialize() {
        engine = V8Engine(rodLength) // (2)
    }

    @Singleton
    fun v8Engine(): Engine? {
        return engine// (3)
    }

    fun setRodLength(rodLength: Double) {
        this.rodLength = rodLength
    }
}

@Singleton
class EngineInitializer : BeanInitializedEventListener<EngineFactory> { // (4)
    override fun onInitialized(event: BeanInitializingEvent<EngineFactory>): EngineFactory {
        val engineFactory = event.bean
        engineFactory.setRodLength(6.6) // (5)
        return engineFactory
    }
}
1 The V8Engine class defines a rodLength property
2 The EngineFactory initializes the value of rodLength and creates the instance
3 The created instance is returned as a Bean
4 The BeanInitializedEventListener interface is implemented to listen for the initialization of the factory
5 Within the onInitialized method the rodLength is overridden prior to the engine being created by the factory bean.

The BeanCreatedEventListener interface is more typically used to decorate or enhance a fully initialized bean, for example by creating a proxy.

Bean event listeners are initialized before type converters. If your event listener relies on type conversion either by relying on a configuration properties bean or by any other mechanism, you may see errors related to type conversion.

3.15 Bean Introspection

Since Micronaut framework 1.1, a compile-time replacement for the JDK’s Introspector class has been included.

The BeanIntrospector and BeanIntrospection interfaces allow looking up bean introspections to instantiate and read/write bean properties without using reflection or caching reflective metadata, which consume excessive memory for large beans.

3.15.1 Making a Bean Available for Introspection

Unlike the JDK’s Introspector, every class is not automatically available for introspection. To make a class available for introspection you must at a minimum enable Micronaut’s annotation processor (micronaut-inject-java for Java and Kotlin and micronaut-inject-groovy for Groovy) in your build and ensure you have a runtime time dependency on micronaut-core.

annotationProcessor("io.micronaut:micronaut-inject-java")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-inject-java</artifactId>
    </path>
</annotationProcessorPaths>

For Kotlin, add the micronaut-inject-java dependency in kapt scope, and for Groovy add micronaut-inject-groovy in compileOnly scope.

runtimeOnly("io.micronaut:micronaut-core")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-core</artifactId>
    <scope>runtime</scope>
</dependency>

Once your build is configured you have a few ways to generate introspection data.

3.15.2 Use the @Introspected Annotation

The @Introspected annotation can be used on any class to make it available for introspection. Simply annotate the class with @Introspected:

import io.micronaut.core.annotation.Introspected;

@Introspected
public class Person {

    private String name;
    private int age = 18;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
import groovy.transform.Canonical
import io.micronaut.core.annotation.Introspected

@Introspected
@Canonical
class Person {

    String name
    int age = 18

    Person(String name) {
        this.name = name
    }
}
import io.micronaut.core.annotation.Introspected

@Introspected
data class Person(var name : String) {
    var age : Int = 18
}

Once introspection data has been produced at compile time, retrieve it via the BeanIntrospection API:

final BeanIntrospection<Person> introspection = BeanIntrospection.getIntrospection(Person.class); // (1)
Person person = introspection.instantiate("John"); // (2)
System.out.println("Hello " + person.getName());

final BeanProperty<Person, String> property = introspection.getRequiredProperty("name", String.class); // (3)
property.set(person, "Fred"); // (4)
String name = property.get(person); // (5)
System.out.println("Hello " + person.getName());
def introspection = BeanIntrospection.getIntrospection(Person) // (1)
Person person = introspection.instantiate("John") // (2)
println("Hello $person.name")

BeanProperty<Person, String> property = introspection.getRequiredProperty("name", String) // (3)
property.set(person, "Fred") // (4)
String name = property.get(person) // (5)
println("Hello $person.name")
val introspection = BeanIntrospection.getIntrospection(Person::class.java) // (1)
val person : Person = introspection.instantiate("John") // (2)
print("Hello ${person.name}")

val property : BeanProperty<Person, String> = introspection.getRequiredProperty("name", String::class.java) // (3)
property.set(person, "Fred") // (4)
val name = property.get(person) // (5)
print("Hello ${person.name}")
1 You can retrieve a BeanIntrospection with the static getIntrospection method
2 Once you have a BeanIntrospection you can instantiate a bean with the instantiate method.
3 A BeanProperty can be retrieved from the introspection
4 Use the set method to set the property value
5 Use the get method to retrieve the property value

3.15.3 Use @Introspected with @AccessorsStyle

It is possible to use the @AccessorsStyle annotation with @Introspected:

import io.micronaut.core.annotation.AccessorsStyle;
import io.micronaut.core.annotation.Introspected;

@Introspected
@AccessorsStyle(readPrefixes = "", writePrefixes = "") // (1)
public class Person {

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name() { // (2)
        return name;
    }

    public void name(String name) { // (2)
        this.name = name;
    }

    public int age() { // (2)
        return age;
    }

    public void age(int age) { // (2)
        this.age = age;
    }
}
import io.micronaut.core.annotation.AccessorsStyle
import io.micronaut.core.annotation.Introspected

@Introspected
@AccessorsStyle(readPrefixes = "", writePrefixes = "") // (1)
class Person {

    private String name
    private int age

    Person(String name, int age) {
        this.name = name
        this.age = age
    }

    String name() { // (2)
        return name
    }

    void name(String name) { // (2)
        this.name = name
    }

    int age() { // (2)
        return age
    }

    void age(int age) { // (2)
        this.age = age
    }
}
import io.micronaut.core.annotation.AccessorsStyle
import io.micronaut.core.annotation.Introspected

@Introspected
@AccessorsStyle(readPrefixes = [""], writePrefixes = [""]) // (1)
class Person(private var name: String, private var age: Int) {
    fun name(): String { // (2)
        return name
    }

    fun name(name: String) { // (2)
        this.name = name
    }

    fun age(): Int { // (2)
        return age
    }

    fun age(age: Int) { // (2)
        this.age = age
    }
}
1 Annotate the class with @AccessorsStyle to define empty read and write prefixes for getters and setters.
2 Define the getters and setters without a prefix.

Now it is possible to retrieve the compile time generated introspection using the BeanIntrospection API:

        BeanIntrospection<Person> introspection = BeanIntrospection.getIntrospection(Person)
        Person person = introspection.instantiate('John', 42)

        person.name() == 'John'
        person.age() == 42
        val introspection = BeanIntrospection.getIntrospection(Person::class.java)
        val person = introspection.instantiate("John", 42)

        Assertions.assertEquals("John", person.name())
        Assertions.assertEquals(42, person.age())

3.15.4 Bean Fields

By default, Java introspections treat only JavaBean getters/setters or Java 16 record components as bean properties. You can however define classes with public or package protected fields in Java using the accessKind member of the @Introspected annotation:

import io.micronaut.core.annotation.Introspected;

@Introspected(accessKind = Introspected.AccessKind.FIELD)
public class User {
    public final String name; // (1)
    public int age = 18; // (2)

    public User(String name) {
        this.name = name;
    }
}
import io.micronaut.core.annotation.Introspected

@Introspected(accessKind = Introspected.AccessKind.FIELD)
class User {
    public final String name // (1)
    public int age = 18 // (2)

    User(String name) {
        this.name = name
    }
}
@Introspected(accessKind = [Introspected.AccessKind.FIELD])
class User(
    val name: String // (1)
) {
    var age = 18 // (2)
}
1 Final fields are treated like read-only properties
2 Mutable fields are treated like read-write properties
The accessKind accepts an array, so it is possible to allow for both types of accessors but prefer one or the other depending on the order they appear in the annotation. The first one in the list has priority.
Introspections on fields are not possible in Kotlin because it is not possible to declare fields directly.

3.15.5 Constructor Methods

For classes with multiple constructors, apply the @Creator annotation to the constructor to use.

import io.micronaut.core.annotation.Creator;
import io.micronaut.core.annotation.Introspected;

import javax.annotation.concurrent.Immutable;

@Introspected
@Immutable
public class Vehicle {

    private final String make;
    private final String model;
    private final int axles;

    public Vehicle(String make, String model) {
        this(make, model, 2);
    }

    @Creator // (1)
    public Vehicle(String make, String model, int axles) {
        this.make = make;
        this.model = model;
        this.axles = axles;
    }

    public String getMake() {
        return make;
    }

    public String getModel() {
        return model;
    }

    public int getAxles() {
        return axles;
    }
}
import io.micronaut.core.annotation.Creator
import io.micronaut.core.annotation.Introspected

import javax.annotation.concurrent.Immutable

@Introspected
@Immutable
class Vehicle {

    final String make
    final String model
    final int axles

    Vehicle(String make, String model) {
        this(make, model, 2)
    }

    @Creator // (1)
    Vehicle(String make, String model, int axles) {
        this.make = make
        this.model = model
        this.axles = axles
    }
}
import io.micronaut.core.annotation.Creator
import io.micronaut.core.annotation.Introspected

import javax.annotation.concurrent.Immutable

@Introspected
@Immutable
class Vehicle @Creator constructor(val make: String, val model: String, val axles: Int) { // (1)

    constructor(make: String, model: String) : this(make, model, 2) {}
}
1 The @Creator annotation denotes which constructor to use
This class has no default constructor, so calls to instantiate without arguments throw an InstantiationException.

3.15.6 Static Creator Methods

The @Creator annotation can be applied to static methods that create class instances.

import io.micronaut.core.annotation.Creator;
import io.micronaut.core.annotation.Introspected;

import javax.annotation.concurrent.Immutable;

@Introspected
@Immutable
public class Business {

    private final String name;

    private Business(String name) {
        this.name = name;
    }

    @Creator // (1)
    public static Business forName(String name) {
        return new Business(name);
    }

    public String getName() {
        return name;
    }
}
import io.micronaut.core.annotation.Creator
import io.micronaut.core.annotation.Introspected

import javax.annotation.concurrent.Immutable

@Introspected
@Immutable
class Business {

    final String name

    private Business(String name) {
        this.name = name
    }

    @Creator // (1)
    static Business forName(String name) {
        new Business(name)
    }
}
import io.micronaut.core.annotation.Creator
import io.micronaut.core.annotation.Introspected

import javax.annotation.concurrent.Immutable

@Introspected
@Immutable
class Business private constructor(val name: String) {
    companion object {

        @Creator // (1)
        fun forName(name: String): Business {
            return Business(name)
        }
    }

}
1 The @Creator annotation is applied to the static method which instantiates the class
There can be multiple "creator" methods annotated. If there is one without arguments, it will be the default construction method. The first method with arguments will be used as the primary construction method.

3.15.7 Builders

If a type can only be constructed via the builder pattern then you can use the builder member of the @Introspected annotation to generate a dynamic builder. For example given this class:

@Introspected(builder = @Introspected.IntrospectionBuilder(
    builderClass = Person.Builder.class
))
public class Person {
    private final String name;
    private final int age;
    private Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public static Builder builder() {
        return new Builder();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Person person = (Person) o;

        if (age != person.age) return false;
        return Objects.equals(name, person.name);
    }

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

    public static final class Builder {
        private String name;
        private int age;

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Person build() {
            Objects.requireNonNull(name);
            if (age < 1) {
                throw new IllegalArgumentException("Age must be a positive number");
            }
            return new Person(name, age);
        }
    }
}
import io.micronaut.core.annotation.Introspected
@CompileStatic
@Introspected(builder = @Introspected.IntrospectionBuilder(
        builderClass = Person.Builder.class
))
@EqualsAndHashCode
class Person {
    final String name
    final int age

    private Person(String name, int age) {
        this.name = name
        this.age = age
    }

    static Builder builder() {
        new Builder()
    }

    static final class Builder {
        private String name
        private int age

        Builder name(String name) {
            this.name = name
            this
        }

        Builder age(int age) {
            this.age = age
            this
        }

        Person build() {
            Objects.requireNonNull(name)
            if (age < 1) {
                throw new IllegalArgumentException("Age must be a positive number")
            }
            new Person(name, age)
        }
    }
}
@Introspected(builder = Introspected.IntrospectionBuilder(builderClass = Person.Builder::class))
data class Person private constructor(val name: String, val age: Int) {
    data class Builder(
        var name: String? = null,
        var age: Int = 0
    ) {
        fun name(name: String) = apply { this.name = name }
        fun age(age: Int) = apply { this.age = age }

        fun build(): Person {
            requireNotNull(name) { "Name must be specified" }
            require(age >= 1) { "Age must be a positive number" }
            return Person(name!!, age)
        }
    }
}

You can use the builder() method of the BeanIntrospection API to construct the instance:

BeanIntrospection<Person> introspection = BeanIntrospection.getIntrospection(Person.class);
BeanIntrospection.Builder<Person> builder = introspection.builder();
Person person = builder
    .with("age", 25)
    .with("name", "Fred")
    .build();
BeanIntrospection<Person> introspection = BeanIntrospection.getIntrospection(Person.class);
BeanIntrospection.Builder<Person> builder = introspection.builder()
Person person = builder
        .with("age", 25)
        .with("name", "Fred")
        .build()
val introspection = BeanIntrospection.getIntrospection(
    Person::class.java
)
val builder = introspection.builder()
val person = builder
    .with("age", 25)
    .with("name", "Fred")
    .build()
The builder() method also works regardless if the type uses a builder and can be used as a general abstraction for object construction. Note however that there is a slight performance overhead vs direct instantiation via the instantiate() method, hence the hasBuilder() method can be checked if optimized code paths are needed.
Introspection Builder does not work with Groovy @Builder AST.

3.15.8 Introspect Enums

It is possible to introspect enums as well. Add the annotation to the enum, and it can be constructed through the standard valueOf method.

3.15.9 Use the @Introspected Annotation on a Configuration Class

If the class to introspect is already compiled and not under your control, an alternative option is to define a configuration class with the classes member of the @Introspected annotation set.

import io.micronaut.core.annotation.Introspected;

@Introspected(classes = Person.class)
public class PersonConfiguration {
}
import io.micronaut.core.annotation.Introspected

@Introspected(classes = Person)
class PersonConfiguration {
}
import io.micronaut.core.annotation.Introspected

@Introspected(classes = [Person::class])
class PersonConfiguration

In the above example the PersonConfiguration class generates introspections for the Person class.

You can also use the packages member of the @Introspected which package scans at compile time and generates introspections for all classes within a package. Note however this feature is currently regarded as experimental.

3.15.10 Write an AnnotationMapper to Introspect Existing Annotations

If there is an existing annotation that you wish to introspect by default you can write an AnnotationMapper.

An example of this is EntityIntrospectedAnnotationMapper which ensures all beans annotated with javax.persistence.Entity are introspectable by default.

The AnnotationMapper must be on the annotation processor classpath.

3.15.11 The BeanWrapper API

A BeanProperty provides raw access to read and write a property value for a given class and does not provide any automatic type conversion.

It is expected that the values you pass to the set and get methods match the underlying property type, otherwise an exception will occur.

To provide additional type conversion smarts the BeanWrapper interface allows wrapping an existing bean instance and setting and getting properties from the bean, plus performing type conversion as necessary.

final BeanWrapper<Person> wrapper = BeanWrapper.getWrapper(new Person("Fred")); // (1)

wrapper.setProperty("age", "20"); // (2)
int newAge = wrapper.getRequiredProperty("age", int.class); // (3)

System.out.println("Person's age now " + newAge);
final BeanWrapper<Person> wrapper = BeanWrapper.getWrapper(new Person("Fred")) // (1)

wrapper.setProperty("age", "20") // (2)
int newAge = wrapper.getRequiredProperty("age", Integer) // (3)

println("Person's age now $newAge")
val wrapper = BeanWrapper.getWrapper(Person("Fred")) // (1)

wrapper.setProperty("age", "20") // (2)
val newAge = wrapper.getRequiredProperty("age", Int::class.java) // (3)

println("Person's age now $newAge")
1 Use the static getWrapper method to obtain a BeanWrapper for a bean instance.
2 You can set properties, and the BeanWrapper will perform type conversion, or throw ConversionErrorException if conversion is not possible.
3 You can retrieve a property using getRequiredProperty and request the appropriate type. If the property doesn’t exist a IntrospectionException is thrown, and if it cannot be converted a ConversionErrorException is thrown.

3.15.12 Jackson and Bean Introspection

Jackson is configured to use the BeanIntrospection API to read and write property values and construct objects, resulting in reflection-free serialization/deserialization. This is beneficial from a performance perspective and requires less configuration to operate correctly with runtimes such as GraalVM native.

This feature is enabled by default; disable it by setting the jackson.bean-introspection-module configuration to false.

Currently only bean properties (private field with public getter/setter) are supported and usage of public fields is not supported.

3.15.13 Kotlin and Bean Introspection

You can annotate a Kotlin Data Class with @Introspected:

Kotlin Data Class annotated with @Introspected
@Introspected
data class UserDataClass(val name: String)

and instantiate it with the BeanIntrospection API:

Kotlin Data Class instantiated via BeanIntrospection API
val introspection: BeanIntrospection<UserDataClass> = BeanIntrospection.getIntrospection(UserDataClass::class.java)
val user: UserDataClass = introspection.instantiate("John")
Kotlin Inline Value Classes are not supported yet by the BeanIntrospection API.

3.16 Bean Mappers

Since 4.1.x the @Mapper annotation can be used on any abstract method to automatically create a mapping between one type and another.

Inspired by similar functionality in libraries like Map Struct, a Mapper uses the Bean Introspection and Expressions features, built into the Micronaut Framework, which are already reflection free.

For Mapping, base and target types need to be introspected.

@Mapper Example

Given the following types:

@Introspected
public record ContactForm(String firstName, String lastName) {
}
@Introspected
class ContactForm {
    String firstName
    String lastName
}
@Introspected
data class ContactForm(val firstName: String, val lastName: String)
@Introspected
public record ContactEntity(Long id, String firstName, String lastName) {
}
@Introspected
class ContactEntity {
    Long id
    String firstName
    String lastName
}
@Introspected
data class ContactEntity(@Nullable var id: Long? = null, val firstName: String, val lastName: String)

You can write an interface to define a mapping between both types by simply annotating a method with @Mapper.

import io.micronaut.context.annotation.Mapper;

public interface ContactMappers {
    @Mapper
    ContactEntity toEntity(ContactForm contactForm);
}
import io.micronaut.context.annotation.Mapper

interface ContactMappers {
    @Mapper
    ContactEntity toEntity(ContactForm contactForm)
}
import io.micronaut.context.annotation.Mapper

interface ContactMappers {
    @Mapper
    fun toEntity(contactForm: ContactForm) : ContactEntity
}

The Micronaut compiler generates an implementation the previous an interface at compilation-time.

You can then inject a bean of type ContactMappers and easily map from one type to another.

ContactMappers contactMappers = context.getBean(ContactMappers.class);
ContactEntity contactEntity = contactMappers.toEntity(new ContactForm("John", "Snow"));
assertEquals("John", contactEntity.firstName());
assertEquals("Snow", contactEntity.lastName());
ContactMappers contactMappers = context.getBean(ContactMappers)
ContactEntity contactEntity = contactMappers.toEntity(new ContactForm(firstName: "John", lastName: "Snow"))
assertEquals("John", contactEntity.firstName)
assertEquals("Snow", contactEntity.lastName)
val contactMappers = context.getBean(ContactMappers::class.java)
val entity : ContactEntity = contactMappers.toEntity(ContactForm("John", "Snow"))
Assertions.assertEquals("John", entity.firstName)
Assertions.assertEquals("Snow", entity.lastName)

@Mapping Example

Each abstract method can define a single @Mapper annotation or one or many @Mapping annotations to define how properties map onto the target type.

For example, given the following type:

import io.micronaut.core.annotation.Introspected;

@Introspected
public record Product(
    String name,
    double price,
    String manufacturer) {
}
import groovy.transform.Canonical
import io.micronaut.core.annotation.Introspected

@Canonical
@Introspected
class Product {
    String name
    double price
    String manufacturer
}
@Introspected
data class Product(val name: String, val price: Double, val manufacturer: String)

It is common to want to alter this type’s representation in HTTP responses. For example, consider this response type:

import io.micronaut.core.annotation.Introspected;

@Introspected
public record ProductDTO(String name, String price, String distributor) {
}
import io.micronaut.core.annotation.Introspected;

@Introspected
@Canonical
class ProductDTO {
    String name
    String price
    String distributor
}
@Introspected
data class ProductDTO(val name: String, val price: String, val distributor: String)

Here the price property is of a different type and an extra property exists called distributor. You could write manual logic to deal the mapping and these differences, or you could define a mapping:

import io.micronaut.context.annotation.Mapper.Mapping;
import jakarta.inject.Singleton;

@Singleton
public interface ProductMappers {
    @Mapping(
        to = "price",
        from = "#{product.price * 2}",
        format = "$#.00"
    )
    @Mapping(
        to = "distributor",
        from = "#{this.getDistributor()}"
    )
    ProductDTO toProductDTO(Product product);

    default String getDistributor() {
        return "Great Product Company";
    }
}
import io.micronaut.context.annotation.Mapper.Mapping
import jakarta.inject.Singleton

@Singleton
interface ProductMappers {
    @Mapping(
        to = "price",
        from = "#{product.price * 2}",
        format = '$#.00'
    )
    @Mapping(
        to = "distributor",
        from = "#{this.getDistributor()}"
    )
    ProductDTO toProductDTO(Product product);

    default String getDistributor() {
        return "Great Product Company"
    }
}
import io.micronaut.context.annotation.Mapper.Mapping
import jakarta.inject.Singleton

@Singleton
abstract class ProductMappers {
    @Mapping(to = "price", from = "#{product.price * 2}", format = "$#.00")
    @Mapping(to = "distributor", from = "#{this.getDistributor()}")
    abstract fun toProductDTO(product: Product): ProductDTO
    fun getDistributor() : String = "Great Product Company"
}

The from member can be used to define either a property name on the source type or an expression that reads values from the method argument and transforms them in whatever way you choose, including invoking other methods of the instance.

A @Mapping definition is only needed if you need to apply a transformation for the mapping to be successful. Other properties will be automatically mapped and converted.

You can retrieve from the context or inject a bean of type ProductMappers. Then, you can use the toProductDTO method to map from the Product type to the ProductDTO type:

ProductMappers productMappers = context.getBean(ProductMappers.class);

ProductDTO productDTO = productMappers.toProductDTO(new Product(
    "MacBook",
    910.50,
    "Apple"
));

assertEquals("MacBook", productDTO.name());
assertEquals("$1821.00", productDTO.price());
assertEquals("Great Product Company", productDTO.distributor());
given:
ProductMappers productMappers = context.getBean(ProductMappers.class)

when:
ProductDTO productDTO = productMappers.toProductDTO(new Product(
        "MacBook",
        910.50,
        "Apple"
))

then:
productDTO.name == 'MacBook'
productDTO.price == '$1821.00'
productDTO.distributor == "Great Product Company"
val productMappers = context.getBean(ProductMappers::class.java)
val (name, price, distributor) = productMappers.toProductDTO(
    Product(
        "MacBook",
        910.50,
        "Apple"
    )
)
Assertions.assertEquals("MacBook", name)
Assertions.assertEquals("$1821.00", price)
Assertions.assertEquals("Great Product Company", distributor)

3.17 Bean Validation

3.18 Bean Annotation Metadata

The methods provided by Java’s AnnotatedElement API in general don’t provide the ability to introspect annotations without loading the annotations themselves. Nor do they provide any ability to introspect annotation stereotypes (often called meta-annotations; an annotation stereotype is where an annotation is annotated with another annotation, essentially inheriting its behaviour).

To solve this problem many frameworks produce runtime metadata or perform expensive reflection to analyze the annotations of a class.

The Micronaut framework instead produces this annotation metadata at compile time, avoiding expensive reflection and saving memory.

The BeanContext API can be used to obtain a reference to a BeanDefinition which implements the AnnotationMetadata interface.

For example the following code obtains all bean definitions annotated with a particular stereotype:

Lookup Bean Definitions by Stereotype
BeanContext beanContext = ... // obtain the bean context
Collection<BeanDefinition> definitions =
    beanContext.getBeanDefinitions(Qualifiers.byStereotype(Controller.class))

for (BeanDefinition definition : definitions) {
    AnnotationValue<Controller> controllerAnn = definition.getAnnotation(Controller.class);
    // do something with the annotation
}

The above example finds all BeanDefinition instances annotated with @Controller whether @Controller is used directly or inherited via an annotation stereotype.

Note that the getAnnotation method and the variations of the method return an AnnotationValue type and not a Java annotation. This is by design, and you should generally try to work with this API when reading annotation values, since synthesizing a proxy implementation is worse from a performance and memory consumption perspective.

If you require a reference to an annotation instance you can use the synthesize method, which creates a runtime proxy that implements the annotation interface:

Synthesizing Annotation Instances
Controller controllerAnn = definition.synthesize(Controller.class);

This approach is not recommended however, as it requires reflection and increases memory consumption due to the use of runtime generated proxies, and should be used as a last resort, for example if you need an instance of the annotation to integrate with a third-party library.

Annotation Inheritance

The Micronaut framework will respect the rules defined in Java’s AnnotatedElement API with regard to annotation inheritance:

  • Annotations meta-annotated with Inherited will be available via the getAnnotation* methods of the AnnotationMetadata API whilst those directly declared are available via the getDeclaredAnnotation* methods.

  • Annotations not meta-annotated with Inherited will not be included in the metadata

The Micronaut framework differs from the AnnotatedElement API in that it extends these rules to methods and method parameters such that:

In general behaviour which you may wish to override is not inherited by default including Bean Scopes, Bean Qualifiers, Bean Conditions, Validation Rules and so on.

If you wish a particular scope, qualifier, or set of requirements to be inherited when subclassing then you can define a meta-annotation that is annotated with @Inherited. For example:

Defining Inherited Meta Annotations
import io.micronaut.context.annotation.AliasFor;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.AnnotationMetadata;
import jakarta.inject.Named;
import jakarta.inject.Singleton;

import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Inherited // (1)
@Retention(RetentionPolicy.RUNTIME)
@Requires(property = "datasource.url") // (2)
@Named // (3)
@Singleton // (4)
public @interface SqlRepository {
    @AliasFor(annotation = Named.class, member = AnnotationMetadata.VALUE_MEMBER) // (5)
    String value() default "";
}
Defining Inherited Meta Annotations
import io.micronaut.context.annotation.AliasFor
import io.micronaut.context.annotation.Requires
import io.micronaut.core.annotation.AnnotationMetadata
import jakarta.inject.Named
import jakarta.inject.Singleton

import java.lang.annotation.Inherited
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy

@Inherited // (1)
@Retention(RetentionPolicy.RUNTIME)
@Requires(property = "datasource.url") // (2)
@Named // (3)
@Singleton // (4)
@interface SqlRepository {
    @AliasFor(annotation = Named.class, member = AnnotationMetadata.VALUE_MEMBER) // (5)
    String value() default "";
}
Defining Inherited Meta Annotations
import io.micronaut.context.annotation.Requires
import jakarta.inject.Named
import jakarta.inject.Singleton
import java.lang.annotation.Inherited

@Inherited // (1)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Requires(property = "datasource.url") // (2)
@Named // (3)
@Singleton // (4)
annotation class SqlRepository(
    val value: String = ""
)
1 The annotation is declared as @Inherited
2 Bean Conditions will be inherited by child classes
3 Bean Qualifiers will be inherited by child classes
4 Bean Scopes will be inherited by child classes
5 You can also alias annotations and they will be inherited

With this meta-annotation in place you can add the annotation to a super class:

Using Inherited Meta Annotations on a Super Class
@SqlRepository
public abstract class BaseSqlRepository {
}
Using Inherited Meta Annotations on a Super Class
@SqlRepository
abstract class BaseSqlRepository {
}
Using Inherited Meta Annotations on a Super Class
@SqlRepository
abstract class BaseSqlRepository

And then a subclass will inherit all the annotations:

Inherting Annotations in a Child Class
import jakarta.inject.Named;
import javax.sql.DataSource;

@Named("bookRepository")
public class BookRepository extends BaseSqlRepository {
    private final DataSource dataSource;

    public BookRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
Inherting Annotations in a Child Class
import jakarta.inject.Named
import javax.sql.DataSource

@Named("bookRepository")
class BookRepository extends BaseSqlRepository {
    private final DataSource dataSource

    BookRepository(DataSource dataSource) {
        this.dataSource = dataSource
    }
}
Inherting Annotations in a Child Class
import jakarta.inject.Named
import javax.sql.DataSource

@Named("bookRepository")
class BookRepository(private val dataSource: DataSource) : BaseSqlRepository()
A child class must at least have one bean definition annotation such as a scope or qualifier.

Aliasing / Mapping Annotations

There are times when you may want to alias the value of an annotation member to the value of another annotation member. To do this, use the @AliasFor annotation.

A common use case is for example when an annotation defines the value() member, but also supports other members. for example the @Client annotation:

The @Client Annotation
public @interface Client {

    /**
     * @return The URL or service ID of the remote service
     */
    @AliasFor(member = "id") (1)
    String value() default "";

    /**
     * @return The ID of the client
     */
    @AliasFor(member = "value") (2)
    String id() default "";
}
1 The value member also sets the id member
2 The id member also sets the value member

With these aliases in place, whether you define @Client("foo") or @Client(id="foo"), both the value and id members will be set, making it easier to parse and work with the annotation.

If you do not have control over the annotation, another approach is to use an AnnotationMapper. To create an AnnotationMapper, do the following:

  • Implement the AnnotationMapper interface

  • Define a META-INF/services/io.micronaut.inject.annotation.AnnotationMapper file referencing the implementation class

  • Add the JAR file containing the implementation to the annotationProcessor classpath (kapt for Kotlin)

Because AnnotationMapper implementations must be on the annotation processor classpath, they should generally be in a project that includes few external dependencies to avoid polluting the annotation processor classpath.

The following is an example AnnotationMapper that improves the introspection capabilities of JPA entities.

EntityIntrospectedAnnotationMapper Mapper Example
public class EntityIntrospectedAnnotationMapper implements NamedAnnotationMapper {
    @NonNull
    @Override
    public String getName() {
        return "javax.persistence.Entity";
    }

    @Override
    public List<AnnotationValue<?>> map(AnnotationValue<Annotation> annotation, VisitorContext visitorContext) { (1)
        (2)
        return Arrays.asList(
                AnnotationValue.builder(Introspected.class).build(),
                AnnotationValue.builder(ReflectiveAccess.class).build()
        );
    }
}
1 The map method receives a AnnotationValue with the values for the annotation.
2 One or more annotations can be returned.
The example above implements the NamedAnnotationMapper interface which allows for annotations to be mixed with runtime code. To operate against a concrete annotation type, use TypedAnnotationMapper instead, although note it requires the annotation class itself to be on the annotation processor classpath.

3.19 Importing Beans from Libraries

You can use the @Import annotation to import beans from external, already compiled libraries that use JSR-330 annotations.

Bean import is currently only supported in the Java language as other languages have limitations on classpath scanning during source code processing.

For example, to import the JSR-330 TCK into an application, add a dependency on the TCK:

implementation("io.micronaut:jakarta.inject")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>jakarta.inject</artifactId>
</dependency>

Then define the @Import annotation on your Application class:

package example;

import io.micronaut.context.annotation.Import;

@Import( (1)
        packages = { (2)
                "org.atinject.tck.auto",
                "org.atinject.tck.auto.accessories"},
        annotated = "*") (3)
public class Application {
}
1 The @Import is defined
2 The packages to import are defined. Note that the Micronaut framework will not recurse through sub-packages so sub-packages need to be listed explicitly
3 By default, Micronaut framework will only import classes that feature a scope or a qualifier. By using * you can make every type a bean.
In general @Import should be used in applications rather than libraries since if two libraries import the same beans the result will likely be a NonUniqueBeanException

3.20 Nullability Annotations

In Java, you can use annotations showing whether a variable can or cannot be null. Such annotations aren’t part of the standard library, but you can add them separately.

Micronaut framework comes with its own set of annotations to declare nullability; @Nullable and @NonNull.

Why does the Micronaut framework add its own set of nullability annotations instead of using one of the existing nullability annotations libraries?

Throughout the history of the framework, we used other nullability annotation libraries. However, licensing issues made us change nullability annotations several times. To avoid having to change nullability annotations in the future, we added our own set of nullability annotations in Micronaut framework 2.4

Are Micronaut Nullability annotations recognized by Kotlin?

Yes, Micronaut framework’s nullability annotations are mapped at compilation time to jakarta.annotation.Nullable and jakarta.annotation.Nonnull.

Why should you use nullability annotations in your code?

Moreover, you can use @Nullable annotation to mark:

  • A Controller method parameter as optional.

  • An injection point as optional. For example, when using constructor injection you can annotate one a constructor parameter as optional by adding the @Nullable annotation.

3.21 Micronaut Beans And Spring

Micronaut framework has integrations with Spring in several forms. See the Micronaut Spring Documentation for more information.

3.22 Android Support

Since Micronaut dependency injection is based on annotation processors and doesn’t rely on reflection, it can be used on Android when using the Android plugin 3.0.0 or higher.

This lets you use the same application framework for both your Android client and server implementation.

Configuring Your Android Build

To get started, add the Micronaut annotation processors to the processor classpath using the annotationProcessor dependency configuration.

Include the Micronaut micronaut-inject-java dependency in both the annotationProcessor and compileOnly scopes of your Android build configuration:

Example Android build.gradle
dependencies {
    ...
    annotationProcessor "io.micronaut:micronaut-inject-java:4.3.11"
    compileOnly "io.micronaut:micronaut-inject-java:4.3.11"
    ...
}

If you use lint as part of your build you may also need to disable the invalid packages check since Android includes a hard-coded check that regards the jakarta.inject package as invalid unless you use Dagger:

Configure lint within build.gradle
android {
    ...
    lintOptions {
        lintOptions { warning 'InvalidPackage' }
    }
}

You can find more information on configuring annotations processors in the Android documentation.

Micronaut inject-java dependency uses Android Java 8 support features.

Enabling Dependency Injection

Once you have configured the classpath correctly, the next step is start the ApplicationContext.

The following example demonstrates creating a subclass of android.app.Application for that purpose:

Example Android Application Class
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;

import io.micronaut.context.ApplicationContext;
import io.micronaut.context.env.Environment;

public class BaseApplication extends Application { (1)

    private ApplicationContext ctx;

    @Override
    public void onCreate() {
        super.onCreate();
        ctx = ApplicationContext.run(MainActivity.class, Environment.ANDROID); (2)
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { (3)
            @Override
            public void onActivityCreated(Activity activity, Bundle bundle) {
                ctx.inject(activity);
            }
            ... // shortened for brevity; it is not necessary to implement other methods
        });
    }
}
1 Extend the android.app.Application class
2 Run the ApplicationContext with the ANDROID environment
3 Register an ActivityLifecycleCallbacks instance to allow dependency injection of Android Activity instances

4 Application Configuration

Micronaut features a flexible configuration mechanism that allows reading configuration from a variety of sources into a unified model that can be bound to Java types annotated with @ConfigurationProperties.

Configuration can by default be provided in Java properties files or JSON with the ability to add support for more formats (such as YAML or Groovy configuration) by adding addition third-party libraries to your classpath. The convention is to search for a file named application.properties or application.json with support for other formats requiring additional dependencies as described by the following table:

Table 1. Supported Configuration Formats
Format File Dependency Required

YAML

application.yml

org.yaml:snakeyaml

Groovy Config

application.groovy

io.micronaut.groovy:micronaut-runtime-groovy

HOCON

application.conf

io.micronaut.kotlin:micronaut-kotlin-runtime

TOML

application.toml

io.micronaut.toml:micronaut-toml

In addition, Micronaut framework allows overriding any property via system properties or environment variables.

Each source of configuration is modeled with the PropertySource interface and the mechanism is extensible, allowing the implementation of additional PropertySourceLoader implementations.

4.1 The Environment

The application environment is modelled by the Environment interface, which allows specifying one or many unique environment names when creating an ApplicationContext.

Initializing the Environment
ApplicationContext applicationContext = ApplicationContext.run("test", "android");
Environment environment = applicationContext.getEnvironment();

assertTrue(environment.getActiveNames().contains("test"));
assertTrue(environment.getActiveNames().contains("android"));
Initializing the Environment
when:
ApplicationContext applicationContext = ApplicationContext.run("test", "android")
Environment environment = applicationContext.getEnvironment()

then:
environment.activeNames.contains("test")
environment.activeNames.contains("android")
Initializing the Environment
val applicationContext = ApplicationContext.run("test", "android")
val environment = applicationContext.environment

environment.activeNames shouldContain "test"
environment.activeNames shouldContain "android"

The active environment names allow loading different configuration files depending on the environment, and also using the @Requires annotation to conditionally load beans or bean @Configuration packages.

In addition, the Micronaut framework attempts to detect the current environments. For example within a Spock or JUnit test the TEST environment is automatically active.

Additional active environments can be specified using the micronaut.environments system property or the MICRONAUT_ENVIRONMENTS environment variable. These are specified as a comma-separated list. For example:

Specifying environments
$ java -Dmicronaut.environments=foo,bar -jar myapp.jar

The above activates environments called foo and bar.

It is also possible to enable the detection of the Cloud environment the application is deployed to (this feature is disabled by default since Micronaut framework 4). See the section on Cloud Configuration for more information.

Environment Priority

The Micronaut framework loads property sources based on the environments specified, and if the same property key exists in multiple property sources specific to an environment, the environment order determines which value to use.

The Micronaut framework uses the following hierarchy for environment processing (lowest to highest priority):

  • Deduced environments

  • Environments from the micronaut.environments system property

  • Environments from the MICRONAUT_ENVIRONMENTS environment variable

  • Environments specified explicitly through the application context builder

    This also applies to @MicronautTest(environments = …​)

Disabling Environment Detection

Automatic detection of environments can be disabled by setting the micronaut.env.deduction system property or the MICRONAUT_ENV_DEDUCTION environment variable to false. This prevents the Micronaut framework from detecting current environments, while still using any environments that are specifically provided as shown above.

Disabling environment detection via system property
$  java -Dmicronaut.env.deduction=false -jar myapp.jar

Alternatively, you can disable environment deduction using the ApplicationContextBuilder deduceEnvironment method when setting up your application.

Using ApplicationContextBuilder to disable environment deduction
@Test
void testDisableEnvironmentDeductionViaBuilder() {
    ApplicationContext ctx = ApplicationContext.builder()
            .deduceEnvironment(false)
            .properties(Collections.singletonMap("micronaut.server.port", -1))
            .start();
    assertFalse(ctx.getEnvironment().getActiveNames().contains(Environment.TEST));
    ctx.close();
}
Using ApplicationContextBuilder to disable environment deduction
void "test disable environment deduction via builder"() {
    when:
    ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start()

    then:
    !ctx.environment.activeNames.contains(Environment.TEST)

    cleanup:
    ctx.close()
}
Using ApplicationContextBuilder to disable environment deduction
"test disable environment deduction via builder"() {
    val ctx = ApplicationContext.builder().deduceEnvironment(false).start()
    ctx.environment.activeNames.shouldNotContain(Environment.TEST)
    ctx.close()
}

Default Environment

The Micronaut framework supports the concept of one or many default environments. A default environment is one that is only applied if no other environments are explicitly specified or deduced. Environments can be explicitly specified either through the application context builder Micronaut.build().environments(…​), through the micronaut.environments system property, or the MICRONAUT_ENVIRONMENTS environment variable. Environments can be deduced to automatically apply the environment appropriate for cloud deployments. If an environment is found through any of the above means, the default environment will not be applied.

To set the default environments, include a public static class that implements ApplicationContextConfigurer and is annotated with ContextConfigurer:

public class Application {

    @ContextConfigurer
    public static class DefaultEnvironmentConfigurer implements ApplicationContextConfigurer {
        @Override
        public void configure(@NonNull ApplicationContextBuilder builder) {
            builder.defaultEnvironments(defaultEnvironment);
        }
    }

    public static void main(String[] args) {
        Micronaut.run(Application.class, args);
    }
}
Previously, we recommended using Micronaut.defaultEnvironments("dev") however this does not allow the Ahead of Time (AOT) compiler to detect the default environments.

Micronaut Banner

Since Micronaut framework 2.3 a banner is shown when the application starts. It is enabled by default, and it also shows the Micronaut version.

$ ./gradlew run
 __  __ _                                  _
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
  Micronaut (4.3.11)

17:07:22.997 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 611ms. Server Running: http://localhost:8080

To customize the banner with your own ASCII Art (just plain ASCII at this moment), create the file src/main/resources/micronaut-banner.txt and it will be used instead.

To disable it, modify your Application class:

public class Application {

    public static void main(String[] args) {
        Micronaut.build(args)
                 .banner(false) (1)
                 .start();
    }
}
1 Disable the banner

4.2 Externalized Configuration with PropertySources

Additional PropertySource instances can be added to the environment prior to initializing the ApplicationContext.

Initializing the Environment
ApplicationContext applicationContext = ApplicationContext.run(
        PropertySource.of(
                "test",
                CollectionUtils.mapOf(
                    "micronaut.server.host", "foo",
                    "micronaut.server.port", 8080
                )
        ),
        "test", "android");
Environment environment = applicationContext.getEnvironment();

assertEquals("foo", environment.getProperty("micronaut.server.host", String.class).orElse("localhost"));
Initializing the Environment
when:
ApplicationContext applicationContext = ApplicationContext.run(
        PropertySource.of(
                "test",
                [
                    "micronaut.server.host": "foo",
                    "micronaut.server.port": 8080
                ]
        ),
        "test", "android")
Environment environment = applicationContext.getEnvironment()

then:
"foo" == environment.getProperty("micronaut.server.host", String.class).orElse("localhost")
Initializing the Environment
val applicationContext = ApplicationContext.run(
    PropertySource.of(
        "test",
        mapOf(
            "micronaut.server.host" to "foo",
            "micronaut.server.port" to 8080
        )
    ),
    "test", "android"
)
val environment = applicationContext.environment

environment.getProperty("micronaut.server.host", String::class.java).orElse("localhost") shouldBe "foo"

The PropertySource.of method can be used to create a PropertySource from a map of values.

Alternatively one can register a PropertySourceLoader by creating a META-INF/services/io.micronaut.context.env.PropertySourceLoader file containing a reference to the class name of the PropertySourceLoader.

Included PropertySource Loaders

Micronaut framework by default contains PropertySourceLoader implementations that load properties from the given locations and priority:

  1. Command line arguments

  2. Properties from SPRING_APPLICATION_JSON (for Spring compatibility)

  3. Properties from MICRONAUT_APPLICATION_JSON

  4. Java System Properties

  5. OS environment variables

  6. Configuration files loaded in order from the system property 'micronaut.config.files' or the environment variable MICRONAUT_CONFIG_FILES. The value can be a comma-separated list of paths with the last file having precedence. The files can be referenced from the file system as a path, or the classpath with a classpath: prefix.

  7. Environment-specific properties from application-{environment}.{extension}

  8. Application-specific properties from application.{extension}

.properties, .json, .yml are supported out of the box. For Groovy users .groovy is supported as well.

Note that if you want full control of where your application loads configuration from you can disable the default PropertySourceLoader implementations listed above by calling the enableDefaultPropertySources(false) method of the ApplicationContextBuilder interface when starting your application.

In this case only explicit PropertySource instances that you add via the propertySources(..) method of the ApplicationContextBuilder interface will be used.

Supplying Configuration via Command Line

Configuration can be supplied at the command line using Gradle or our Maven plugin. For example:

Gradle
$ ./gradlew run --args="-endpoints.health.enabled=true -config.property=test"
Maven
$ ./mvnw mn:run -Dmn.appArgs="-endpoints.health.enabled=true -config.property=test"

For the configuration to be a part of the context, the args from the main method must be passed to the context builder. For example:

import io.micronaut.runtime.Micronaut;

public class Application {

    public static void main(String[] args) {
        Micronaut.run(Application.class, args); // passing args
    }
}

Secrets and Sensitive Configuration

It is important to note that it is not recommended to store sensitive configuration such as passwords and tokens within configuration files that can potentially be checked into source control systems.

It is good practise to instead externalize sensitive configuration completely from the application code using preferably an external secret manager system (there are many options here, many provided by Cloud providers) or environment variables that are set during the deployment of the application. You can also use property placeholders (see the following section), to customize names of the environment variables to use and supply default values:

Using Property Value Placeholders to Define Secure Configuration
datasources.default.url=${JDBC_URL:`jdbc:mysql://localhost:3306/db`}
datasources.default.username=${JDBC_USER:root}
datasources.default.password=${JDBC_PASSWORD:}
datasources.default.dialect=MYSQL
datasources.default.driverClassName=${JDBC_DRIVER:com.mysql.cj.jdbc.Driver}
datasources:
  default:
    url: ${JDBC_URL:`jdbc:mysql://localhost:3306/db`}
    username: ${JDBC_USER:root}
    password: ${JDBC_PASSWORD:}
    dialect: MYSQL
    driverClassName: ${JDBC_DRIVER:com.mysql.cj.jdbc.Driver}
[datasources]
  [datasources.default]
    url="${JDBC_URL:`jdbc:mysql://localhost:3306/db`}"
    username="${JDBC_USER:root}"
    password="${JDBC_PASSWORD:}"
    dialect="MYSQL"
    driverClassName="${JDBC_DRIVER:com.mysql.cj.jdbc.Driver}"
datasources {
  'default' {
    url = "${JDBC_URL:`jdbc:mysql://localhost:3306/db`}"
    username = "${JDBC_USER:root}"
    password = "${JDBC_PASSWORD:}"
    dialect = "MYSQL"
    driverClassName = "${JDBC_DRIVER:com.mysql.cj.jdbc.Driver}"
  }
}
{
  datasources {
    default {
      url = "${JDBC_URL:`jdbc:mysql://localhost:3306/db`}"
      username = "${JDBC_USER:root}"
      password = "${JDBC_PASSWORD:}"
      dialect = "MYSQL"
      driverClassName = "${JDBC_DRIVER:com.mysql.cj.jdbc.Driver}"
    }
  }
}
{
  "datasources": {
    "default": {
      "url": "${JDBC_URL:`jdbc:mysql://localhost:3306/db`}",
      "username": "${JDBC_USER:root}",
      "password": "${JDBC_PASSWORD:}",
      "dialect": "MYSQL",
      "driverClassName": "${JDBC_DRIVER:com.mysql.cj.jdbc.Driver}"
    }
  }
}

To securely externalize configuration consider using a secrets manager system supported by the Micronaut framework such as:

Property Value Placeholders

As mentioned in the previous section, the Micronaut framework includes a property placeholder syntax to reference configuration properties both within configuration values and with any Micronaut annotation. See @Value and the section on Configuration Injection.

Programmatic usage is also possible via the PropertyPlaceholderResolver interface.

The basic syntax is to wrap a reference to a property in ${…​}. For example:

Defining Property Placeholders
myapp.endpoint=http://${micronaut.server.host}:${micronaut.server.port}/foo
myapp:
  endpoint: http://${micronaut.server.host}:${micronaut.server.port}/foo
[myapp]
  endpoint="http://${micronaut.server.host}:${micronaut.server.port}/foo"
myapp {
  endpoint = "http://${micronaut.server.host}:${micronaut.server.port}/foo"
}
{
  myapp {
    endpoint = "http://${micronaut.server.host}:${micronaut.server.port}/foo"
  }
}
{
  "myapp": {
    "endpoint": "http://${micronaut.server.host}:${micronaut.server.port}/foo"
  }
}

The above example embeds references to the micronaut.server.host and micronaut.server.port properties.

You can specify default values by defining a value after the : character. For example:

Using Default Values
myapp.endpoint=http://${micronaut.server.host:localhost}:${micronaut.server.port:8080}/foo
myapp:
  endpoint: http://${micronaut.server.host:localhost}:${micronaut.server.port:8080}/foo
[myapp]
  endpoint="http://${micronaut.server.host:localhost}:${micronaut.server.port:8080}/foo"
myapp {
  endpoint = "http://${micronaut.server.host:localhost}:${micronaut.server.port:8080}/foo"
}
{
  myapp {
    endpoint = "http://${micronaut.server.host:localhost}:${micronaut.server.port:8080}/foo"
  }
}
{
  "myapp": {
    "endpoint": "http://${micronaut.server.host:localhost}:${micronaut.server.port:8080}/foo"
  }
}

The above example defaults to localhost and port 8080 if no value is found (rather than throwing an exception). Note that if the default value contains a : character, you must escape it using backticks:

Using Backticks
myapp.endpoint=${server.address:`http://localhost:8080`}/foo
myapp:
  endpoint: ${server.address:`http://localhost:8080`}/foo
[myapp]
  endpoint="${server.address:`http://localhost:8080`}/foo"
myapp {
  endpoint = "${server.address:`http://localhost:8080`}/foo"
}
{
  myapp {
    endpoint = "${server.address:`http://localhost:8080`}/foo"
  }
}
{
  "myapp": {
    "endpoint": "${server.address:`http://localhost:8080`}/foo"
  }
}

The above example looks for a server.address property and defaults to http://localhost:8080. This default value is escaped with backticks since it has a : character.

Property Value Binding

Note that these property references should be in kebab case (lowercase and hyphen-separated) when placing references in code or in placeholder values. For example, use micronaut.server.default-charset and not micronaut.server.defaultCharset.

The Micronaut framework still allows specifying the latter in configuration, but normalizes the properties into kebab case form to optimize memory consumption and reduce complexity when resolving properties. The following table summarizes how properties are normalized from different sources:

Table 1. Property Value Normalization
Configuration Value Resulting Properties Property Source

myApp.myStuff

my-app.my-stuff

Properties, YAML etc.

my-app.myStuff

my-app.my-stuff

Properties, YAML etc.

myApp.my-stuff

my-app.my-stuff

Properties, YAML etc.

MYAPP_MYSTUFF

myapp.mystuff, myapp-mystuff

Environment Variable

MY_APP_MY_STUFF

my.app.my.stuff, my.app.my-stuff, my.app-my.stuff, my.app-my-stuff, my-app.my.stuff, my-app.my-stuff, my-app-my.stuff, my-app-my-stuff

Environment Variable

Environment variables are treated specially to allow more flexibility. Note that there is no way to reference an environment variable with camel-case.

Because the number of properties generated is exponential based on the number of _ characters in an environment variable, it is recommended to refine which, if any, environment variables are included in configuration if the number of environment variables with multiple underscores is high.

To control how environment properties participate in configuration, call the respective methods on the Micronaut builder.

Application class
import io.micronaut.runtime.Micronaut;

public class Application {

    public static void main(String[] args) {
        Micronaut.build(args)
                .mainClass(Application.class)
                .environmentPropertySource(false)
                //or
                .environmentVariableIncludes("THIS_ENV_ONLY")
                //or
                .environmentVariableExcludes("EXCLUDED_ENV")
                .start();
    }
}
Application class
import io.micronaut.runtime.Micronaut

class Application {

    static void main(String[] args) {
        Micronaut.build()
                .mainClass(Application)
                .environmentPropertySource(false)
                //or
                .environmentVariableIncludes("THIS_ENV_ONLY")
                //or
                .environmentVariableExcludes("EXCLUDED_ENV")
                .start()
    }
}
Application class
import io.micronaut.runtime.Micronaut

object Application {

    @JvmStatic
    fun main(args: Array<String>) {
        Micronaut.build(null)
                .mainClass(Application::class.java)
                .environmentPropertySource(false)
                //or
                .environmentVariableIncludes("THIS_ENV_ONLY")
                //or
                .environmentVariableExcludes("EXCLUDED_ENV")
                .start()
    }
}
The configuration above does not have any impact on property placeholders. It is still possible to reference an environment variable in a placeholder regardless of whether environment configuration is disabled, or even if the specific property is explicitly excluded.

Using Random Properties

You can use random values by using the following properties. These can be used in configuration files as variables like the following.

micronaut.application.name=myapplication
micronaut.application.instance.id=${random.shortuuid}
micronaut:
  application:
    name: myapplication
    instance:
      id: ${random.shortuuid}
[micronaut]
  [micronaut.application]
    name="myapplication"
    [micronaut.application.instance]
      id="${random.shortuuid}"
micronaut {
  application {
    name = "myapplication"
    instance {
      id = "${random.shortuuid}"
    }
  }
}
{
  micronaut {
    application {
      name = "myapplication"
      instance {
        id = "${random.shortuuid}"
      }
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "myapplication",
      "instance": {
        "id": "${random.shortuuid}"
      }
    }
  }
}
Table 2. Random Values
Property Value

random.port

An available random port number

random.int

Random int

random.integer

Random int

random.long

Random long

random.float

Random float

random.shortuuid

Random UUID of only 10 chars in length (Note: As this isn’t full UUID, collision COULD occur)

random.uuid

Random UUID with dashes

random.uuid2

Random UUID without dashes

The random.int, random.integer, random.long and random.float properties supports a range suffix whose syntax is one of as follows:

  • (max) where max is an exclusive value

  • [min,max] where min being inclusive and max being exclusive values.

instance.id=${random.int[5,10]}
instance.count=${random.int(5)}
instance:
  id: ${random.int[5,10]}
  count: ${random.int(5)}
[instance]
  id="${random.int[5,10]}"
  count="${random.int(5)}"
instance {
  id = "${random.int[5,10]}"
  count = "${random.int(5)}"
}
{
  instance {
    id = "${random.int[5,10]}"
    count = "${random.int(5)}"
  }
}
{
  "instance": {
    "id": "${random.int[5,10]}",
    "count": "${random.int(5)}"
  }
}
The range could vary from negative to positive as well.

Fail Fast Property Injection

For beans that inject required properties, the injection and potential failure will not occur until the bean is requested. To verify at startup that the properties exist and can be injected, the bean can be annotated with @Context. Context-scoped beans are injected at startup, and startup fails if any required properties are missing or cannot be converted to the required type.

It is recommended to use this feature sparingly to ensure fast startup.

4.3 Configuration Injection

You can inject configuration values into beans using the @Value annotation.

Using the @Value Annotation

Consider the following example:

@Value Example
import io.micronaut.context.annotation.Value;

import jakarta.inject.Singleton;

@Singleton
public class EngineImpl implements Engine {

    @Value("${my.engine.cylinders:6}") // (1)
    protected int cylinders;

    @Override
    public int getCylinders() {
        return cylinders;
    }

    @Override
    public String start() {// (2)
        return "Starting V" + getCylinders() + " Engine";
    }

}
@Value Example
import io.micronaut.context.annotation.Value

import jakarta.inject.Singleton

@Singleton
class EngineImpl implements Engine {

    @Value('${my.engine.cylinders:6}') // (1)
    protected int cylinders

    @Override
    int getCylinders() {
        cylinders
    }

    @Override
    String start() { // (2)
        "Starting V$cylinders Engine"
    }
}
@Value Example
import io.micronaut.context.annotation.Value

import jakarta.inject.Singleton

@Singleton
class EngineImpl : Engine {

    @Value("\${my.engine.cylinders:6}") // (1)
    override var cylinders: Int = 0
        protected set

    override fun start(): String { // (2)
        return "Starting V$cylinders Engine"
    }
}
1 The @Value annotation accepts a string that can have embedded placeholder values (the default value can be provided by specifying a value after the colon : character). Also try to avoid setting the member visibility to private, since this requires the Micronaut Framework to use reflection. Prefer to use protected.
2 The injected value can then be used within code.

Note that @Value can also be used to inject a static value. For example the following injects the number 10:

Static @Value Example
@Value("10")
int number;

This is even more useful when used to compose injected values combining static content and placeholders. For example to set up a URL:

Placeholders with @Value
@Value("http://${my.host}:${my.port}")
URL url;

In the above example the URL is constructed from two placeholder properties that must be present in configuration: my.host and my.port.

Remember that to specify a default value in a placeholder expression, you use the colon : character. However, if the default you specify includes a colon, you must escape the value with backticks. For example:

Placeholders with @Value
@Value("${my.url:`http://foo.com`}")
URL url;

Note that there is nothing special about @Value itself regarding the resolution of property value placeholders.

Due to Micronaut’s extensive support for annotation metadata you can use property placeholder expressions on any annotation. For example, to make the path of a @Controller configurable you can do:

@Controller("${hello.controller.path:/hello}")
class HelloController {
    ...
}

In the above case, if hello.controller.path is specified in configuration the controller will be mapped to the specified path, otherwise it will be mapped to /hello.

You can also make the target server for @Client configurable (although service discovery approaches are often better), for example:

@Client("${my.server.url:`http://localhost:8080`}")
interface HelloClient {
    ...
}

In the above example the property my.server.url can be used to configure the client, otherwise the client falls back to a localhost address.

Using the @Property Annotation

Recall that the @Value annotation receives a String value which can be a mix of static content and placeholder expressions. This can lead to confusion if you attempt to do the following:

Incorrect usage of @Value
@Value("my.url")
String url;

In the above case the literal string value my.url is injected and set to the url field and not the value of the my.url property from your application configuration. This is because @Value only resolves placeholders within the value specified to it.

To inject a specific property name, you may be better off using @Property:

Using @Property
import io.micronaut.context.annotation.Property;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

@Singleton
public class Engine {

    @Property(name = "my.engine.cylinders") // (1)
    protected int cylinders; // (2)

    private String manufacturer;

    public int getCylinders() {
        return cylinders;
    }

    public String getManufacturer() {
        return manufacturer;
    }

    @Inject
    public void setManufacturer(@Property(name = "my.engine.manufacturer") String manufacturer) { // (3)
        this.manufacturer = manufacturer;
    }

}
Using @Property
import io.micronaut.context.annotation.Property

import jakarta.inject.Singleton

@Singleton
class Engine {

    @Property(name = "my.engine.cylinders") // (1)
    protected int cylinders // (2)

    @Property(name = "my.engine.manufacturer") //(3)
    String manufacturer

    int getCylinders() {
        cylinders
    }
}
Using @Property
import io.micronaut.context.annotation.Property

import jakarta.inject.Inject
import jakarta.inject.Singleton


@Singleton
class Engine {

    @field:Property(name = "my.engine.cylinders") // (1)
    protected var cylinders: Int = 0 // (2)

    @set:Inject
    @setparam:Property(name = "my.engine.manufacturer") // (3)
    var manufacturer: String? = null

    fun cylinders(): Int {
        return cylinders
    }
}
1 The my.engine.cylinders property is resolved from configuration and injected into the field.
2 Fields subject to injection should not be private because expensive reflection must be used
3 The @Property annotation is used to inject through the setter
Because it is not possible to define a default value with @Property, if the value doesn’t exist or cannot be converted to the required type, bean instantiation will fail.

The above instead injects the value of the my.engine.cylinders property resolved from application configuration. If the property cannot be found in configuration, an exception is thrown. As with other types of injection, the injection point can also be annotated with @Nullable to make the injection optional.

You can also use this feature to resolve sub maps. For example, consider the following configuration:

datasources.default.name=mydb
jpa.default.properties.hibernate.hbm2ddl.auto=update
jpa.default.properties.hibernate.show_sql=true
datasources:
  default:
    name: 'mydb'
jpa:
  default:
    properties:
      hibernate:
        hbm2ddl:
          auto: update
        show_sql: true
[datasources]
  [datasources.default]
    name="mydb"
[jpa]
  [jpa.default]
    [jpa.default.properties]
      [jpa.default.properties.hibernate]
        show_sql=true
        [jpa.default.properties.hibernate.hbm2ddl]
          auto="update"
datasources {
  'default' {
    name = "mydb"
  }
}
jpa {
  'default' {
    properties {
      hibernate {
        hbm2ddl {
          auto = "update"
        }
        show_sql = true
      }
    }
  }
}
{
  datasources {
    default {
      name = "mydb"
    }
  }
  jpa {
    default {
      properties {
        hibernate {
          hbm2ddl {
            auto = "update"
          }
          show_sql = true
        }
      }
    }
  }
}
{
  "datasources": {
    "default": {
      "name": "mydb"
    }
  },
  "jpa": {
    "default": {
      "properties": {
        "hibernate": {
          "hbm2ddl": {
            "auto": "update"
          },
          "show_sql": true
        }
      }
    }
  }
}

To resolve a flattened map containing only the properties starting with hibernate, use @Property, for example:

Using @Property
@Property(name = "jpa.default.properties")
Map<String, String> jpaProperties;

The injected map will contain the keys hibernate.hbm2ddl.auto and hibernate.show_sql and their values.

The @MapFormat annotation can be used to customize the injected map depending on whether you want nested keys or flat keys, and it allows customization of the key style via the StringConvention enum.

4.4 Expression Language

Since 4.0, Micronaut framework supports embedding evaluated expressions in annotation values using #{…​} syntax which allows to achieve even more flexibility while configuring your application.

Evaluated Expression example
@Value("#{ T(Math).random() }")
double injectedValue;

Expressions can be defined whenever an annotation member accepts a string or an array of strings.

Expressions are currently not supported for "type use" annotations (that declare ElementType.TYPE_USE).
Evaluated Expression in array
@Singleton
@Requires(env = {"dev", "#{ 'test' }"})
public class EvaluatedExpressionInArray {}

You can also embed one or more expressions in a string template in a similar manner to embedding properties with the ${…​} syntax.

Evaluated Expression template
@Value("http://#{'hostname'}/#{'path'}")
String url;

Evaluated Expressions are validated and compiled at build time which guarantees type safety at runtime.

Once an application is running expressions are evaluated on demand as part of annotation metadata resolution. The usage of expressions does not impact performance as evaluation process is completely reflection free.

Note that, for security reasons expressions cannot be dynamically compiled at runtime from potentially untrusted input. All expressions are compiled and checked statically during the compilation process of the application with errors reported as compilation failures.

In general, expressions can be treated as statement written using a programming language with reduced set of available features. Even though the complexity of expression is only limited by the list of supported syntax constructs, it is in general not recommended to place complex logic inside an expression as there are usually better ways to achieve the same result.

Using Expressions in Micronaut framework

Expressions can be used anywhere throughout the Micronaut framework and associated modules, but as an example, you can use them to implement simple scheduled job control, for example:

Job Control with Expressions
import io.micronaut.scheduling.annotation.Scheduled;
import jakarta.inject.Singleton;

@Singleton
public class ExampleJob {
    private boolean jobRan = false;
    private boolean paused = true;


    @Scheduled(
        fixedRate = "1s",
        condition = "#{!this.paused}") // (1)
    void run() {
        System.out.println("Job Running");
        this.jobRan = true;
    }

    public boolean isPaused() {
        return paused;
    } // (2)

    public boolean hasJobRun() {
        return jobRan;
    }

    public void unpause() {
        paused = false;
    }

    public void pause() {
        paused = true;
    }

}
Job Control with Expressions
import io.micronaut.scheduling.annotation.Scheduled
import jakarta.inject.Singleton

@Singleton
class ExampleJob {
    boolean paused = true // (2)
    private boolean jobRan = false

    @Scheduled(
            fixedRate = "1s",
            condition = '#{!this.paused}') // (1)
    void run() {
        println("Job Running")
        this.jobRan = true
    }

    boolean hasJobRun() {
        return jobRan
    }

    void unpause() {
        paused = false
    }

    void pause() {
        paused = true
    }
}
Job Control with Expressions
import io.micronaut.scheduling.annotation.Scheduled
import jakarta.inject.Singleton

@Singleton
class ExampleJob {
    var paused = true
    private var jobRan = false
    @Scheduled(
        fixedRate = "1s",
        condition = "#{!this.paused}") // (1)
    fun run() {
        println("Job Running")
        jobRan = true
    }

    fun hasJobRun(): Boolean {
        return jobRan
    }

    fun unpause() {
        paused = false
    }

    fun pause() {
        paused = true
    }
}
1 Here the condition member of the @Scheduled annotation is used to only execute the job if a pre-condition is met.
2 The condition invokes a method of the type that checks if the job is paused. Other methods can be used to pause and resume execution of the job as desired.
You can also use expressions to perform conditional routing using the @RouteCondition annotation.

Evaluated Expression Language Reference

The Evaluated Expressions syntax supports the following functionality:

  • Literal Values

  • Math Operators

  • Comparison Operators

  • Logical Operators

  • Ternary Operator

  • Type References

  • Method Invocation

  • Property Access

  • Retrieving Beans from Bean Context

  • Retrieving Environment Properties

Literal Values

The following types of literal values are supported:

  • null

  • boolean values (true, false)

  • strings, which need to be surrounded with single quotation mark (')

  • numeric values (int, long, float, double)

Integer and Long values can also be specified in hexadecimal or octal notation. Float and Double values can also be specified in exponential notation. All numeric values can be negative as well.

Literal values examples
#{ null }
#{ true }
#{ 'string value' }
#{ 10 }
#{ 0xFFL }
#{ 10L }
#{ .123f }
#{ 1E+1d }
#{ 123D }

Math Operators

The supported mathematical operators are `, `-`, `*`, `/`, `%`, `^`. Math operators can only be applied to numeric values (except ` which can be used for string concatenation as well). Mathematical operations are performed in order enforced by standard operator precedence. You can also change evaluation order by using brackets ().

/ and % operators can be aliased by div and mod keywords respectively.

Math operators examples
#{ 1 + 2 }             // 3
#{ 'a' + 'b' + 'c' }   // 'abc'
#{ 7 - 3 }             // 4
#{ 7 * 3 }             // 21
#{ 7 * ( 3 + 1) }      // 28

#{ 15 / 3 }            // 5
#{ 15 div 3 }          // 5

#{ 15 % 3 }            // 0
#{ 15 mod 3 }          // 0

// Unlike in Java, ^ operator means exponentiation
#{ 3 ^ 2 }             // 9

Comparison Operators

The following comparison operators are supported: ==, !=, >, <, >=, <=, matches Comparison operations are performed in order enforced by standard operator precedence. You can also change evaluation order by using brackets ().

Equality check is supported for both primitive types and objects. It is performed using Object.equals() method.

>, <, >=, <= operations can be applied to numeric types or types that implement java.lang.Comparable interface.

matches keyword can be used to determine whether a string matches provided regular expression which has to be specified as string literal. The regular expression itself will be checked for validity at compilation time.

Comparison operators examples
#{ 1 + 2 == 3 }         // true
#{ 'abc' != 'abc' }     // false
#{ 7 > 3 }              // true
#{ 7 < 3 }              // false
#{ 7 >= 7 }             // true
#{ 7 <= 8 }             // false

#{ 'AbC' matches '[A-Za-z*'  }      // Compilation failure
#{ 'AbC' matches '[A-Za-z]*'  }     // true
#{ 'AbC' matches '[a-z]*'  }        // false

Logical Operators

The following logical operators are supported:

  • && (can be aliased with and)

  • || (can be aliased with or),

  • ! (can be aliaded with not)

  • empty / not empty (works with strings, collections, arrays, and maps)

Logical operations are performed in order enforced by standard operator precedence. You can also change evaluation order by using brackets ().

Logical operators examples
#{ true && false }         // false
#{ true and true }         // true

#{ true || false }         // true
#{ false or false }        // false

#{ !false }                // true
#{ !!true }                // true

#{ empty '' }              // true
#{ not empty '' }          // false

Ternary Operator

A standard ternary operator is supported to allow specifying if-then-else conditional logic in expression

condition ? thenBranch : elseBranch

where condition evaluation should provide boolean value, and the complexity of then and else branches is not limited.

Ternary operator examples
#{ 15 > 10 ? 'a' : 'b' }    // 'a'
#{ 15 >= 16 ? 'a' : 'b' }   // 'b'

Dot and Safe Navigation Operator

The dot operator can be used to access methods and properties of a value within an expression. For example:

Dot operator usage
#{ collection.size() > 0 }
#{ foo.bar.name == "Fred" }

You can also use the safe dereference operator ?. to navigate paths in a null safe way:

Safe dereference operator
#{ foo?.bar?.name == "Fred" }
When used, the safe dereference operator will also automatically unwrap Java’s Optional type.

Type References

A predefined syntax construct T(…​) can be used to reference a class. The value inside brackets should be fully qualified class name (including the package name). The only exception is java.lang.* classes which can be referenced directly by only specifying the simple class name. Primitive types can not be referenced.

Type References are evaluated in different ways depending on the context.

Simple type reference

A simple type reference is resolved as a Class<?> object.

Type reference example
#{ T(java.lang.String) }    // String.class

Same rule applies if type reference is specified as a method argument.

Type check with instanceof

A Type Reference can be used as the right-hand side part of the instanceof operator

Type check example
#{ 'abc' instanceof T(String) }  // true

which is equivalent to the following Java code and will be evaluated as a boolean value:

"abc" instanceof String

Static method invocation

Type Reference can be used to invoke a static method of a class

Static method invocation
#{ T(Math).random() }

Expression Evaluation Context

By default, the only methods you can invoke inside Evaluated Expressions are static methods using type references.

The available methods can be extended by extended the evaluation context. There are two ways to extend the evaluation context. The first involves registering new context class via a custom TypeElementVisitor.

The TypeElementVisitor has to be on the annotation processor classpath, therefore needs to be defined in a separate module that can be included on this classpath.

Once a class is registered within evaluation context the methods and properties of the class are available for referencing in evaluated expressions.

Consider the following example:

User-defined evaluated expression context
package io.micronaut.docs.expressions;

import jakarta.inject.Singleton;
import java.util.Random;

@Singleton
public class CustomEvaluationContext {
    public int generateRandom(int min, int max) {
        return new Random().nextInt(max - min) + min;
    }
}
The class should be resolvable as a bean can use jakarta.inject annotations to inject other types if necessary. In addition, for performance reasons all evaluation context classes are effectively singleton regardless of the defined scope.

Registering this class can be achieved with a custom implementation of ExpressionEvaluationContextRegistrar that is registered via service loader as a TypeElementVisitor (create a new META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor file referencing the new class) and placed on the annotation processor classpath:

Defining a ExpressionEvaluationContextRegistrar
package io.micronaut.docs.expressions;

import io.micronaut.expressions.context.ExpressionEvaluationContextRegistrar;

public class ContextRegistrar implements ExpressionEvaluationContextRegistrar {
    @Override
    public String getContextClassName() {
        return "io.micronaut.docs.expressions.CustomEvaluationContext";
    }
}

Method generateRandom(int, int) can now be used within Evaluated Expression in the following way:

Usage of user-defined evaluated expression context
package io.micronaut.docs.expressions;

import io.micronaut.context.annotation.Value;
import jakarta.inject.Singleton;

@Singleton
public class ContextConsumer {

    @Value("#{ generateRandom(1, 10) }")
    public int randomField;

}

At runtime, the bean will be retrieved from application context and respective method will be invoked.

If a matching method is not found within evaluation context at compilation time, the compilation will fail. A compilation error will also occur if multiple suitable methods are found in the evaluation context, keep that in mind if you provide multiple ExpressionEvaluationContextRegistrar that a conflict can occur as these types are effectively global.

The methods will be considered ambiguous (leading to compilation failure) when their names are the same and list of provided arguments matches multiple methods parameters.

Using a ExpressionEvaluationContextRegistrar makes its methods and properties available for evaluated expressions within any annotation in a global manner.

However, you can also specify evaluation context scoped to concrete annotation or annotation member using @AnnotationExpressionContext.

Usage of annotation level evaluated expression context
package io.micronaut.docs.expressions;

import jakarta.inject.Singleton;
import io.micronaut.context.annotation.AnnotationExpressionContext;

@Singleton
@CustomAnnotation(value = "#{firstValue() + secondValue()}") // (1)
class Example {
}

@Singleton
class AnnotationContext { // (2)
    String firstValue() {
        return "first value";
    }
}

@Singleton
class AnnotationMemberContext { // (3)
    String secondValue() {
        return "second value";
    }
}

@AnnotationExpressionContext(AnnotationContext.class) // (4)
@interface CustomAnnotation {

    @AnnotationExpressionContext(AnnotationMemberContext.class) // (5)
    String value();
}
Usage of annotation level evaluated expression context
package io.micronaut.docs.expressions;

import jakarta.inject.Singleton;
import io.micronaut.context.annotation.AnnotationExpressionContext;

@Singleton
@CustomAnnotation(value = "#{firstValue() + secondValue()}") // (1)
class Example {
}

@Singleton
class AnnotationContext { // (2)
    String firstValue() {
        return "first value"
    }
}

@Singleton
class AnnotationMemberContext { // (3)
    String secondValue() {
        return "second value"
    }
}

@AnnotationExpressionContext(AnnotationContext.class) // (4)
@interface CustomAnnotation {

    @AnnotationExpressionContext(AnnotationMemberContext.class) // (5)
    String value();
}
Usage of annotation level evaluated expression context
package io.micronaut.docs.expressions

import io.micronaut.context.annotation.AnnotationExpressionContext
import jakarta.inject.Singleton

@Singleton
@CustomAnnotation(value = "#{firstValue() + secondValue()}") // (1)
class Example

@Singleton
class AnnotationContext { // (2)
    fun firstValue() = "first value"
}

@Singleton
class AnnotationMemberContext { // (3)
    fun secondValue() = "second value"
}

@AnnotationExpressionContext(AnnotationContext::class) // (4)
annotation class CustomAnnotation(
    @get:AnnotationExpressionContext(AnnotationMemberContext::class) // (5)
    val value: String
)
1 Here two new methods are introduced to the context called firstValue() and secondValue() only for the scope of the @CustomAnnotation
2 The firstValue() method is defined in a bean called AnnotationContext
3 The secondValue() method is defined in a bean called AnnotationMemberContext
4 On the @CustomAnnotation annotation the methods of the AnnotationContext type are exposed to all members of the annotation (type level context).
5 On the value() member of the @CustomAnnotation annotation the methods of the AnnotationContextExample are made available but scoped only to the value() member.

Again context classes need to be explicitly defined as beans to make them available for retrieval from application context at runtime.

Method Invocation

You can invoke both static methods using type references, methods from evaluation context and methods on objects, which means method chaining is supported.

Chaining methods in expression
import io.micronaut.context.annotation.Value;
import jakarta.inject.Singleton;

@Singleton
class CustomEvaluationContext {

    public String stringValue() {
        return "stringValue";
    }

}

@Singleton
class ContextConsumer {

    @Value("#{ #stringValue().length() }")
    public int stringLength;

}

Varargs methods invocation is supported as well. Note that if last parameter of a method is an array, you can still invoke it providing list of arguments separated by comma without explicitly wrapping it into array. So in this case it will be treated in same way as if last method argument was explicitly specified as varargs parameter.

Invoking varargs methods in expressions
import io.micronaut.context.annotation.Value;
import jakarta.inject.Singleton;

@Singleton
class CustomEvaluationContext {

    public int countIntegers(int... values) {
        return values.length;
    }

    public int countStrings(String[] values) {
        return values.length;
    }

}

@Singleton
class ContextConsumer {

    @Value("#{ #countIntegers(1, 2, 3) }")
    public int totalIntegers;

    @Value("#{ #countStrings('a', 'b', 'c') }")
    public int totalStrings;

}

Property Access

JavaBean properties can be accessed simply be referencing their names from evaluation context prefixed with #. Bean properties can also be chained with dot in the same way as methods.

Accessing bean properties in expressions
import io.micronaut.context.annotation.Value;
import jakarta.inject.Singleton;

@Singleton
class CustomEvaluationContext {

    public String getName() {
        return "Bob";
    }

    public int getAge() {
        return 25;
    }

}

@Singleton
class ContextConsumer {

    @Value("#{ 'Name is ' + #name + ', age is ' + #age }")
    public String value;

}

Retrieving Beans from Bean Context

A predefined syntax construct ctx[…​] can be used to retrieve beans from bean context. The argument inside square brackets has to be a fully qualified class name (note that T(…​) wrapper is optional and can be omitted for simplicity).

Retrieving beans from bean context
#{ ctx[T(io.micronaut.example.ContextBean)] }
#{ ctx[io.micronaut.example.ContextBean] }

Retrieving Environment Properties

A syntax construct env[…​] can be used to retrieve environment properties by name. The expression inside square brackets has to resolve to string value, otherwise compilation will fail. If property value will be absent at runtime, the expression will return null

Retrieving Environment Properties
#{ env['test.property'] }

4.5 Configuration Properties

You can create type-safe configuration by creating classes that are annotated with @ConfigurationProperties.

The Micronaut framework will produce a reflection-free @ConfigurationProperties bean and will also at compile time calculate the property paths to evaluate, greatly improving the speed and efficiency of loading @ConfigurationProperties.

For example:

@ConfigurationProperties Example
import io.micronaut.context.annotation.ConfigurationProperties;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import java.util.Optional;

@ConfigurationProperties("my.engine") // (1)
public class EngineConfig {

    public String getManufacturer() {
        return manufacturer;
    }

    public void setManufacturer(String manufacturer) {
        this.manufacturer = manufacturer;
    }

    public int getCylinders() {
        return cylinders;
    }

    public void setCylinders(int cylinders) {
        this.cylinders = cylinders;
    }

    public CrankShaft getCrankShaft() {
        return crankShaft;
    }

    public void setCrankShaft(CrankShaft crankShaft) {
        this.crankShaft = crankShaft;
    }

    @NotBlank // (2)
    private String manufacturer = "Ford"; // (3)

    @Min(1L)
    private int cylinders;

    private CrankShaft crankShaft = new CrankShaft();

    @ConfigurationProperties("crank-shaft")
    public static class CrankShaft { // (4)

        private Optional<Double> rodLength = Optional.empty(); // (5)

        public Optional<Double> getRodLength() {
            return rodLength;
        }

        public void setRodLength(Optional<Double> rodLength) {
            this.rodLength = rodLength;
        }
    }
}
@ConfigurationProperties Example
import io.micronaut.context.annotation.ConfigurationProperties

import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank

@ConfigurationProperties('my.engine') // (1)
class EngineConfig {

    @NotBlank // (2)
    String manufacturer = "Ford" // (3)

    @Min(1L)
    int cylinders

    CrankShaft crankShaft = new CrankShaft()

    @ConfigurationProperties('crank-shaft')
    static class CrankShaft { // (4)
        Optional<Double> rodLength = Optional.empty() // (5)
    }
}
@ConfigurationProperties Example
import io.micronaut.context.annotation.ConfigurationProperties
import java.util.Optional
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank

@ConfigurationProperties("my.engine") // (1)
class EngineConfig {

    @NotBlank // (2)
    var manufacturer = "Ford" // (3)

    @Min(1L)
    var cylinders: Int = 0

    var crankShaft = CrankShaft()

    @ConfigurationProperties("crank-shaft")
    class CrankShaft { // (4)
        var rodLength: Optional<Double> = Optional.empty() // (5)
    }
}
1 The @ConfigurationProperties annotation takes the configuration prefix
2 You can use jakarta.validation annotations to validate the configuration
3 Default values can be assigned to the property
4 Static inner classes can provide nested configuration
5 Optional configuration values can be wrapped in java.util.Optional

Once you have prepared a type-safe configuration it can be injected into your beans like any other bean:

@ConfigurationProperties Dependency Injection
@Singleton
public class EngineImpl implements Engine {
    private final EngineConfig config;

    public EngineImpl(EngineConfig config) { // (1)
        this.config = config;
    }

    @Override
    public int getCylinders() {
        return config.getCylinders();
    }

    @Override
    public String start() {// (2)
        return getConfig().getManufacturer() + " Engine Starting V" + getConfig().getCylinders() +
                " [rodLength=" + getConfig().getCrankShaft().getRodLength().orElse(6d) + "]";
    }

    public final EngineConfig getConfig() {
        return config;
    }
}
@ConfigurationProperties Dependency Injection
@Singleton
class EngineImpl implements Engine {
    final EngineConfig config

    EngineImpl(EngineConfig config) { // (1)
        this.config = config
    }

    @Override
    int getCylinders() {
        config.cylinders
    }

    @Override
    String start() { // (2)
        "$config.manufacturer Engine Starting V$config.cylinders [rodLength=${config.crankShaft.rodLength.orElse(6.0d)}]"
    }
}
@ConfigurationProperties Dependency Injection
@Singleton
class EngineImpl(val config: EngineConfig) : Engine {// (1)

    override val cylinders: Int
        get() = config.cylinders

    override fun start(): String {// (2)
        return "${config.manufacturer} Engine Starting V${config.cylinders} [rodLength=${config.crankShaft.rodLength.orElse(6.0)}]"
    }
}
1 Inject the EngineConfig bean
2 Use the configuration properties

Configuration values can then be supplied from one of the PropertySource instances. For example:

Supply Configuration
Map<String, Object> map = new LinkedHashMap<>(1);
map.put("my.engine.cylinders", "8");
ApplicationContext applicationContext = ApplicationContext.run(map, "test");

Vehicle vehicle = applicationContext.getBean(Vehicle.class);
System.out.println(vehicle.start());
Supply Configuration
ApplicationContext applicationContext = ApplicationContext.run(
        ['my.engine.cylinders': '8'],
        "test"
)

def vehicle = applicationContext.getBean(Vehicle)
println(vehicle.start())
Supply Configuration
val map = mapOf( "my.engine.cylinders" to "8")
val applicationContext = ApplicationContext.run(map, "test")

val vehicle = applicationContext.getBean(Vehicle::class.java)
println(vehicle.start())

The above example prints: "Ford Engine Starting V8 [rodLength=6.0]"

You can directly reference configuration properties in @Requires annotation to conditionally load beans using the following syntax: @Requires(bean=Config.class, beanProperty="property", value="true")

Note for more complex configurations you can structure @ConfigurationProperties beans through inheritance.

For example creating a subclass of EngineConfig with @ConfigurationProperties('bar') will resolve all properties under the path my.engine.bar.

Includes / Excludes

For the cases where the configuration properties class inherits properties from a parent class, it may be desirable to exclude properties from the parent class. The includes and excludes members of the @ConfigurationProperties annotation allow for that functionality. The list applies to both local properties and inherited properties.

The names supplied to the includes/excludes list must be the "property" name. For example if a setter method is injected, the property name is the de-capitalized setter name (setConnectionTimeoutconnectionTimeout).

Change accessors style

Since 3.3, the Micronaut framework supports defining different accessors prefixes for getters and setter other than the default get and set defined for Java Beans. Annotate your POJO or @ConfigurationProperties class with the @AccessorsStyle annotation.

This is useful when you write the getters and setters in a fluent way. For example:

Using @AccessorsStyle
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.annotation.AccessorsStyle;

@AccessorsStyle(readPrefixes = "", writePrefixes = "") (1)
@ConfigurationProperties("my.engine")
public class EngineConfig {

    private String manufacturer;
    private int cylinders;

    public EngineConfig(String manufacturer, int cylinders) {
        this.manufacturer = manufacturer;
        this.cylinders = cylinders;
    }

    public String manufacturer() { (2)
        return manufacturer;
    }

    public void manufacturer(String manufacturer) { (2)
        this.manufacturer = manufacturer;
    }

    public int cylinders() { (2)
        return cylinders;
    }

    public void cylinders(int cylinders) { (2)
        this.cylinders = cylinders;
    }

}
1 The Micronaut framework will use an empty prefix for getters and setters.
2 Define the getters and setters with an empty prefix.

Now you can inject EngineConfig and use it with engineConfig.manufacturer() and engineConfig.cylinders() to retrieve the values from configuration.

Property Type Conversion

The Micronaut framework uses the ConversionService bean to convert values when resolving properties. You can register additional converters for types not supported by Micronaut by defining beans that implement the TypeConverter interface.

The Micronaut framework features some built-in conversions that are useful, which are detailed below.

Duration Conversion

Durations can be specified by appending the unit with a number. Supported units are s, ms, m etc. The following table summarizes examples:

Table 1. Duration Conversion
Configuration Value Resulting Value

10ms

Duration of 10 milliseconds

10m

Duration of 10 minutes

10s

Duration of 10 seconds

10d

Duration of 10 days

10h

Duration of 10 hours

10ns

Duration of 10 nanoseconds

PT15M

Duration of 15 minutes using ISO-8601 format

For example to configure the default HTTP client read timeout:

Using Duration Values
micronaut.http.client.read-timeout=15s
micronaut:
  http:
    client:
      read-timeout: 15s
[micronaut]
  [micronaut.http]
    [micronaut.http.client]
      read-timeout="15s"
micronaut {
  http {
    client {
      readTimeout = "15s"
    }
  }
}
{
  micronaut {
    http {
      client {
        read-timeout = "15s"
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "client": {
        "read-timeout": "15s"
      }
    }
  }
}

List / Array Conversion

Lists and arrays can be specified in Java properties files as comma-separated values, or in YAML using native YAML lists. The generic types are used to convert the values. For example in YAML:

Specifying lists or arrays in YAML
my.app.integers[0]=1
my.app.integers[1]=2
my.app.urls[0]=http://foo.com
my.app.urls[1]=http://bar.com
my:
  app:
    integers:
      - 1
      - 2
    urls:
      - http://foo.com
      - http://bar.com
[my]
  [my.app]
    integers=[
      1,
      2
    ]
    urls=[
      "http://foo.com",
      "http://bar.com"
    ]
my {
  app {
    integers = [1, 2]
    urls = ["http://foo.com", "http://bar.com"]
  }
}
{
  my {
    app {
      integers = [1, 2]
      urls = ["http://foo.com", "http://bar.com"]
    }
  }
}
{
  "my": {
    "app": {
      "integers": [1, 2],
      "urls": ["http://foo.com", "http://bar.com"]
    }
  }
}

For the above example configurations you can define properties to bind to with the target type supplied via generics:

List<Integer> integers;
List<URL> urls;

Readable Bytes

You can annotate any setter parameter with @ReadableBytes to allow the value to be set using a shorthand syntax for specifying bytes, kilobytes etc. For example the following is taken from HttpClientConfiguration:

Using @ReadableBytes
public void setMaxContentLength(@ReadableBytes int maxContentLength) {
    this.maxContentLength = maxContentLength;
}

With the above in place you can set micronaut.http.client.max-content-length using the following values:

Table 2. @ReadableBytes Conversion
Configuration Value Resulting Value

10mb

10 megabytes

10kb

10 kilobytes

10gb

10 gigabytes

1024

A raw byte length

Formatting Dates

The @Format annotation can be used on setters to specify the date format to use when binding java.time date objects.

Using @Format for Dates
public void setMyDate(@Format("yyyy-MM-dd") LocalDate date) {
    this.myDate = date;
}

Configuration Builder

Many frameworks and tools already use builder-style classes to construct configuration.

You can use the @ConfigurationBuilder annotation to populate a builder-style class with configuration values. ConfigurationBuilder can be applied to fields or methods in a class annotated with @ConfigurationProperties.

Since there is no consistent way to define builders in the Java world, one or more method prefixes can be specified in the annotation to support builder methods like withXxx or setXxx. If the builder methods have no prefix, assign an empty string to the parameter.

A configuration prefix can also be specified to tell the Micronaut framework where to look for configuration values. By default, builder methods use the configuration prefix specified in a class-level @ConfigurationProperties annotation.

For example:

@ConfigurationBuilder Example
import io.micronaut.context.annotation.ConfigurationBuilder;
import io.micronaut.context.annotation.ConfigurationProperties;

@ConfigurationProperties("my.engine") // (1)
class EngineConfig {

    @ConfigurationBuilder(prefixes = "with") // (2)
    EngineImpl.Builder builder = EngineImpl.builder();

    @ConfigurationBuilder(prefixes = "with", configurationPrefix = "crank-shaft") // (3)
    CrankShaft.Builder crankShaft = CrankShaft.builder();

    private SparkPlug.Builder sparkPlug = SparkPlug.builder();

    SparkPlug.Builder getSparkPlug() {
        return sparkPlug;
    }

    @ConfigurationBuilder(prefixes = "with", configurationPrefix = "spark-plug") // (4)
    void setSparkPlug(SparkPlug.Builder sparkPlug) {
        this.sparkPlug = sparkPlug;
    }
}
@ConfigurationBuilder Example
import io.micronaut.context.annotation.ConfigurationBuilder
import io.micronaut.context.annotation.ConfigurationProperties

@ConfigurationProperties('my.engine') // (1)
class EngineConfig {

    @ConfigurationBuilder(prefixes = "with") // (2)
    EngineImpl.Builder builder = EngineImpl.builder()

    @ConfigurationBuilder(prefixes = "with", configurationPrefix = "crank-shaft") // (3)
    CrankShaft.Builder crankShaft = CrankShaft.builder()

    SparkPlug.Builder sparkPlug = SparkPlug.builder()

    @ConfigurationBuilder(prefixes = "with", configurationPrefix = "spark-plug") // (4)
    void setSparkPlug(SparkPlug.Builder sparkPlug) {
        this.sparkPlug = sparkPlug
    }
}
@ConfigurationBuilder Example
import io.micronaut.context.annotation.ConfigurationBuilder
import io.micronaut.context.annotation.ConfigurationProperties

@ConfigurationProperties("my.engine") // (1)
internal class EngineConfig {

    @ConfigurationBuilder(prefixes = ["with"])  // (2)
    val builder = EngineImpl.builder()

    @ConfigurationBuilder(prefixes = ["with"], configurationPrefix = "crank-shaft") // (3)
    val crankShaft = CrankShaft.builder()

    @set:ConfigurationBuilder(prefixes = ["with"], configurationPrefix = "spark-plug") // (4)
    var sparkPlug = SparkPlug.builder()
}
1 The @ConfigurationProperties annotation takes the configuration prefix
2 The first builder can be configured without the class configuration prefix; it inherits from the above.
3 The second builder can be configured with the class configuration prefix + the configurationPrefix value.
4 The third builder demonstrates that the annotation can be applied to a method as well as a property.
By default, only single-argument builder methods are supported. For methods with no arguments, set the allowZeroArgs parameter of the annotation to true.

Like in the previous example, we can construct an EngineImpl. Since we are using a builder, we can use a factory class to build the engine from the builder.

Factory Bean
import io.micronaut.context.annotation.Factory;

import jakarta.inject.Singleton;

@Factory
class EngineFactory {

    @Singleton
    EngineImpl buildEngine(EngineConfig engineConfig) {
        return engineConfig.builder.build(engineConfig.crankShaft, engineConfig.getSparkPlug());
    }
}
Factory Bean
import io.micronaut.context.annotation.Factory

import jakarta.inject.Singleton

@Factory
class EngineFactory {

    @Singleton
    EngineImpl buildEngine(EngineConfig engineConfig) {
        engineConfig.builder.build(engineConfig.crankShaft, engineConfig.sparkPlug)
    }
}
Factory Bean
import io.micronaut.context.annotation.Factory
import jakarta.inject.Singleton

@Factory
internal class EngineFactory {

    @Singleton
    fun buildEngine(engineConfig: EngineConfig): EngineImpl {
        return engineConfig.builder.build(engineConfig.crankShaft, engineConfig.sparkPlug)
    }
}

The engine that was returned can then be injected anywhere an engine is required.

Configuration values can be supplied from one of the PropertySource instances. For example:

Supply Configuration
        Map<String, Object> properties = new HashMap<>();
        properties.put("my.engine.cylinders"             ,"4");
        properties.put("my.engine.manufacturer"          , "Subaru");
        properties.put("my.engine.crank-shaft.rod-length", 4);
        properties.put("my.engine.spark-plug.name"       , "6619 LFR6AIX");
        properties.put("my.engine.spark-plug.type"       , "Iridium");
        properties.put("my.engine.spark-plug.companyName", "NGK");
        ApplicationContext applicationContext = ApplicationContext.run(properties, "test");

        Vehicle vehicle = applicationContext.getBean(Vehicle.class);
        System.out.println(vehicle.start());
Supply Configuration
        ApplicationContext applicationContext = ApplicationContext.run(
                ['my.engine.cylinders'             : '4',
                 'my.engine.manufacturer'          : 'Subaru',
                 'my.engine.crank-shaft.rod-length': 4,
                 'my.engine.spark-plug.name'       : '6619 LFR6AIX',
                 'my.engine.spark-plug.type'       : 'Iridium',
                 'my.engine.spark-plug.companyName': 'NGK'
                ],
                "test"
        )

        Vehicle vehicle = applicationContext.getBean(Vehicle)
        println(vehicle.start())
Supply Configuration
        val applicationContext = ApplicationContext.run(
                mapOf(
                        "my.engine.cylinders" to "4",
                        "my.engine.manufacturer" to "Subaru",
                        "my.engine.crank-shaft.rod-length" to 4,
                        "my.engine.spark-plug.name" to "6619 LFR6AIX",
                        "my.engine.spark-plug.type" to "Iridium",
                        "my.engine.spark-plug.company" to "NGK"
                ),
                "test"
        )

        val vehicle = applicationContext.getBean(Vehicle::class.java)
        println(vehicle.start())

The above example prints: "Subaru Engine Starting V4 [rodLength=4.0, sparkPlug=Iridium(NGK 6619 LFR6AIX)]"

MapFormat

For some use cases it may be desirable to accept a map of arbitrary configuration properties that can be supplied to a bean, especially if the bean represents a third-party API where not all the possible configuration properties are known. For example, a datasource may accept a map of configuration properties specific to a particular database driver, allowing the user to specify any desired options in the map without coding each property explicitly.

For this purpose, the MapFormat annotation lets you bind a map to a single configuration property, and specify whether to accept a flat map of keys to values, or a nested map (where the values may be additional maps).

@MapFormat Example
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.convert.format.MapFormat;

import jakarta.validation.constraints.Min;
import java.util.Map;

@ConfigurationProperties("my.engine")
public class EngineConfig {

    @Min(1L)
    private int cylinders;

    @MapFormat(transformation = MapFormat.MapTransformation.FLAT) //(1)
    private Map<Integer, String> sensors;

    public int getCylinders() {
        return cylinders;
    }

    public void setCylinders(int cylinders) {
        this.cylinders = cylinders;
    }

    public Map<Integer, String> getSensors() {
        return sensors;
    }

    public void setSensors(Map<Integer, String> sensors) {
        this.sensors = sensors;
    }
}
@MapFormat Example
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.convert.format.MapFormat

import jakarta.validation.constraints.Min

@ConfigurationProperties('my.engine')
class EngineConfig {

    @Min(1L)
    int cylinders

    @MapFormat(transformation = MapFormat.MapTransformation.FLAT) //(1)
    Map<Integer, String> sensors
}
@MapFormat Example
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.convert.format.MapFormat
import jakarta.validation.constraints.Min

@ConfigurationProperties("my.engine")
class EngineConfig {

    @Min(1L)
    var cylinders: Int = 0

    @MapFormat(transformation = MapFormat.MapTransformation.FLAT) //(1)
    var sensors: Map<Int, String>? = null
}
1 Note the transformation argument to the annotation; possible values are MapTransformation.FLAT (for flat maps) and MapTransformation.NESTED (for nested maps)
EngineImpl
@Singleton
public class EngineImpl implements Engine {

    @Inject
    EngineConfig config;

    @Override
    public Map getSensors() {
        return config.getSensors();
    }

    @Override
    public String start() {
        return "Engine Starting V" + getConfig().getCylinders() +
               " [sensors=" + getSensors().size() + "]";
    }

    public EngineConfig getConfig() {
        return config;
    }

    public void setConfig(EngineConfig config) {
        this.config = config;
    }
}
EngineImpl
@Singleton
class EngineImpl implements Engine {

    @Inject EngineConfig config

    @Override
    Map getSensors() {
        config.sensors
    }

    @Override
    String start() {
        "Engine Starting V$config.cylinders [sensors=${sensors.size()}]"
    }
}
EngineImpl
@Singleton
class EngineImpl : Engine {

    override val sensors: Map<*, *>?
        get() = config!!.sensors

    @Inject
    var config: EngineConfig? = null

    override fun start(): String {
        return "Engine Starting V${config!!.cylinders} [sensors=${sensors!!.size}]"
    }
}

Now a map of properties can be supplied to the my.engine.sensors configuration property.

Use Map Configuration
Map<String, Object> map = new LinkedHashMap<>(2);
map.put("my.engine.cylinders", "8");

Map<Integer, String> map1 = new LinkedHashMap<>(2);
map1.put(0, "thermostat");
map1.put(1, "fuel pressure");

map.put("my.engine.sensors", map1);

ApplicationContext applicationContext = ApplicationContext.run(map, "test");

Vehicle vehicle = applicationContext.getBean(Vehicle.class);
System.out.println(vehicle.start());
Use Map Configuration
ApplicationContext applicationContext = ApplicationContext.run(
        ['my.engine.cylinders': '8',
         'my.engine.sensors'  : [0: 'thermostat',
                                 1: 'fuel pressure']],
        "test"
)

def vehicle = applicationContext.getBean(Vehicle)
println(vehicle.start())
Use Map Configuration
val subMap = mapOf(
    0 to "thermostat",
    1 to "fuel pressure"
)
val map = mapOf(
    "my.engine.cylinders" to "8",
    "my.engine.sensors" to subMap
)

val applicationContext = ApplicationContext.run(map, "test")

val vehicle = applicationContext.getBean(Vehicle::class.java)
println(vehicle.start())

The above example prints: "Engine Starting V8 [sensors=2]"

See the guide for @Configuration and @ConfigurationBuilder to learn more.

4.6 Custom Type Converters

The Micronaut framework includes an extensible type conversion mechanism. To add additional type converters you register beans of type TypeConverter.

The following example shows how to use one of the built-in converters (Map to an Object) or create your own.

Consider the following ConfigurationProperties:

@ConfigurationProperties(MyConfigurationProperties.PREFIX)
public class MyConfigurationProperties {

    public static final String PREFIX = "myapp";

    protected LocalDate updatedAt;

    public LocalDate getUpdatedAt() {
        return updatedAt;
    }
}
@ConfigurationProperties(MyConfigurationProperties.PREFIX)
class MyConfigurationProperties {

    public static final String PREFIX = "myapp"

    protected LocalDate updatedAt

    LocalDate getUpdatedAt() {
        updatedAt
    }
}
@ConfigurationProperties(MyConfigurationProperties.PREFIX)
class MyConfigurationProperties {

    var updatedAt: LocalDate? = null
        protected set

    companion object {
        const val PREFIX = "myapp"
    }
}

The type MyConfigurationProperties has a property named updatedAt of type LocalDate.

To bind this property from a map via configuration:

private static ApplicationContext ctx;

@BeforeAll
static void setupCtx() {
    ctx = ApplicationContext.run(
            new LinkedHashMap<>() {{
                put("myapp.updatedAt", // (1)
                        new LinkedHashMap<String, Integer>() {{
                            put("day", 28);
                            put("month", 10);
                            put("year", 1982);
                        }}
                );
            }}
    );
}

@AfterAll
static void teardownCtx() {
    if(ctx != null) {
        ctx.stop();
    }
}
@AutoCleanup
@Shared
ApplicationContext ctx = ApplicationContext.run(
        "myapp.updatedAt": [day: 28, month: 10, year: 1982]  // (1)
)
lateinit var ctx: ApplicationContext

@BeforeEach
fun setup() {
    ctx = ApplicationContext.run(
        mapOf(
            "myapp.updatedAt" to mapOf( // (1)
                "day" to 28,
                "month" to 10,
                "year" to 1982
            )
        )
    )
}

@AfterEach
fun teardown() {
    ctx.close()
}
1 Note how we match the myapp prefix and updatedAt property name in our MyConfigurationProperties class above

This won’t work by default, since there is no built-in conversion from Map to LocalDate. To resolve this, define a custom TypeConverter:

import io.micronaut.context.annotation.Prototype;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.TypeConverter;

import java.time.DateTimeException;
import java.time.LocalDate;
import java.util.Map;
import java.util.Optional;

@Prototype
public class MapToLocalDateConverter implements TypeConverter<Map, LocalDate> { // (1)

    private final ConversionService  conversionService;

    public MapToLocalDateConverter(ConversionService conversionService) { // (2)
        this.conversionService = conversionService;
    }

    @Override
    public Optional<LocalDate> convert(Map propertyMap, Class<LocalDate> targetType, ConversionContext context) {
        Optional<Integer> day = conversionService.convert(propertyMap.get("day"), Integer.class);
        Optional<Integer> month = conversionService.convert(propertyMap.get("month"), Integer.class);
        Optional<Integer> year = conversionService.convert(propertyMap.get("year"), Integer.class);
        if (day.isPresent() && month.isPresent() && year.isPresent()) {
            try {
                return Optional.of(LocalDate.of(year.get(), month.get(), day.get())); // (3)
            } catch (DateTimeException e) {
                context.reject(propertyMap, e); // (4)
                return Optional.empty();
            }
        }

        return Optional.empty();
    }
}
import io.micronaut.core.convert.ConversionService
import io.micronaut.core.convert.TypeConverter

import java.time.DateTimeException
import java.time.LocalDate

@Prototype
class MapToLocalDateConverter implements TypeConverter<Map, LocalDate> { // (1)

    final ConversionService  conversionService

    MapToLocalDateConverter(ConversionService conversionService) { // (2)
        this.conversionService = conversionService;
    }

    @Override
    Optional<LocalDate> convert(Map propertyMap, Class<LocalDate> targetType, ConversionContext context) {
        Optional<Integer> day = conversionService.convert(propertyMap.day, Integer)
        Optional<Integer> month = conversionService.convert(propertyMap.month, Integer)
        Optional<Integer> year = conversionService.convert(propertyMap.year, Integer)
        if (day.present && month.present && year.present) {
            try {
                return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // (3)
            } catch (DateTimeException e) {
                context.reject(propertyMap, e) // (4)
                return Optional.empty()
            }
        }
        return Optional.empty()
    }
}
import io.micronaut.context.annotation.Prototype
import io.micronaut.core.convert.ConversionContext
import io.micronaut.core.convert.ConversionService
import io.micronaut.core.convert.TypeConverter
import java.time.DateTimeException
import java.time.LocalDate
import java.util.Optional

@Prototype
class MapToLocalDateConverter(
    private val conversionService: ConversionService // (2)
)
    : TypeConverter<Map<*, *>, LocalDate> { // (1)

    override fun convert(propertyMap: Map<*, *>, targetType: Class<LocalDate>, context: ConversionContext): Optional<LocalDate> {
        val day = conversionService.convert(propertyMap["day"], Int::class.java)
        val month = conversionService.convert(propertyMap["month"], Int::class.java)
        val year = conversionService.convert(propertyMap["year"], Int::class.java)
        if (day.isPresent && month.isPresent && year.isPresent) {
            try {
                return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) // (3)
            } catch (e: DateTimeException) {
                context.reject(propertyMap, e) // (4)
                return Optional.empty()
            }
        }

        return Optional.empty()
    }
}
1 The class implements TypeConverter which has two generic arguments, the type you are converting from, and the type you are converting to
2 The constructor injects a bean of type ConversionService, introduced in Micronaut 4, instead of making static calls to ConversionService.SHARED used in previous versions
3 The implementation delegates to the injected conversion service to convert the values from the Map used to create a LocalDate
4 If an exception occurs during binding, call reject(..) which propagates additional information to the container
It’s possible to add a custom type converter into ConversionService.SHARED by registering it in a TypeConverterRegistrar via the service loader.

4.7 Using @EachProperty to Drive Configuration

The @ConfigurationProperties annotation is great for a single configuration class, but sometimes you want multiple instances, each with its own distinct configuration. That is where EachProperty comes in.

The @EachProperty annotation creates a ConfigurationProperties bean for each sub-property within the given name. As an example consider the following class:

Using @EachProperty
import java.net.URI;
import java.net.URISyntaxException;

import io.micronaut.context.annotation.Parameter;
import io.micronaut.context.annotation.EachProperty;

@EachProperty("test.datasource")  // (1)
public class DataSourceConfiguration {

    private final String name;
    private URI url = new URI("localhost");

    public DataSourceConfiguration(@Parameter String name) // (2)
            throws URISyntaxException {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public URI getUrl() { // (3)
        return url;
    }

    public void setUrl(URI url) {
        this.url = url;
    }
}
Using @EachProperty
import io.micronaut.context.annotation.EachProperty
import io.micronaut.context.annotation.Parameter

@EachProperty("test.datasource") // (1)
class DataSourceConfiguration {

    final String name
    URI url = new URI("localhost") // (3)

    DataSourceConfiguration(@Parameter String name) // (2)
            throws URISyntaxException {
        this.name = name
    }
}
Using @EachProperty
import io.micronaut.context.annotation.EachProperty
import io.micronaut.context.annotation.Parameter
import java.net.URI
import java.net.URISyntaxException

@EachProperty("test.datasource")  // (1)
class DataSourceConfiguration
@Throws(URISyntaxException::class)
constructor(@param:Parameter val name: String) { // (2)
    var url = URI("localhost") // (3)
}
1 The @EachProperty annotation defines the property name to be handled.
2 The @Parameter annotation can be used to inject the name of the sub-property that defines the name of the bean (which is also the bean qualifier)
3 Each property of the bean is bound to configuration.
Micronaut configuration uses kebap case, not lower camel case. For example, using @EachProperty("my-bean") works, but @EachProperty("myBean") fails.

The above DataSourceConfiguration defines a url property to configure one or more data sources. The URLs themselves can be configured using any of the PropertySource instances evaluated to Micronaut:

Providing Configuration to @EachProperty
ApplicationContext applicationContext = ApplicationContext.run(PropertySource.of(
        "test",
        CollectionUtils.mapOf(
                "test.datasource.one.url", "jdbc:mysql://localhost/one",
                "test.datasource.two.url", "jdbc:mysql://localhost/two")
));
Providing Configuration to @EachProperty
ApplicationContext applicationContext = ApplicationContext.run(PropertySource.of(
        "test",
        [
                "test.datasource.one.url": "jdbc:mysql://localhost/one",
                "test.datasource.two.url": "jdbc:mysql://localhost/two"
        ]
))
Providing Configuration to @EachProperty
val applicationContext = ApplicationContext.run(
    PropertySource.of(
        "test",
        mapOf(
            "test.datasource.one.url" to "jdbc:mysql://localhost/one",
            "test.datasource.two.url" to "jdbc:mysql://localhost/two"
        )
    )
)

In the above example two data sources (called one and two) are defined under the test.datasource prefix defined earlier in the @EachProperty annotation. Each of these configuration entries triggers the creation of a new DataSourceConfiguration bean such that the following test succeeds:

Evaluating Beans Built by @EachProperty
Collection<DataSourceConfiguration> beansOfType = applicationContext.getBeansOfType(DataSourceConfiguration.class);
assertEquals(2, beansOfType.size()); // (1)

DataSourceConfiguration firstConfig = applicationContext.getBean(
        DataSourceConfiguration.class,
        Qualifiers.byName("one") // (2)
);

assertEquals(new URI("jdbc:mysql://localhost/one"), firstConfig.getUrl());
Evaluating Beans Built by @EachProperty
when:
Collection<DataSourceConfiguration> beansOfType = applicationContext.getBeansOfType(DataSourceConfiguration.class)

then:
beansOfType.size() == 2 // (1)

when:
DataSourceConfiguration firstConfig = applicationContext.getBean(
        DataSourceConfiguration.class,
        Qualifiers.byName("one") // (2)
)

then:
new URI("jdbc:mysql://localhost/one") == firstConfig.getUrl()
Evaluating Beans Built by @EachProperty
val beansOfType = applicationContext.getBeansOfType(DataSourceConfiguration::class.java)
beansOfType.size shouldBe 2 // (1)

val firstConfig = applicationContext.getBean(
    DataSourceConfiguration::class.java,
    Qualifiers.byName("one") // (2)
)

firstConfig.url shouldBe URI("jdbc:mysql://localhost/one")
1 All beans of type DataSourceConfiguration can be retrieved using getBeansOfType
2 Individual beans can be retrieved by using the byName qualifier.

List-Based Binding

The default behavior of @EachProperty is to bind from a map style of configuration, where the key is the named qualifier of the bean and the value is the data to bind from. For cases where map style configuration doesn’t make sense, it is possible to inform the Micronaut framework that the class is bound from a list. Simply set the list member on the annotation to true.

@EachProperty List Example
import io.micronaut.context.annotation.EachProperty;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.core.order.Ordered;

import java.time.Duration;

@EachProperty(value = "ratelimits", list = true) // (1)
public class RateLimitsConfiguration implements Ordered { // (2)

    private final Integer index;
    private Duration period;
    private Integer limit;

    RateLimitsConfiguration(@Parameter Integer index) { // (3)
        this.index = index;
    }

    @Override
    public int getOrder() {
        return index;
    }

    public Duration getPeriod() {
        return period;
    }

    public void setPeriod(Duration period) {
        this.period = period;
    }

    public Integer getLimit() {
        return limit;
    }

    public void setLimit(Integer limit) {
        this.limit = limit;
    }
}
@EachProperty List Example
import io.micronaut.context.annotation.EachProperty
import io.micronaut.context.annotation.Parameter
import io.micronaut.core.order.Ordered

import java.time.Duration

@EachProperty(value = "ratelimits", list = true) // (1)
class RateLimitsConfiguration implements Ordered { // (2)

    private final Integer index
    Duration period
    Integer limit

    RateLimitsConfiguration(@Parameter Integer index) { // (3)
        this.index = index
    }

    @Override
    int getOrder() {
        index
    }
}
@EachProperty List Example
import io.micronaut.context.annotation.EachProperty
import io.micronaut.context.annotation.Parameter
import io.micronaut.core.order.Ordered
import java.time.Duration

@EachProperty(value = "ratelimits", list = true) // (1)
class RateLimitsConfiguration
    constructor(@param:Parameter private val index: Int) // (3)
    : Ordered { // (2)

    var period: Duration? = null
    var limit: Int? = null

    override fun getOrder(): Int {
        return index
    }
}
1 The list member of the annotation is set to true
2 Implement Ordered if order matters when retrieving the beans
3 The index is injected into the constructor

4.8 Using @EachBean to Drive Configuration

The @EachProperty annotation is a great way to drive dynamic configuration, but typically you want to inject that configuration into another bean that depends on it. Injecting a single instance with a hard-coded qualifier is not a great solution, hence @EachProperty is typically used in combination with @EachBean:

Using @EachBean
@Factory // (1)
public class DataSourceFactory {

    @EachBean(DataSourceConfiguration.class) // (2)
    DataSource dataSource(DataSourceConfiguration configuration) { // (3)
        URI url = configuration.getUrl();
        return new DataSource(url);
    }
Using @EachBean
@Factory // (1)
class DataSourceFactory {

    @EachBean(DataSourceConfiguration) // (2)
    DataSource dataSource(DataSourceConfiguration configuration) { // (3)
        URI url = configuration.url
        return new DataSource(url)
    }
Using @EachBean
@Factory // (1)
class DataSourceFactory {

    @EachBean(DataSourceConfiguration::class) // (2)
    internal fun dataSource(configuration: DataSourceConfiguration): DataSource { // (3)
        val url = configuration.url
        return DataSource(url)
    }
1 The above example defines a bean Factory that creates instances of javax.sql.DataSource.
2 The @EachBean annotation indicates that a new DataSource bean will be created for each DataSourceConfiguration defined in the previous section.
3 The DataSourceConfiguration instance is injected as a method argument and used to drive the configuration of each javax.sql.DataSource
@EachBean requires that the parent bean has a @Named qualifier, since the qualifier is inherited by each bean created by @EachBean.

In other words, to retrieve the DataSource created by test.datasource.one you can do:

Using a Qualifier
Collection<DataSource> beansOfType = applicationContext.getBeansOfType(DataSource.class);
assertEquals(2, beansOfType.size()); // (1)

DataSource firstConfig = applicationContext.getBean(
        DataSource.class,
        Qualifiers.byName("one") // (2)
);
Using a Qualifier
when:
Collection<DataSource> beansOfType = applicationContext.getBeansOfType(DataSource)

then:
beansOfType.size() == 2 // (1)

when:
DataSource firstConfig = applicationContext.getBean(
        DataSource,
        Qualifiers.byName("one") // (2)
)
Using a Qualifier
val beansOfType = applicationContext.getBeansOfType(DataSource::class.java)
beansOfType.size shouldBe 2 // (1)

val firstConfig = applicationContext.getBean(
        DataSource::class.java,
        Qualifiers.byName("one") // (2)
)
1 We demonstrate here that there are indeed two data sources. How can we get one in particular?
2 By using Qualifiers.byName("one"), we can select which of the two beans we’d like to reference.

4.9 Immutable Configuration

Since 1.3, Micronaut framework supports the definition of immutable configuration. Immutable configuration with an interface requires the Micronaut Context dependency.

implementation("io.micronaut:micronaut-context")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-context</artifactId>
</dependency>

micronaut-context is a transitive dependency of micronaut-http. If you use a Micronaut HTTP runtime, your project already includes the Micronaut-context dependency.

There are two ways to define immutable configuration. The preferred way is to define an interface annotated with @ConfigurationProperties. For example:

@ConfigurationProperties Example
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.bind.annotation.Bindable;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.Optional;

@ConfigurationProperties("my.engine") // (1)
public interface EngineConfig {

    @Bindable(defaultValue = "Ford") // (2)
    @NotBlank // (3)
    String getManufacturer();

    @Min(1L)
    int getCylinders();

    @NotNull
    CrankShaft getCrankShaft(); // (4)

    @ConfigurationProperties("crank-shaft")
    interface CrankShaft { // (5)
        Optional<Double> getRodLength(); // (6)
    }
}
@ConfigurationProperties Example
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.bind.annotation.Bindable

import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull

@ConfigurationProperties("my.engine") // (1)
interface EngineConfig {

    @Bindable(defaultValue = "Ford") // (2)
    @NotBlank // (3)
    String getManufacturer()

    @Min(1L)
    int getCylinders()

    @NotNull
    CrankShaft getCrankShaft() // (4)

    @ConfigurationProperties("crank-shaft")
    static interface CrankShaft { // (5)
        Optional<Double> getRodLength() // (6)
    }
}
@ConfigurationProperties Example
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.bind.annotation.Bindable
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull

@ConfigurationProperties("my.engine") // (1)
interface EngineConfig {

    @get:Bindable(defaultValue = "Ford") // (2)
    @get:NotBlank // (3)
    val manufacturer: String

    @get:Min(1L)
    val cylinders: Int

    @get:NotNull
    val crankShaft: CrankShaft // (4)

    @ConfigurationProperties("crank-shaft")
    interface CrankShaft { // (5)
        val rodLength: Double? // (6)
    }
}
1 The @ConfigurationProperties annotation takes the configuration prefix and is declared on an interface
2 You can use @Bindable to set a default value
3 Validation annotations can also be used
4 You can also specify references to other @ConfigurationProperties beans.
5 You can nest immutable configuration
6 Optional configuration can be indicated by returning an Optional or specifying @Nullable

In this case the Micronaut framework provides a compile-time implementation that delegates all getters to call the getProperty(..) method of the Environment interface.

This has the advantage that if the application configuration is refreshed (for example by invoking the /refresh endpoint), the injected interface automatically sees the new values.

If you try to specify any other abstract method other than a getter, a compilation error occurs (default methods are supported).

Another way to implement immutable configuration is to define a class and use the @ConfigurationInject annotation on a constructor of a @ConfigurationProperties or @EachProperty bean.

For example:

@ConfigurationProperties Example
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.context.annotation.ConfigurationInject;
import io.micronaut.context.annotation.ConfigurationProperties;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.Optional;

@ConfigurationProperties("my.engine") // (1)
public class EngineConfig {

    private final String manufacturer;
    private final int cylinders;
    private final CrankShaft crankShaft;

    @ConfigurationInject // (2)
    public EngineConfig(
            @Bindable(defaultValue = "Ford") @NotBlank String manufacturer, // (3)
            @Min(1L) int cylinders, // (4)
            @NotNull CrankShaft crankShaft) {
        this.manufacturer = manufacturer;
        this.cylinders = cylinders;
        this.crankShaft = crankShaft;
    }

    public String getManufacturer() {
        return manufacturer;
    }

    public int getCylinders() {
        return cylinders;
    }

    public CrankShaft getCrankShaft() {
        return crankShaft;
    }

    @ConfigurationProperties("crank-shaft")
    public static class CrankShaft { // (5)
        private final Double rodLength; // (6)

        @ConfigurationInject
        public CrankShaft(@Nullable Double rodLength) {
            this.rodLength = rodLength;
        }

        public Optional<Double> getRodLength() {
            return Optional.ofNullable(rodLength);
        }
    }
}
@ConfigurationProperties Example
import io.micronaut.context.annotation.ConfigurationInject
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.bind.annotation.Bindable

import jakarta.annotation.Nullable
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull

@ConfigurationProperties("my.engine") // (1)
class EngineConfig {

    final String manufacturer
    final int cylinders
    final CrankShaft crankShaft

    @ConfigurationInject // (2)
    EngineConfig(
            @Bindable(defaultValue = "Ford") @NotBlank String manufacturer, // (3)
            @Min(1L) int cylinders, // (4)
            @NotNull CrankShaft crankShaft) {
        this.manufacturer = manufacturer
        this.cylinders = cylinders
        this.crankShaft = crankShaft
    }

    @ConfigurationProperties("crank-shaft")
    static class CrankShaft { // (5)
        private final Double rodLength // (6)

        @ConfigurationInject
        CrankShaft(@Nullable Double rodLength) {
            this.rodLength = rodLength
        }

        Optional<Double> getRodLength() {
            Optional.ofNullable(rodLength)
        }
    }
}
@ConfigurationProperties Example
import io.micronaut.context.annotation.ConfigurationInject
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.bind.annotation.Bindable
import java.util.Optional
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull

@ConfigurationProperties("my.engine") // (1)
data class EngineConfig @ConfigurationInject // (2)
    constructor(
        @Bindable(defaultValue = "Ford") @NotBlank val manufacturer: String, // (3)
        @Min(1) val cylinders: Int, // (4)
        @NotNull val crankShaft: CrankShaft) {

    @ConfigurationProperties("crank-shaft")
    data class CrankShaft @ConfigurationInject
    constructor(// (5)
            private val rodLength: Double? // (6)
    ) {

        fun getRodLength(): Optional<Double> {
            return Optional.ofNullable(rodLength)
        }
    }
}
1 The @ConfigurationProperties annotation takes the configuration prefix
2 The @ConfigurationInject annotation is defined on the constructor
3 You can use @Bindable to set a default value
4 Validation annotations can be used too
5 You can nest immutable configuration
6 Optional configuration can be indicated with @Nullable

The @ConfigurationInject annotation provides a hint to the Micronaut framework to prioritize binding values from configuration instead of injecting beans.

Using this approach, to make the configuration refreshable, add the @Refreshable annotation to the class as well. This allows the bean to be re-created in the case of a runtime configuration refresh event.

There are a few exceptions to this rule. Micronaut framework will not perform configuration binding for a parameter if any of these conditions is met:

  • The parameter is annotated with @Value (explicit binding)

  • The parameter is annotated with @Property (explicit binding)

  • The parameter is annotated with @Parameter (parameterized bean handling)

  • The parameter is annotated with @Inject (generic bean injection)

  • The type of the parameter is annotated with a bean scope (such as @Singleton)

Once you have prepared a type-safe configuration it can be injected into your beans like any other bean:

@ConfigurationProperties Dependency Injection
@Singleton
public class Engine {
    private final EngineConfig config;

    public Engine(EngineConfig config) {// (1)
        this.config = config;
    }

    public int getCylinders() {
        return config.getCylinders();
    }

    public String start() {// (2)
        return getConfig().getManufacturer() + " Engine Starting V" + getConfig().getCylinders() +
                " [rodLength=" + getConfig().getCrankShaft().getRodLength().orElse(6.0d) + "]";
    }

    public final EngineConfig getConfig() {
        return config;
    }
}
@ConfigurationProperties Dependency Injection
@Singleton
class Engine {
    private final EngineConfig config

    Engine(EngineConfig config) {// (1)
        this.config = config
    }

    int getCylinders() {
        return config.cylinders
    }

    String start() {// (2)
        return "$config.manufacturer Engine Starting V$config.cylinders [rodLength=${config.crankShaft.rodLength.orElse(6.0d)}]"
    }

    final EngineConfig getConfig() {
        return config
    }
}
@ConfigurationProperties Dependency Injection
@Singleton
class Engine(val config: EngineConfig)// (1)
{
    val cylinders: Int
        get() = config.cylinders

    fun start(): String {// (2)
        return  "${config.manufacturer} Engine Starting V${config.cylinders} [rodLength=${config.crankShaft.getRodLength().orElse(6.0)}]"
    }
}
1 Inject the EngineConfig bean
2 Use the configuration properties

Configuration values can then be supplied when running the application. For example:

Supply Configuration
ApplicationContext applicationContext = ApplicationContext.run(CollectionUtils.mapOf(
        "my.engine.cylinders", "8",
        "my.engine.crank-shaft.rod-length", "7.0"
));

Vehicle vehicle = applicationContext.getBean(Vehicle.class);
System.out.println(vehicle.start());
Supply Configuration
ApplicationContext applicationContext = ApplicationContext.run(
        "my.engine.cylinders": "8",
        "my.engine.crank-shaft.rod-length": "7.0"
)

Vehicle vehicle = applicationContext.getBean(Vehicle)
System.out.println(vehicle.start())
Supply Configuration
val map = mapOf(
        "my.engine.cylinders" to "8",
        "my.engine.crank-shaft.rod-length" to "7.0"
)
val applicationContext = ApplicationContext.run(map)

val vehicle = applicationContext.getBean(Vehicle::class.java)
println(vehicle.start())

The above example prints: "Ford Engine Starting V8 [rodLength=7B.0]"

Using Java Record Classes to define immutable configuration

For Java language applications, it’s also possible to use Java Record Classes for immutable configuration with @ConfigurationProperties. For example:

Java Record Example
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.NonNull;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;

@ConfigurationProperties("vat")
public record ValueAddedTaxConfiguration(
    @NonNull @NotNull BigDecimal percentage) { // (1)
}
1 The percentage field defines a configuration property for "vat"
From a performance perspective Java records are better than interfaces.

Customizing accessors

As already explained in Change accessors style, it is also possible to customize the accessors when creating immutable configuration properties:

import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.annotation.AccessorsStyle;
import io.micronaut.core.bind.annotation.Bindable;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.Optional;

@ConfigurationProperties("my.engine") (1)
@AccessorsStyle(readPrefixes = "read") (2)
public interface EngineConfigAccessors {

    @Bindable(defaultValue = "Ford")
    @NotBlank
    String readManufacturer(); (3)

    @Min(1L)
    int readCylinders(); (3)

    @NotNull
    CrankShaft readCrankShaft(); (3)

    @ConfigurationProperties("crank-shaft")
    @AccessorsStyle(readPrefixes = "read") (4)
    interface CrankShaft {
        Optional<Double> readRodLength(); (5)
    }
}
1 The @ConfigurationProperties annotation takes the configuration prefix and is declared on an interface
2 The @AccessorsStyle annotation defines the readPrefixes as read.
3 The getters are all prefixed with read.
4 Nested immutable configuration can also be annotated with @ConfigurationProperties.
5 The getter is prefixed with read.

4.10 Bootstrap Configuration

Most application configuration is stored in your configuration file (e.g application.yml), environment-specific files like application-{environment}.{extension}, environment and system properties, etc. These configure the application context. But during application startup, before the application context is created, a "bootstrap" context can be created to store configuration necessary to retrieve additional configuration for the main context. Typically, that additional configuration is in some remote source.

The bootstrap context is enabled depending on the following conditions. The conditions are checked in the following order:

  • If The BOOTSTRAP_CONTEXT_PROPERTY system property is set, that value determines if the bootstrap context is enabled.

  • If The application context builder option bootstrapEnvironment is set, that value determines if the bootstrap context is enabled.

  • If a BootstrapPropertySourceLocator bean is present the bootstrap context is enabled. Normally this comes from the micronaut-discovery-client dependency.

Configuration properties that must be present before application context configuration properties are resolved, for example when using distributed configuration, are stored in a bootstrap configuration file. Once it is determined the bootstrap context is enabled (as described above), the bootstrap configuration files are read using the same rules as regular application configuration. See the property source documentation for the details. The only difference is the prefix (bootstrap instead of application).

The file name prefix bootstrap is configurable with a system property micronaut.bootstrap.name.

The bootstrap context configuration is carried over to the main context automatically, so it is not necessary for configuration properties to be duplicated in the main context. In addition, the bootstrap context configuration has a higher precedence than the main context, meaning if a configuration property appears in both contexts, then the value will be taken from the bootstrap context first.

That means if a configuration property is needed in both places, it should go into the bootstrap context configuration.

See the distributed configuration section of the documentation for the list of integrations with common distributed configuration solutions.

Bootstrap Context Beans

In order for a bean to be resolvable in the bootstrap context it must be annotated with @BootstrapContextCompatible. If any given bean is not annotated then it will not be able to be resolved in the bootstrap context. Typically, any bean that is participating in the process of retrieving distributed configuration needs to be annotated.

4.11 JMX Support

Micronaut framework provides basic support for JMX.

For more information, see the documentation for the micronaut-jmx project.

5 Aspect Oriented Programming

Aspect-Oriented Programming (AOP) has historically had many incarnations and some very complicated implementations. Generally AOP can be thought of as a way to define cross-cutting concerns (logging, transactions, tracing, etc.) separate from application code in the form of aspects that define advice.

There are typically two forms of advice:

  • Around Advice - decorates a method or class

  • Introduction Advice - introduces new behaviour to a class.

In modern Java applications, declaring advice typically takes the form of an annotation. The most well-known annotation advice in the Java world is probably @Transactional, which demarcates transaction boundaries in Spring and Grails applications.

The disadvantage of traditional approaches to AOP is the heavy reliance on runtime proxy creation and reflection, which slows application performance, makes debugging harder and increases memory consumption.

Micronaut framework tries to address these concerns by providing a simple compile-time AOP API that does not use reflection.

5.1 Around Advice

The most common type of advice you may want to apply is "Around" advice, which lets you decorate a method’s behaviour.

Writing Around Advice

The first step is to define an annotation that will trigger a MethodInterceptor:

Around Advice Annotation Example
import io.micronaut.aop.Around;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME) // (1)
@Target({TYPE, METHOD}) // (2)
@Around // (3)
public @interface NotNull {
}
Around Advice Annotation Example
import io.micronaut.aop.Around
import java.lang.annotation.*
import static java.lang.annotation.ElementType.*
import static java.lang.annotation.RetentionPolicy.RUNTIME

@Documented
@Retention(RUNTIME) // (1)
@Target([TYPE, METHOD]) // (2)
@Around // (3)
@interface NotNull {
}
Around Advice Annotation Example
import io.micronaut.aop.Around
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.annotation.AnnotationTarget.FILE
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER

@MustBeDocumented
@Retention(RUNTIME) // (1)
@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) // (2)
@Around // (3)
annotation class NotNull
1 The retention policy of the annotation should be RUNTIME
2 Generally you want to be able to apply advice at the class or method level so the target types are TYPE and METHOD
3 The @Around annotation is added to tell the Micronaut framework that the annotation is Around advice

The next step to defining Around advice is to implement a MethodInterceptor. For example the following interceptor disallows parameters with null values:

MethodInterceptor Example
import io.micronaut.aop.InterceptorBean;
import io.micronaut.aop.MethodInterceptor;
import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.type.MutableArgumentValue;

import jakarta.inject.Singleton;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

@Singleton
@InterceptorBean(NotNull.class) // (1)
public class NotNullInterceptor implements MethodInterceptor<Object, Object> { // (2)
    @Nullable
    @Override
    public Object intercept(MethodInvocationContext<Object, Object> context) {
        Optional<Map.Entry<String, MutableArgumentValue<?>>> nullParam = context.getParameters()
            .entrySet()
            .stream()
            .filter(entry -> {
                MutableArgumentValue<?> argumentValue = entry.getValue();
                return Objects.isNull(argumentValue.getValue());
            })
            .findFirst(); // (3)
        if (nullParam.isPresent()) {
            throw new IllegalArgumentException("Null parameter [" + nullParam.get().getKey() + "] not allowed"); // (4)
        }
        return context.proceed(); // (5)
    }
}
MethodInterceptor Example
import io.micronaut.aop.InterceptorBean
import io.micronaut.aop.MethodInterceptor
import io.micronaut.aop.MethodInvocationContext
import io.micronaut.core.annotation.Nullable
import io.micronaut.core.type.MutableArgumentValue

import jakarta.inject.Singleton

@Singleton
@InterceptorBean(NotNull) // (1)
class NotNullInterceptor implements MethodInterceptor<Object, Object> { // (2)
    @Nullable
    @Override
    Object intercept(MethodInvocationContext<Object, Object> context) {
        Optional<Map.Entry<String, MutableArgumentValue<?>>> nullParam = context.parameters
            .entrySet()
            .stream()
            .filter({entry ->
                MutableArgumentValue<?> argumentValue = entry.value
                return Objects.isNull(argumentValue.value)
            })
            .findFirst() // (3)
        if (nullParam.present) {
            throw new IllegalArgumentException("Null parameter [${nullParam.get().key}] not allowed") // (4)
        }
        return context.proceed() // (5)
    }
}
MethodInterceptor Example
import io.micronaut.aop.InterceptorBean
import io.micronaut.aop.MethodInterceptor
import io.micronaut.aop.MethodInvocationContext
import java.util.Objects
import jakarta.inject.Singleton

@Singleton
@InterceptorBean(NotNull::class) // (1)
class NotNullInterceptor : MethodInterceptor<Any, Any> { // (2)
    override fun intercept(context: MethodInvocationContext<Any, Any>): Any? {
        val nullParam = context.parameters
                .entries
                .stream()
                .filter { entry ->
                    val argumentValue = entry.value
                    Objects.isNull(argumentValue.value)
                }
                .findFirst() // (3)
        return if (nullParam.isPresent) {
            throw IllegalArgumentException("Null parameter [${nullParam.get().key}] not allowed") // (4)
        } else {
            context.proceed() // (5)
        }
    }
}
1 The @InterceptorBean annotation is used to indicate what annotation the interceptor is associated with. Note that @InterceptorBean is meta-annotated with a default scope of @Singleton therefore if you want a new interceptor created and associated with each intercepted bean you should annotate the interceptor with @Prototype.
2 An interceptor implements the MethodInterceptor interface.
3 The passed MethodInvocationContext is used to find the first parameter that is null
4 If a null parameter is found an exception is thrown
5 Otherwise proceed() is called to proceed with the method invocation.
Micronaut AOP interceptors use no reflection which improves performance and reducing stack trace sizes, thus improving debugging.

Apply the annotation to target classes to put the new MethodInterceptor to work:

Around Advice Usage Example
import jakarta.inject.Singleton;

@Singleton
public class NotNullExample {

    @NotNull
    void doWork(String taskName) {
        System.out.println("Doing job: " + taskName);
    }
}
Around Advice Usage Example
import jakarta.inject.Singleton

@Singleton
class NotNullExample {

    @NotNull
    void doWork(String taskName) {
        println "Doing job: $taskName"
    }
}
Around Advice Usage Example
import jakarta.inject.Singleton

@Singleton
open class NotNullExample {

    @NotNull
    open fun doWork(taskName: String?) {
        println("Doing job: $taskName")
    }
}

Whenever the type NotNullExample is injected into a class, a compile-time-generated proxy is injected that decorates method calls with the @NotNull advice defined earlier. You can verify that the advice works by writing a test. The following test verifies that the expected exception is thrown when the argument is null:

Around Advice Test
Around Advice Test
void "test not null"() {
    when:
    def applicationContext = ApplicationContext.run()
    def exampleBean = applicationContext.getBean(NotNullExample)

    exampleBean.doWork(null)

    then:
    IllegalArgumentException e = thrown()
    e.message == 'Null parameter [taskName] not allowed'

    cleanup:
    applicationContext.close()
}
Around Advice Test
@Test
fun testNotNull() {
    val applicationContext = ApplicationContext.run()
    val exampleBean = applicationContext.getBean(NotNullExample::class.java)

    val exception = shouldThrow<IllegalArgumentException> {
        exampleBean.doWork(null)
    }
    exception.message shouldBe "Null parameter [taskName] not allowed"
    applicationContext.close()
}
Since Micronaut injection happens at compile time, generally the advice should be packaged in a dependent JAR file that is on the classpath when the above test is compiled. It should not be in the same codebase since you don’t want the test to be compiled before the advice itself is compiled.

Customizing Proxy Generation

The default behaviour of the Around annotation is to generate a proxy at compile time that is a subclass of the proxied class. In other words, in the previous example a compile-time subclass of the NotNullExample class will be produced where proxied methods are decorated with interceptor handling, and the original behaviour is invoked via a call to super.

This behaviour is more efficient as only one instance of the bean is required, however depending on the use case you may wish to alter this behaviour. The @Around annotation supports various attributes that allow you to alter this behaviour, including:

  • proxyTarget (defaults to false) - If set to true, instead of a subclass that calls super, the proxy delegates to the original bean instance

  • hotswap (defaults to false) - Same as proxyTarget=true, but in addition the proxy implements HotSwappableInterceptedProxy which wraps each method call in a ReentrantReadWriteLock and allows swapping the target instance at runtime.

  • lazy (defaults to false) - By default the Micronaut framework eagerly initializes the proxy target when the proxy is created. If set to true the proxy target is instead resolved lazily for each method call.

AOP Advice on @Factory Beans

The semantics of AOP advice when applied to Bean Factories differs from regular beans, with the following rules applying:

  1. AOP advice applied at the class level of a @Factory bean applies the advice to the factory itself and not to any beans defined with the @Bean annotation.

  2. AOP advice applied on a method annotated with a bean scope applies the AOP advice to the bean that the factory produces.

Consider the following two examples:

AOP Advice at the type level of a @Factory
@Timed
@Factory
public class MyFactory {

    @Prototype
    public MyBean myBean() {
        return new MyBean();
    }
}
AOP Advice at the type level of a @Factory
@Timed
@Factory
class MyFactory {

    @Prototype
    MyBean myBean() {
        new MyBean()
    }
}
AOP Advice at the type level of a @Factory
@Timed
@Factory
open class MyFactory {

    @Prototype
    open fun myBean(): MyBean {
        return MyBean()
    }
}

The above example logs the time it takes to create the MyBean bean.

Now consider this example:

AOP Advice at the method level of a @Factory
@Factory
public class MyFactory {

    @Prototype
    @Timed
    public MyBean myBean() {
        return new MyBean();
    }
}
AOP Advice at the method level of a @Factory
@Factory
class MyFactory {

    @Prototype
    @Timed
    MyBean myBean() {
        new MyBean()
    }
}
AOP Advice at the method level of a @Factory
@Factory
open class MyFactory {

    @Prototype
    @Timed
    open fun myBean(): MyBean {
        return MyBean()
    }
}

The above example logs the time it takes to execute the public methods of the MyBean bean, but not the bean creation.

The rationale for this behaviour is that you may at times wish to apply advice to a factory and at other times apply advice to the bean produced by the factory.

Note that there is currently no way to apply advice at the method level to a @Factory bean, and all advice for factories must be applied at the type level. You can control which methods have advice applied by defining methods as non-public which do not have advice applied.

5.2 Introduction Advice

Introduction advice is distinct from Around advice in that it involves providing an implementation instead of decorating.

Examples of introduction advice include GORM and Spring Data which implement persistence logic for you.

Micronaut Client annotation is another example of introduction advice where the Micronaut framework implements HTTP client interfaces for you at compile time.

The way you implement Introduction advice is very similar to how you implement Around advice.

You start by defining an annotation that powers the introduction advice. As an example, say you want to implement advice to return a stubbed value for every method in an interface (a common requirement in testing frameworks). Consider the following @Stub annotation:

Introduction Advice Annotation Example
import io.micronaut.aop.Introduction;
import io.micronaut.context.annotation.Bean;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Introduction // (1)
@Bean // (2)
@Documented
@Retention(RUNTIME)
@Target({TYPE, ANNOTATION_TYPE, METHOD})
public @interface Stub {
    String value() default "";
}
Introduction Advice Annotation Example
import io.micronaut.aop.Introduction
import io.micronaut.context.annotation.Bean

import java.lang.annotation.Documented
import java.lang.annotation.Retention
import java.lang.annotation.Target

import static java.lang.annotation.ElementType.ANNOTATION_TYPE
import static java.lang.annotation.ElementType.METHOD
import static java.lang.annotation.ElementType.TYPE
import static java.lang.annotation.RetentionPolicy.RUNTIME

@Introduction // (1)
@Bean // (2)
@Documented
@Retention(RUNTIME)
@Target([TYPE, ANNOTATION_TYPE, METHOD])
@interface Stub {
    String value() default ""
}
Introduction Advice Annotation Example
import io.micronaut.aop.Introduction
import io.micronaut.context.annotation.Bean
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.annotation.AnnotationTarget.FILE
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER

@Introduction // (1)
@Bean // (2)
@MustBeDocumented
@Retention(RUNTIME)
@Target(CLASS, FILE, ANNOTATION_CLASS, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
annotation class Stub(val value: String = "")
1 The introduction advice is annotated with Introduction
2 The Bean annotation is added so that all types annotated with @Stub become beans

The StubIntroduction class referred to in the previous example must then implement the MethodInterceptor interface, just like around advice.

The following is an example implementation:

StubIntroduction
import io.micronaut.aop.*;
import io.micronaut.core.annotation.Nullable;
import jakarta.inject.Singleton;

@Singleton
@InterceptorBean(Stub.class) // (1)
public class StubIntroduction implements MethodInterceptor<Object, Object> { // (2)

    @Nullable
    @Override
    public Object intercept(MethodInvocationContext<Object, Object> context) {
        return context.getValue( // (3)
                Stub.class,
                context.getReturnType().getType()
        ).orElse(null); // (4)
    }
}
StubIntroduction
import io.micronaut.aop.MethodInterceptor
import io.micronaut.aop.MethodInvocationContext
import io.micronaut.aop.InterceptorBean
import io.micronaut.core.annotation.Nullable
import jakarta.inject.Singleton

@Singleton
@InterceptorBean(Stub) // (1)
class StubIntroduction implements MethodInterceptor<Object,Object> { // (2)

    @Nullable
    @Override
    Object intercept(MethodInvocationContext<Object, Object> context) {
        context.getValue( // (3)
                Stub,
                context.returnType.type
        ).orElse(null) // (4)
    }
}
StubIntroduction
import io.micronaut.aop.*
import jakarta.inject.Singleton

@Singleton
@InterceptorBean(Stub::class) // (1)
class StubIntroduction : MethodInterceptor<Any, Any> { // (2)

    override fun intercept(context: MethodInvocationContext<Any, Any>): Any? {
        return context.getValue<Any>( // (3)
                Stub::class.java,
                context.returnType.type
        ).orElse(null) // (4)
    }
}
1 The InterceptorBean annotation is used to associate the interceptor with the @Stub annotation
2 The class is annotated with @Singleton and implements the MethodInterceptor interface
3 The value of the @Stub annotation is read from the context and an attempt made to convert the value to the return type
4 Otherwise null is returned

To now use this introduction advice in an application, annotate your abstract classes or interfaces with @Stub:

StubExample
@Stub
public interface StubExample {

    @Stub("10")
    int getNumber();

    LocalDateTime getDate();
}
StubExample
@Stub
interface StubExample {

    @Stub("10")
    int getNumber()

    LocalDateTime getDate()
}
StubExample
@Stub
interface StubExample {

    @get:Stub("10")
    val number: Int

    val date: LocalDateTime?
}

All abstract methods delegate to the StubIntroduction class to be implemented.

The following test demonstrates the behaviour or StubIntroduction:

Testing Introduction Advice
StubExample stubExample = applicationContext.getBean(StubExample.class);

assertEquals(10, stubExample.getNumber());
assertNull(stubExample.getDate());
Testing Introduction Advice
when:
def stubExample = applicationContext.getBean(StubExample)

then:
stubExample.number == 10
stubExample.date == null
Testing Introduction Advice
val stubExample = applicationContext.getBean(StubExample::class.java)

stubExample.number.shouldBe(10)
stubExample.date.shouldBe(null)

Note that if the introduction advice cannot implement the method, call the proceed method of the MethodInvocationContext. This lets other introduction advice interceptors implement the method, and an UnsupportedOperationException will be thrown if no advice can implement the method.

In addition, if multiple introduction advice are present you may wish to override the getOrder() method of MethodInterceptor to control the priority of advice.

The following sections cover core advice types provided by Micronaut.

5.3 Method Adapter Advice

There are cases where you want to introduce a new bean based on the presence of an annotation on a method. An example of this is the @EventListener annotation which produces an implementation of ApplicationEventListener for each annotated method that invokes the annotated method.

For example the following snippet runs the logic contained within the method when the ApplicationContext starts up:

import io.micronaut.context.event.StartupEvent;
import io.micronaut.runtime.event.annotation.EventListener;
...

@EventListener
void onStartup(StartupEvent event) {
    // startup logic here
}

The presence of the @EventListener annotation causes the Micronaut framework to create a new class that implements ApplicationEventListener and invokes the onStartup method defined in the bean above.

The actual implementation of the @EventListener is trivial; it simply uses the @Adapter annotation to specify which SAM (single abstract method) type it adapts:

import io.micronaut.aop.Adapter;
import io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.core.annotation.Indexed;

import java.lang.annotation.*;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Adapter(ApplicationEventListener.class) (1)
@Indexed(ApplicationEventListener.class)
@Inherited
public @interface EventListener {
}
1 The @Adapter annotation indicates which SAM type to adapt, in this case ApplicationEventListener.
The Micronaut framework also automatically aligns the generic types for the SAM interface if they are specified.

Using this mechanism you can define custom annotations that use the @Adapter annotation and a SAM interface to automatically implement beans for you at compile time.

5.4 Bean Life Cycle Advice

Sometimes you may need to apply advice to a bean’s lifecycle. There are 3 types of advice that are applicable in this case:

  • Interception of the construction of the bean

  • Interception of the bean’s @PostConstruct invocation

  • Interception of a bean’s @PreDestroy invocation

The Micronaut framework supports these 3 use cases by allowing the definition of additional @InterceptorBinding meta-annotations.

Consider the following annotation definition:

AroundConstruct example
import io.micronaut.aop.*;
import io.micronaut.context.annotation.Prototype;
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@AroundConstruct // (1)
@InterceptorBinding(kind = InterceptorKind.POST_CONSTRUCT) // (2)
@InterceptorBinding(kind = InterceptorKind.PRE_DESTROY) // (3)
@Prototype // (4)
public @interface ProductBean {
}
AroundConstruct example
import io.micronaut.aop.*
import io.micronaut.context.annotation.Prototype
import java.lang.annotation.*

@Retention(RetentionPolicy.RUNTIME)
@AroundConstruct // (1)
@InterceptorBinding(kind = InterceptorKind.POST_CONSTRUCT) // (2)
@InterceptorBinding(kind = InterceptorKind.PRE_DESTROY) // (3)
@Prototype // (4)
@interface ProductBean {
}
AroundConstruct example
import io.micronaut.aop.AroundConstruct
import io.micronaut.aop.InterceptorBinding
import io.micronaut.aop.InterceptorBindingDefinitions
import io.micronaut.aop.InterceptorKind
import io.micronaut.context.annotation.Prototype

@Retention(AnnotationRetention.RUNTIME)
@AroundConstruct // (1)
@InterceptorBindingDefinitions(
    InterceptorBinding(kind = InterceptorKind.POST_CONSTRUCT), // (2)
    InterceptorBinding(kind = InterceptorKind.PRE_DESTROY) // (3)
)
@Prototype // (4)
annotation class ProductBean
1 The @AroundConstruct annotation is added to indicate that interception of the constructor should occur
2 An @InterceptorBinding definition is used to indicate that @PostConstruct interception should occur
3 An @InterceptorBinding definition is used to indicate that @PreDestroy interception should occur
4 The bean is defined as @Prototype so a new instance is required for each injection point

Note that if you do not need @PostConstruct and @PreDestroy interception you can simply remove those bindings.

The @ProductBean annotation can then be used on the target class:

Using an AroundConstruct meta-annotation
import io.micronaut.context.annotation.Parameter;

import jakarta.annotation.PreDestroy;

@ProductBean // (1)
public class Product {
    private final String productName;
    private boolean active = false;

    public Product(@Parameter String productName) { // (2)
        this.productName = productName;
    }

    public String getProductName() {
        return productName;
    }

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }

    @PreDestroy // (3)
    void disable() {
        active = false;
    }
}
Using an AroundConstruct meta-annotation
import io.micronaut.context.annotation.Parameter
import jakarta.annotation.PreDestroy

@ProductBean // (1)
class Product {
    final String productName
    boolean active = false

    Product(@Parameter String productName) { // (2)
        this.productName = productName
    }

    @PreDestroy // (3)
    void disable() {
        active = false
    }
}
Using an AroundConstruct meta-annotation
import io.micronaut.context.annotation.Parameter
import jakarta.annotation.PreDestroy

@ProductBean // (1)
class Product(@param:Parameter val productName: String ) { // (2)

    var active: Boolean = false
    @PreDestroy
    fun disable() { // (3)
        active = false
    }
}
1 The @ProductBean annotation is defined on a class of type Product
2 The @Parameter annotation indicates that this bean requires an argument to complete constructions
3 Any @PreDestroy or @PostConstruct methods are executed last in the interceptor chain

Now you can define ConstructorInterceptor beans for constructor interception and MethodInterceptor beans for @PostConstruct or @PreDestroy interception.

The following factory defines a ConstructorInterceptor that intercepts construction of Product instances and registers them with a hypothetical ProductService validating the product name first:

Defining a constructor interceptor
import io.micronaut.aop.*;
import io.micronaut.context.annotation.Factory;

@Factory
public class ProductInterceptors {
    private final ProductService productService;

    public ProductInterceptors(ProductService productService) {
        this.productService = productService;
    }
}

@InterceptorBean(ProductBean.class)
ConstructorInterceptor<Product> aroundConstruct() { // (1)
    return context -> {
        final Object[] parameterValues = context.getParameterValues(); // (2)
        final Object parameterValue = parameterValues[0];
        if (parameterValue == null || parameterValues[0].toString().isEmpty()) {
            throw new IllegalArgumentException("Invalid product name");
        }
        String productName = parameterValues[0].toString().toUpperCase();
        parameterValues[0] = productName;
        final Product product = context.proceed(); // (3)
        productService.addProduct(product);
        return product;
    };
}
Defining a constructor interceptor
import io.micronaut.aop.*
import io.micronaut.context.annotation.Factory


@Factory
class ProductInterceptors {
    private final ProductService productService

    ProductInterceptors(ProductService productService) {
        this.productService = productService
    }
}

@InterceptorBean(ProductBean.class)
ConstructorInterceptor<Product> aroundConstruct() { // (1)
    return  { context ->
        final Object[] parameterValues = context.parameterValues // (2)
        final Object parameterValue = parameterValues[0]
        if (parameterValue == null || parameterValues[0].toString().isEmpty()) {
            throw new IllegalArgumentException("Invalid product name")
        }
        String productName = parameterValues[0].toString().toUpperCase()
        parameterValues[0] = productName
        final Product product = context.proceed() // (3)
        productService.addProduct(product)
        return product
    }
}
Defining a constructor interceptor
import io.micronaut.aop.*
import io.micronaut.context.annotation.Factory

@Factory
class ProductInterceptors(private val productService: ProductService) {
}

@InterceptorBean(ProductBean::class)
fun aroundConstruct(): ConstructorInterceptor<Product> { // (1)
    return ConstructorInterceptor { context: ConstructorInvocationContext<Product> ->
        val parameterValues = context.parameterValues // (2)
        val parameterValue = parameterValues[0]
        require(!(parameterValue == null || parameterValues[0].toString().isEmpty())) { "Invalid product name" }
        val productName = parameterValues[0].toString().uppercase()
        parameterValues[0] = productName
        val product = context.proceed() // (3)
        productService.addProduct(product)
        product
    }
}
1 A new @InterceptorBean is defined that is a ConstructorInterceptor
2 The constructor parameter values can be retrieved and modified as needed
3 The constructor can be invoked with the proceed() method

Defining MethodInterceptor instances that interceptor the @PostConstruct and @PreDestroy methods is no different from defining interceptors for regular methods. Note however that you can use the passed MethodInvocationContext to identify what kind of interception is occurring and adapt the code accordingly like in the following example:

Defining a constructor interceptor
@InterceptorBean(ProductBean.class) // (1)
MethodInterceptor<Product, Object> aroundInvoke() {
    return context -> {
        final Product product = context.getTarget();
        switch (context.getKind()) {
            case POST_CONSTRUCT: // (2)
                product.setActive(true);
                return context.proceed();
            case PRE_DESTROY: // (3)
                productService.removeProduct(product);
                return context.proceed();
            default:
                return context.proceed();
        }
    };
}
Defining a constructor interceptor
@InterceptorBean(ProductBean.class) // (1)
MethodInterceptor<Product, Object> aroundInvoke() {
    return { context ->
        final Product product = context.getTarget()
        switch (context.kind) {
            case InterceptorKind.POST_CONSTRUCT: // (2)
                product.setActive(true)
                return context.proceed()
            case InterceptorKind.PRE_DESTROY: // (3)
                productService.removeProduct(product)
                return context.proceed()
            default:
                return context.proceed()
        }
    }
}
Defining a constructor interceptor
@InterceptorBean(ProductBean::class)
fun  aroundInvoke(): MethodInterceptor<Product, Any> { // (1)
    return MethodInterceptor { context: MethodInvocationContext<Product, Any> ->
        val product = context.target
        return@MethodInterceptor when (context.kind) {
            InterceptorKind.POST_CONSTRUCT -> { // (2)
                product.active = true
                context.proceed()
            }
            InterceptorKind.PRE_DESTROY -> { // (3)
                productService.removeProduct(product)
                context.proceed()
            }
            else -> context.proceed()
        }
    }
}
1 A new @InterceptorBean is defined that is a MethodInterceptor
2 @PostConstruct interception is handled
3 @PreDestroy interception is handled

5.5 Validation Advice

Validation advice is one of the most common advice types you are likely to want to use in your application.

Validation advice is built on Bean Validation JSR 380, a specification of the Java API for bean validation which ensures that the properties of a bean meet specific criteria, using jakarta.validation annotations such as @NotNull, @Min, and @Max.

The Micronaut framework provides native support for the jakarta.validation annotations with the micronaut-validation dependency:

annotationProcessor("io.micronaut.validation:micronaut-validation-processor")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.validation</groupId>
        <artifactId>micronaut-validation-processor</artifactId>
    </path>
</annotationProcessorPaths>

implementation("io.micronaut.validation:micronaut-validation")
<dependency>
    <groupId>io.micronaut.validation</groupId>
    <artifactId>micronaut-validation</artifactId>
</dependency>

Or full JSR 380 compliance with the micronaut-hibernate-validator dependency:

implementation("io.micronaut:micronaut-hibernate-validator")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-hibernate-validator</artifactId>
</dependency>

See the section on Bean Validation for more information on how to apply validation rules to your bean classes.

5.6 Cache Advice

Like Spring and Grails, the Micronaut framework provides caching annotations in the io.micronaut.cache package.

The CacheManager interface allows different cache implementations to be plugged in as necessary.

The SyncCache interface provides a synchronous API for caching, whilst the AsyncCache API allows non-blocking operation.

Cache Annotations

The following cache annotations are supported:

  • @Cacheable - Indicates a method is cacheable in the specified cache

  • @CachePut - Indicates that the return value of a method invocation should be cached. Unlike @Cacheable the original operation is never skipped.

  • @CacheInvalidate - Indicates the invocation of a method should cause the invalidation of one or more caches.

Using one of these annotations activates the CacheInterceptor, which in the case of @Cacheable caches the return value of the method.

The emitted result is cached if the method return type is a non-blocking type (either CompletableFuture or an instance of Publisher) .

In addition, if the underlying Cache implementation supports non-blocking cache operations, cache values are read without blocking, resulting in non-blocking cache operations.

Configuring Caches

By default, Caffeine is used to create caches from application configuration. For example:

Cache Configuration Example
micronaut.caches.my-cache.maximum-size=20
micronaut:
  caches:
    my-cache:
      maximum-size: 20
[micronaut]
  [micronaut.caches]
    [micronaut.caches.my-cache]
      maximum-size=20
micronaut {
  caches {
    myCache {
      maximumSize = 20
    }
  }
}
{
  micronaut {
    caches {
      my-cache {
        maximum-size = 20
      }
    }
  }
}
{
  "micronaut": {
    "caches": {
      "my-cache": {
        "maximum-size": 20
      }
    }
  }
}

The above example configures a cache called "my-cache" with a maximum size of 20.

Naming Caches

Define names of caches under micronaut.caches in kebab case (lowercase and hyphen separated); if you use camel case, the names are normalized to kebab case. For example myCache becomes my-cache. The kebab-case form must be used when referencing caches in the @Cacheable annotation.

To configure a weigher to be used with the maximumWeight configuration, create a bean that implements io.micronaut.caffeine.cache.Weigher. To associate a given weigher with only a specific cache, annotate the bean with @Named(<cache name>). Weighers without a named qualifier apply to all caches that don’t have a named weigher. If no beans are found, a default implementation is used.

See the configuration reference for all available configuration options.

Dynamic Cache Creation

A DynamicCacheManager bean can be registered for use cases where caches cannot be configured ahead of time. When a cache is attempted to be retrieved that was not predefined, the dynamic cache manager is invoked to create and return a cache.

By default, if there is no other dynamic cache manager defined in the application, the Micronaut framework registers an instance of DefaultDynamicCacheManager that creates Caffeine caches with default values.

Other Cache Implementations

Check the Micronaut Cache project for more information.

5.7 Retry Advice

In distributed systems and microservice environments, failure is something you have to plan for, and it is common to want to attempt to retry an operation if it fails. If first you don’t succeed try again!

With this in mind, the Micronaut framework includes a Retryable annotation.

Retry Dependency

Since Micronaut Framework 4.0 to use the Retry functionality you need to add the following dependency:

implementation("io.micronaut:micronaut-retry")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-retry</artifactId>
</dependency>

Simple Retry

The simplest form of retry is just to add the @Retryable annotation to a type or method. The default behaviour of @Retryable is to retry three times with a linear delay of one second between each retry. (first attempt with 1s delay, second attempt with 2s delay, third attempt with 3s delay).

For example:

Simple Retry Example
@Retryable
public List<Book> listBooks() {
    // ...
Simple Retry Example
@Retryable
List<Book> listBooks() {
    // ...
Simple Retry Example
@Retryable
open fun listBooks(): List<Book> {
    // ...

With the above example if the listBooks() method throws a RuntimeException, it is retried until the maximum number of attempts is reached.

The multiplier value of the @Retryable annotation can be used to configure a multiplier used to calculate the delay between retries, allowing exponential retry support.

To customize retry behaviour, set the attempts and delay members, For example to configure five attempts with a two seconds delay:

Setting Retry Attempts
@Retryable(attempts = "5",
           delay = "2s")
public Book findBook(String title) {
    // ...
Setting Retry Attempts
@Retryable(attempts = "5",
           delay = "2s")
Book findBook(String title) {
    // ...
Setting Retry Attempts
@Retryable(attempts = "5",
           delay = "2s")
open fun findBook(title: String): Book {
    // ...

Notice how both attempts and delay are defined as strings. This is to support configurability through annotation metadata. For example, you can allow the retry policy to be configured using property placeholder resolution:

Setting Retry via Configuration
@Retryable(attempts = "${book.retry.attempts:3}",
           delay = "${book.retry.delay:1s}")
public Book getBook(String title) {
    // ...
Setting Retry via Configuration
@Retryable(attempts = '${book.retry.attempts:3}',
           delay = '${book.retry.delay:1s}')
Book getBook(String title) {
    // ...
Setting Retry via Configuration
@Retryable(attempts = "\${book.retry.attempts:3}",
           delay = "\${book.retry.delay:1s}")
open fun getBook(title: String): Book {
    // ...

With the above in place, if book.retry.attempts is specified in configuration it is bound to the value of the attempts member of the @Retryable annotation via annotation metadata.

Reactive Retry

@Retryable advice can also be applied to methods that return reactive types, such as Publisher (Project Reactor's Flux or RxJava's Flowable). For example:

Applying Retry Policy to Reactive Types
@Retryable
public Publisher<Book> streamBooks() {
    // ...
Applying Retry Policy to Reactive Types
@Retryable
Flux<Book> streamBooks() {
    // ...
Applying Retry Policy to Reactive Types
@Retryable
open fun streamBooks(): Flux<Book> {
    // ...

In this case @Retryable advice applies the retry policy to the reactive type.

Circuit Breaker

Retry is useful in a microservice environment, but in some cases excessive retries can overwhelm the system as clients repeatedly re-attempt failing operations.

The Circuit Breaker pattern is designed to resolve this issue by allowing a certain number of failing requests and then opening a circuit that remains open for a period before allowing additional retry attempts.

The CircuitBreaker annotation is a variation of the @Retryable annotation that supports a reset member which indicates how long the circuit should remain open before it is reset (the default is 20 seconds).

Applying CircuitBreaker Advice
@CircuitBreaker(reset = "30s")
public List<Book> findBooks() {
    // ...
Applying CircuitBreaker Advice
@CircuitBreaker(reset = "30s")
List<Book> findBooks() {
    // ...
Applying CircuitBreaker Advice
@CircuitBreaker(reset = "30s")
open fun findBooks(): List<Book> {
    // ...

The above example retries the findBooks method three times and then opens the circuit for 30 seconds, rethrowing the original exception and preventing potential downstream traffic such as HTTP requests and I/O operations flooding the system.

Factory Bean Retry

When @Retryable is applied to bean factory methods, it behaves as if the annotation was placed on the type being returned. The retry behavior applies when the methods on the returned object are invoked. Note that the bean factory method itself is not retried. If you want the functionality of creating the bean to be retried, it should be delegated to another singleton that has the @Retryable annotation applied.

For example:

@Factory (1)
public class Neo4jDriverFactory {
    ...
    @Retryable(ServiceUnavailableException.class) (2)
    @Bean(preDestroy = "close")
    public Driver buildDriver() {
        ...
    }
}
1 A factory bean is created that defines methods that create beans
2 The @Retryable annotation is used to catch exceptions thrown from methods executed on the Driver.

Retry Events

You can register RetryEventListener instances as beans to listen for RetryEvent events that are published every time an operation is retried.

In addition, you can register event listeners for CircuitOpenEvent to be notified when a circuit breaker circuit is opened, or CircuitClosedEvent for when a circuit is closed.

5.8 Scheduled Tasks

Like Spring and Grails, the Micronaut framework features a Scheduled annotation for scheduling background tasks.

Using the @Scheduled Annotation

The Scheduled annotation can be added to any method of a bean, and you should set one of the fixedRate, fixedDelay, or cron members. Scheduling requires the Micronaut Context dependency:

implementation("io.micronaut:micronaut-context")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-context</artifactId>
</dependency>

micronaut-context is a transitive dependency of micronaut-http. If you use a Micronaut HTTP runtime, your project already includes the Micronaut-context dependency.

Remember that the scope of a bean impacts behaviour. A @Singleton bean shares state (the fields of the instance) each time the scheduled method is executed, while for a @Prototype bean a new instance is created for each execution.

Scheduling at a Fixed Rate

To schedule a task at a fixed rate, use the fixedRate member. For example:

Fixed Rate Example
@Scheduled(fixedRate = "5m")
void everyFiveMinutes() {
    System.out.println("Executing everyFiveMinutes()");
}
Fixed Rate Example
@Scheduled(fixedRate = "5m")
void everyFiveMinutes() {
    println "Executing everyFiveMinutes()"
}
Fixed Rate Example
@Scheduled(fixedRate = "5m")
internal fun everyFiveMinutes() {
    println("Executing everyFiveMinutes()")
}

The task above executes every five minutes.

Scheduling with a Fixed Delay

To schedule a task, so it runs five minutes after the termination of the previous task use the fixedDelay member. For example:

Fixed Delay Example
@Scheduled(fixedDelay = "5m")
void fiveMinutesAfterLastExecution() {
    System.out.println("Executing fiveMinutesAfterLastExecution()");
}
Fixed Delay Example
@Scheduled(fixedDelay = "5m")
void fiveMinutesAfterLastExecution() {
    println "Executing fiveMinutesAfterLastExecution()"
}
Fixed Delay Example
@Scheduled(fixedDelay = "5m")
internal fun fiveMinutesAfterLastExecution() {
    println("Executing fiveMinutesAfterLastExecution()")
}

Scheduling a Cron Task

To schedule a Cron task use the cron member:

Cron Example
@Scheduled(cron = "0 15 10 ? * MON")
void everyMondayAtTenFifteenAm() {
    System.out.println("Executing everyMondayAtTenFifteenAm()");
}
Cron Example
@Scheduled(cron = "0 15 10 ? * MON")
void everyMondayAtTenFifteenAm() {
    println "Executing everyMondayAtTenFifteenAm()"
}
Cron Example
@Scheduled(cron = "0 15 10 ? * MON")
internal fun everyMondayAtTenFifteenAm() {
    println("Executing everyMondayAtTenFifteenAm()")
}

The above example runs the task every Monday morning at 10:15AM in the time zone of the server.

Scheduling with only an Initial Delay

To schedule a task, so it runs once after the server starts, use the initialDelay member:

Initial Delay Example
@Scheduled(initialDelay = "1m")
void onceOneMinuteAfterStartup() {
    System.out.println("Executing onceOneMinuteAfterStartup()");
}
Initial Delay Example
@Scheduled(initialDelay = "1m")
void onceOneMinuteAfterStartup() {
    println "Executing onceOneMinuteAfterStartup()"
}
Initial Delay Example
@Scheduled(initialDelay = "1m")
internal fun onceOneMinuteAfterStartup() {
    println("Executing onceOneMinuteAfterStartup()")
}

The above example only runs once, one minute after the server starts.

Programmatically Scheduling Tasks

To programmatically schedule tasks, use the TaskScheduler bean which can be injected as follows:

@Inject
@Named(TaskExecutors.SCHEDULED)
TaskScheduler taskScheduler;
@Inject
@Named(TaskExecutors.SCHEDULED)
TaskScheduler taskScheduler
@Inject
@Named(TaskExecutors.SCHEDULED)
lateinit var taskScheduler: TaskScheduler

Configuring Scheduled Tasks with Annotation Metadata

To make your application’s tasks configurable, you can use annotation metadata and property placeholder configuration. For example:

Allow tasks to be configured
@Scheduled(fixedRate = "${my.task.rate:5m}",
        initialDelay = "${my.task.delay:1m}")
void configuredTask() {
    System.out.println("Executing configuredTask()");
}
Allow tasks to be configured
@Scheduled(fixedRate = '${my.task.rate:5m}',
        initialDelay = '${my.task.delay:1m}')
void configuredTask() {
    println "Executing configuredTask()"
}
Allow tasks to be configured
@Scheduled(fixedRate = "\${my.task.rate:5m}",
        initialDelay = "\${my.task.delay:1m}")
internal fun configuredTask() {
    println("Executing configuredTask()")
}

The above example allows the task execution frequency to be configured with the property my.task.rate, and the initial delay to be configured with the property my.task.delay.

Configuring the Scheduled Task Thread Pool

Tasks executed by @Scheduled are run by default on a ScheduledExecutorService configured to have twice the number of threads as available processors.

You can configure this thread pool in your configuration file (e.g application.yml):

Configuring Scheduled Task Thread Pool
micronaut.executors.scheduled.type=scheduled
micronaut.executors.scheduled.core-pool-size=30
micronaut:
  executors:
    scheduled:
      type: scheduled
      core-pool-size: 30
[micronaut]
  [micronaut.executors]
    [micronaut.executors.scheduled]
      type="scheduled"
      core-pool-size=30
micronaut {
  executors {
    scheduled {
      type = "scheduled"
      corePoolSize = 30
    }
  }
}
{
  micronaut {
    executors {
      scheduled {
        type = "scheduled"
        core-pool-size = 30
      }
    }
  }
}
{
  "micronaut": {
    "executors": {
      "scheduled": {
        "type": "scheduled",
        "core-pool-size": 30
      }
    }
  }
}
🔗
Table 1. Configuration Properties for UserExecutorConfiguration
Property Type Description

micronaut.executors.*.n-threads

java.lang.Integer

number of threads

micronaut.executors.*.type

ExecutorType

the type

micronaut.executors.*.parallelism

java.lang.Integer

the parallelism

micronaut.executors.*.core-pool-size

java.lang.Integer

the core pool size

micronaut.executors.*.virtual

java.lang.Boolean

whether to use virtual threads

micronaut.executors.*.thread-factory-class

java.lang.Class

the thread factory class

micronaut.executors.*.name

java.lang.String

Sets the executor name.

micronaut.executors.*.number-of-threads

java.lang.Integer

Sets the number of threads for FIXED. Default value (2 * Number of processors available to the Java virtual machine).

Handling Exceptions

By default, the Micronaut framework includes a DefaultTaskExceptionHandler bean that implements the TaskExceptionHandler interface and simply logs the exception if an error occurs invoking a scheduled task.

If you have custom requirements you can replace this bean with your own implementation (for example to send an email or shutdown the context to fail fast). To do so, write your own TaskExceptionHandler and annotate it with @Replaces(DefaultTaskExceptionHandler.class).

5.9 Bridging Spring AOP

Although the Micronaut framework’s design is based on a compile-time approach and does not rely on Spring dependency injection, there is still a lot of value in the Spring ecosystem that does not depend directly on the Spring container.

You may wish to use existing Spring projects within the Micronaut framework and configure beans to be used within the Micronaut framework.

You may also wish to leverage existing AOP advice from Spring. One example of this is Spring’s support for declarative transactions with @Transactional.

The Micronaut framework provides support for Spring-based transaction management without requiring Spring itself. Simply add the spring module to your application dependencies:

implementation("io.micronaut.spring:micronaut-spring")
<dependency>
    <groupId>io.micronaut.spring</groupId>
    <artifactId>micronaut-spring</artifactId>
</dependency>

This also requires adding the spring-annotation module dependency as an annotation processor:

annotationProcessor("io.micronaut.spring:micronaut-spring-annotation")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.spring</groupId>
        <artifactId>micronaut-spring-annotation</artifactId>
    </path>
</annotationProcessorPaths>

If you use Micronaut’s Hibernate support you already get this dependency and a HibernateTransactionManager is configured for you.

This is done by intercepting method calls annotated with Spring’s @Transactional with TransactionInterceptor.

The benefit here is you can use Micronaut’s compile-time, reflection-free AOP to declare programmatic Spring transactions. For example:

Using @Transactional
import org.springframework.transaction.annotation.Transactional;
...

@Transactional
public Book saveBook(String title) {
    ...
}

6 The HTTP Server

Using the CLI

If you create your project using the Micronaut CLI create-app command, the http-server dependency is included by default.

Micronaut framework includes both non-blocking HTTP server and client APIs based on Netty.

The design of the HTTP server in the Micronaut framework is optimized for interchanging messages between Microservices, typically in JSON, and is not intended as a full server-side MVC framework. For example, there is currently no support for server-side views or features typical of a traditional server-side MVC framework.

The goal of the HTTP server is to make it as easy as possible to expose APIs to be consumed by HTTP clients, regardless of the language they are written in. To use the HTTP server you need the http-server-netty dependency in your build:

implementation("io.micronaut:micronaut-http-server-netty")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-http-server-netty</artifactId>
</dependency>

A "Hello World" server application can be seen below:

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/hello") // (1)
public class HelloController {

    @Get(produces = MediaType.TEXT_PLAIN) // (2)
    public String index() {
        return "Hello World"; // (3)
    }
}
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller('/hello') // (1)
class HelloController {

    @Get(produces = MediaType.TEXT_PLAIN) // (2)
    String index() {
        'Hello World' // (3)
    }
}
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/hello") // (1)
class HelloController {

    @Get(produces = [MediaType.TEXT_PLAIN]) // (2)
    fun index(): String {
        return "Hello World" // (3)
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /hello
2 The method responds to a GET requests to /hello and returns a response with a text/plain content type
3 By defining a method named index, by convention the method is exposed via the /hello URI

6.1 Running the Embedded Server

To run the server, create an Application class with a static void main method, for example:

Micronaut Application Class
import io.micronaut.runtime.Micronaut;

public class Application {

    public static void main(String[] args) {
        Micronaut.run(Application.class);
    }
}
Micronaut Application Class
import io.micronaut.runtime.Micronaut

class Application {

    static void main(String... args) {
        Micronaut.run Application
    }
}
Micronaut Application Class
import io.micronaut.runtime.Micronaut

object Application {

    @JvmStatic
    fun main(args: Array<String>) {
        Micronaut.run(Application.javaClass)
    }
}

To run the application from a unit test, use the EmbeddedServer interface:

Micronaut Test Case
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.runtime.server.EmbeddedServer;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import jakarta.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest
public class HelloControllerSpec {

    @Inject
    EmbeddedServer server; // (1)

    @Inject
    @Client("/")
    HttpClient client; // (2)

    @Test
    void testHelloWorldResponse() {
        String response = client.toBlocking() // (3)
                .retrieve(HttpRequest.GET("/hello"));
        assertEquals("Hello World", response); // (4)
    }
}
Micronaut Test Case
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification

import jakarta.inject.Inject

@MicronautTest
class HelloControllerSpec extends Specification {

    @Inject
    EmbeddedServer embeddedServer // (1)

    @Inject
    @Client("/")
    HttpClient client // (2)

    void "test hello world response"() {
        expect:
            client.toBlocking() // (3)
                    .retrieve(HttpRequest.GET('/hello')) == "Hello World" // (4)
    }
}
Micronaut Test Case
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import jakarta.inject.Inject

@MicronautTest
class HelloControllerSpec {

    @Inject
    lateinit var server: EmbeddedServer // (1)

    @Inject
    @field:Client("/")
    lateinit var client: HttpClient // (2)

    @Test
    fun testHelloWorldResponse() {
        val rsp: String = client.toBlocking() // (3)
                .retrieve("/hello")
        assertEquals("Hello World", rsp) // (4)
    }
}
1 The EmbeddedServer is run and the Spock @AutoCleanup annotation ensures the server is stopped after the specification completes.
2 The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
3 The test uses the Micronaut HTTP client to make the call
4 The retrieve method returns the response of the controller as a String
Without explicit port configuration, the port will be 8080, unless the application is run under the test environment where the port is random. When the application context starts from the context of a test class, the test environment is added automatically.

6.2 Running Server on a Specific Port

By default, the server runs on port 8080. However, you can set the server to run on a specific port:

micronaut.server.port=8086
micronaut:
  server:
    port: 8086
[micronaut]
  [micronaut.server]
    port=8086
micronaut {
  server {
    port = 8086
  }
}
{
  micronaut {
    server {
      port = 8086
    }
  }
}
{
  "micronaut": {
    "server": {
      "port": 8086
    }
  }
}
This is also configurable from an environment variable, e.g. MICRONAUT_SERVER_PORT=8086

To run on a random port:

micronaut.server.port=-1
micronaut:
  server:
    port: -1
[micronaut]
  [micronaut.server]
    port=-1
micronaut {
  server {
    port = -1
  }
}
{
  micronaut {
    server {
      port = -1
    }
  }
}
{
  "micronaut": {
    "server": {
      "port": -1
    }
  }
}
Setting an explicit port may cause tests to fail if multiple servers start simultaneously on the same port. To prevent that, specify a random port in the test environment configuration.

6.3 HTTP Routing

The @Controller annotation used in the previous section is one of several annotations that allow you to control the construction of HTTP routes.

URI Paths

The value of the @Controller annotation is an RFC-6570 URI template, so you can embed URI variables within the path using the syntax defined by the URI template specification.

Many other frameworks, including Spring, implement the URI template specification

The actual implementation is handled by the UriMatchTemplate class, which extends UriTemplate.

You can use this class in your applications to build URIs, for example:

Using a UriTemplate
UriMatchTemplate template = UriMatchTemplate.of("/hello/{name}");

assertTrue(template.match("/hello/John").isPresent()); // (1)
assertEquals("/hello/John", template.expand( // (2)
        Collections.singletonMap("name", "John")
));
Using a UriTemplate
given:
UriMatchTemplate template = UriMatchTemplate.of("/hello/{name}")

expect:
template.match("/hello/John").isPresent() // (1)
template.expand(["name": "John"]) == "/hello/John" // (2)
Using a UriTemplate
val template = UriMatchTemplate.of("/hello/{name}")

template.match("/hello/John").isPresent.shouldBeTrue() // (1)
template.expand(mapOf("name" to "John")) shouldBe "/hello/John"  // (2)
1 Use the match method to match a path
2 Use the expand method to expand a template into a URI

You can use UriTemplate to build paths to include in your responses.

URI Path Variables

URI variables can be referenced via method arguments. When the path variable matches the method argument name, they are bound together automatically. If you want to use different names or specify a default value for a missing URI Variable, the PathVariable annotation can be used. The following example illustrates these options:

URI Variables Example
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;

@Controller("/issues") // (1)
public class IssuesController {

    @Get("/{number}") // (2)
    public String issue(Integer number) { // (3)
        return "Issue # " + number + "!"; // (4)
    }

    @Get("/issue/{number}")
    public String issueFromId(@PathVariable("number") Integer id) { // (5)
        return "Issue # " + id + "!";
    }

}
URI Variables Example
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable

@Controller("/issues") // (1)
class IssuesController {

    @Get("/{number}") // (2)
    String issue(Integer number) { // (3)
        "Issue # " + number + "!" // (4)
    }

    @Get("/issue/{number}")
    String issueFromId(@PathVariable("number") Integer id) { // (5)
        "Issue # " + id + "!"
    }

}
URI Variables Example
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable

@Controller("/issues") // (1)
class IssuesController {

    @Get("/{number}") // (2)
    fun issue(number: Int): String { // (3)
        return "Issue # $number!" // (4)
    }

    @Get("/issue/{number}")
    fun issueById(@PathVariable("number") id: Int): String {
        return "Issue # $id!" // (5)
    }

}
1 The @Controller annotation is specified with a base URI of /issues
2 The Get annotation maps the method to an HTTP GET with a URI variable embedded in the URI named number
3 The method argument number is bound automatically to the path variable {number} because the names match
4 The value of the URI variable is referenced in the implementation
5 The method argument requires the PathVariable annotation when method argument and path variable names don’t match

The Micronaut framework maps the URI /issues/{number} for the above controller. We can assert this is the case by writing unit tests:

Testing URI Variables
import io.micronaut.context.ApplicationContext;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

class IssuesControllerTest {

    private static EmbeddedServer server;
    private static HttpClient client;

    @BeforeAll // (1)
    static void setupServer() {
        server = ApplicationContext.run(EmbeddedServer.class);
        client = server
                    .getApplicationContext()
                    .createBean(HttpClient.class, server.getURL());
    }

    @AfterAll // (2)
    static void stopServer() {
        if (server != null) {
            server.stop();
        }
        if (client != null) {
            client.stop();
        }
    }

    @Test
    void testIssue() {
        String body = client.toBlocking().retrieve("/issues/12"); // (3)

        assertNotNull(body);
        assertEquals("Issue # 12!", body); // (4)
    }

    @Test
    void testIssueFromId() {
        String body = client.toBlocking().retrieve("/issues/issue/13");

        assertNotNull(body);
        assertEquals("Issue # 13!", body); // (5)
    }

    @Test
    void testShowWithInvalidInteger() {
        HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () ->
                client.toBlocking().exchange("/issues/hello"));

        assertEquals(400, e.getStatus().getCode()); // (6)
    }

    @Test
    void testIssueWithoutNumber() {
        HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () ->
                client.toBlocking().exchange("/issues/"));

        assertEquals(404, e.getStatus().getCode()); // (7)
    }

}
Testing URI Variables
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification


class IssuesControllerTest extends Specification {

    @Shared
    @AutoCleanup // (2)
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) // (1)

    @Shared
    @AutoCleanup // (2)
    HttpClient client = HttpClient.create(embeddedServer.URL) // (1)

    void "test issue"() {
        when:
        String body = client.toBlocking().retrieve("/issues/12") // (3)

        then:
        body != null
        body == "Issue # 12!" // (4)
    }

    void "test issue from id"() {
        when:
        String body = client.toBlocking().retrieve("/issues/issue/13")

        then:
        body != null
        body == "Issue # 13!" // (5)
    }

    void "/issues/{number} with an invalid Integer number responds 400"() {
        when:
        client.toBlocking().exchange("/issues/hello")

        then:
        def e = thrown(HttpClientResponseException)
        e.status.code == 400 // (6)
    }

    void "/issues/{number} without number responds 404"() {
        when:
        client.toBlocking().exchange("/issues/")

        then:
        def e = thrown(HttpClientResponseException)
        e.status.code == 404 // (7)
    }

}
Testing URI Variables
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.micronaut.context.ApplicationContext
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer

class IssuesControllerTest: StringSpec() {

    val embeddedServer = autoClose( // (2)
        ApplicationContext.run(EmbeddedServer::class.java) // (1)
    )

    val client = autoClose( // (2)
        embeddedServer.applicationContext.createBean(
            HttpClient::class.java,
            embeddedServer.url) // (1)
    )

    init {
        "test issue" {
            val body = client.toBlocking().retrieve("/issues/12") // (3)

            body shouldNotBe null
            body shouldBe "Issue # 12!" // (4)
        }

        "test issue from id" {
            val body = client.toBlocking().retrieve("/issues/issue/13")

            body shouldNotBe null
            body shouldBe "Issue # 13!" // (5)
        }

        "test issue with invalid integer" {
            val e = shouldThrow<HttpClientResponseException> {
                client.toBlocking().exchange<Any>("/issues/hello")
            }

            e.status.code shouldBe 400 // (6)
        }

        "test issue without number" {
            val e = shouldThrow<HttpClientResponseException> {
                client.toBlocking().exchange<Any>("/issues/")
            }

            e.status.code shouldBe 404 // (7)
        }

    }
}
1 The embedded server and HTTP client are started
2 The server and client are cleaned up after the tests finish
3 The tests send a request to the URI /issues/12
4 And then asserts the response is "Issue # 12"
5 Another test using the end point defined with @PathVariable asserts the response is "Issue # 13"
6 Another test asserts a 400 response is returned when an invalid number is sent in the URL
7 Another test asserts a 404 response is returned when no number is provided in the URL. The variable being present is required for the route to be executed.

Note that the URI template in the previous example requires that the number variable is specified. You can specify optional URI templates with the syntax: /issues{/number} and by annotating the number parameter with @Nullable. Alternatively, you can use the defaultValue element of the PathVariable annotation to specify a default value when the URI variable is missing. For example:

@Get("/default{/number}") // (1)
public String issueFromIdOrDefault(@PathVariable(defaultValue = "0") Integer number) { // (2)
    return "Issue # " + number + "!";
}
@Get("/default{/number}") // (1)
String issueFromIdOrDefault(@PathVariable(defaultValue = "0") Integer number) { // (2)
    "Issue # " + number + "!"
}
@Get("/default{/number}") // (1)
fun issueFromIdOrDefault(@PathVariable(defaultValue = "0") number: Int): String { // (2)
    return "Issue # $number!"
}
1 The forward slash inside the braces designates number as an optional URI variable
2 The defaultValue attribute specifies the default value for number when the URI variable is missing
@Test
void testDefaultIssue() {
    String body = client.toBlocking().retrieve("/issues/default");

    assertNotNull(body);
    assertEquals("Issue # 0!", body); // (1)
}

@Test
void testNotDefaultIssue() {
    String body = client.toBlocking().retrieve("/issues/default/1");

    assertNotNull(body);
    assertEquals("Issue # 1!", body); // (2)
}
void "test default issue"() {
    when:
    String body = client.toBlocking().retrieve("/issues/default")

    then:
    body != null
    body == "Issue # 0!" // (1)
}

void "test not default issue"() {
    when:
    String body = client.toBlocking().retrieve("/issues/default/1")

    then:
    body != null
    body == "Issue # 1!" // (2)
}
"test issue from id" {
    val body = client.toBlocking().retrieve("/issues/default")

    body shouldNotBe null
    body shouldBe "Issue # 0!" // (1)
}

"test issue from id" {
    val body = client.toBlocking().retrieve("/issues/default/1")

    body shouldNotBe null
    body shouldBe "Issue # 1!" // (2)
}
1 This test illustrates the substitution of a default `PathVariable' value when the URI variable is missing
2 And another test to illustrate when the optional URI variable is provided

The following table provides examples of URI templates and what they match:

Table 1. URI Template Matching
Template Description Matching URI

/books/{id}

Simple match

/books/1

/books/{id:2}

A variable of two characters max

/books/10

/books{/id}

An optional URI variable

/books/10 or /books

/book{/id:[a-zA-Z]+}

An optional URI variable with regex

/books/foo

/books{?max,offset}

Optional query parameters

/books?max=10&offset=10

/books{/path:.*}{.ext}

Regex path match with extension

/books/foo/bar.xml

URI Reserved Character Matching

By default, URI variables as defined by the RFC-6570 URI template spec cannot include reserved characters such as /, ? etc.

This can be problematic if you wish to match or expand entire paths. As per section 3.2.3 of the specification, you can use reserved expansion or matching using the + operator.

For example the URI /books/{path}` matches both `/books/foo` and `/books/foo/bar` since the ` indicates that the variable path should include reserved characters (in this case /).

Routing Annotations

The previous example uses the @Get annotation to add a method that accepts HTTP GET requests. The following table summarizes the available annotations and how they map to HTTP methods:

Table 2. HTTP Routing Annotations
Annotation HTTP Method

@Delete

DELETE

@Get

GET

@Head

HEAD

@Options

OPTIONS

@Patch

PATCH

@Put

PUT

@Post

POST

@Trace

TRACE

All the method annotations default to /.

@Options

CORS support handles OPTIONS preflight requests. However, if you want to dispatch OPTIONS requests without an Origin HTTP Header, you can enable it via:

micronaut.server.dispatch-options-requests=true
micronaut:
  server:
    dispatch-options-requests: true
[micronaut]
  [micronaut.server]
    dispatch-options-requests=true
micronaut {
  server {
    dispatchOptionsRequests = true
  }
}
{
  micronaut {
    server {
      dispatch-options-requests = true
    }
  }
}
{
  "micronaut": {
    "server": {
      "dispatch-options-requests": true
    }
  }
}

Multiple URIs

Each of the routing annotations supports multiple URI templates. For each template, a route is created. This feature is useful for example to change the path of the API and leave the existing path as is for backwards compatibility. For example:

Multiple URIs
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/hello")
public class BackwardCompatibleController {

    @Get(uris = {"/{name}", "/person/{name}"}) // (1)
    public String hello(String name) { // (2)
        return "Hello, " + name;
    }
}
Multiple URIs
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/hello")
class BackwardCompatibleController {

    @Get(uris = ["/{name}", "/person/{name}"]) // (1)
    String hello(String name) { // (2)
        "Hello, $name"
    }
}
Multiple URIs
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/hello")
class BackwardCompatibleController {

    @Get(uris = ["/{name}", "/person/{name}"]) // (1)
    fun hello(name: String): String { // (2)
        return "Hello, $name"
    }
}
1 Specify multiple templates
2 Bind to the template arguments as normal
Route validation is more complicated with multiple templates. If a variable that would normally be required does not exist in all templates, that variable is considered optional since it may not exist for every execution of the method.

Building Routes Programmatically

If you prefer to not use annotations and instead declare all routes in code then never fear, the Micronaut framework has a flexible RouteBuilder API that makes it a breeze to define routes programmatically.

To start, subclass DefaultRouteBuilder and inject the controller to route to into the method, and define your routes:

URI Variables Example
import io.micronaut.context.ExecutionHandleLocator;
import io.micronaut.web.router.DefaultRouteBuilder;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

@Singleton
public class MyRoutes extends DefaultRouteBuilder { // (1)

    public MyRoutes(ExecutionHandleLocator executionHandleLocator,
                    UriNamingStrategy uriNamingStrategy) {
        super(executionHandleLocator, uriNamingStrategy);
    }

    @Inject
    void issuesRoutes(IssuesController issuesController) { // (2)
        GET("/issues/show/{number}", issuesController, "issue", Integer.class); // (3)
    }
}
URI Variables Example
import io.micronaut.context.ExecutionHandleLocator
import io.micronaut.core.convert.ConversionService
import io.micronaut.web.router.GroovyRouteBuilder

import jakarta.inject.Inject
import jakarta.inject.Singleton

@Singleton
class MyRoutes extends GroovyRouteBuilder { // (1)

    MyRoutes(ExecutionHandleLocator executionHandleLocator,
             UriNamingStrategy uriNamingStrategy,
             ConversionService conversionService) {
        super(executionHandleLocator, uriNamingStrategy, conversionService)
    }

    @Inject
    void issuesRoutes(IssuesController issuesController) { // (2)
        GET("/issues/show/{number}", issuesController.&issue) // (3)
    }
}
URI Variables Example
import io.micronaut.context.ExecutionHandleLocator
import io.micronaut.web.router.DefaultRouteBuilder
import io.micronaut.web.router.RouteBuilder
import jakarta.inject.Inject
import jakarta.inject.Singleton

@Singleton
class MyRoutes(executionHandleLocator: ExecutionHandleLocator,
               uriNamingStrategy: RouteBuilder.UriNamingStrategy) :
        DefaultRouteBuilder(executionHandleLocator, uriNamingStrategy) { // (1)

    @Inject
    fun issuesRoutes(issuesController: IssuesController) { // (2)
        GET("/issues/show/{number}", issuesController, "issue", Int::class.java) // (3)
    }
}
1 Route definitions should subclass DefaultRouteBuilder
2 Use @Inject to inject a method with the controller to route to
3 Use methods such as RouteBuilder::GET(String,Class,String,Class…​) to route to controller methods. Note that even though the issues controller is used, the route has no knowledge of its @Controller annotation and thus the full path must be specified.
Unfortunately due to type erasure, a Java method lambda reference cannot be used with the API. For Groovy there is a GroovyRouteBuilder class which can be subclassed that allows passing Groovy method references.

Route Compile-Time Validation

The Micronaut framework supports validating route arguments at compile time with the validation library. To get started, add the micronaut-http-validation dependency to your build:

annotationProcessor("io.micronaut:micronaut-http-validation")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-http-validation</artifactId>
    </path>
</annotationProcessorPaths>

With the correct dependency on your classpath, route arguments will automatically be checked at compile time. Compilation will fail if any of the following conditions are met:

  • The URI template contains a variable that is optional, but the method parameter is not annotated with @Nullable or is an java.util.Optional.

An optional variable is one that allows the route to match a URI even if the value is not present. For example /foo{/bar} matches requests to /foo and /foo/abc. The non-optional variant would be /foo/{bar}. See the URI Path Variables section for more information.

  • The URI template contains a variable that is missing from the method arguments.

To disable route compile-time validation, set the system property -Dmicronaut.route.validation=false. For Java and Kotlin users using Gradle, the same effect can be achieved by removing the micronaut-http-validation dependency from the annotationProcessor/kapt scope.

Routing non-standard HTTP methods

The @CustomHttpMethod annotation supports non-standard HTTP methods for a client or server. Specifications like RFC-4918 Webdav require additional methods like REPORT or LOCK for example.

RoutingExample
@CustomHttpMethod(method = "LOCK", value = "/{name}")
String lock(String name)

The annotation can be used anywhere the standard method annotations can be used, including controllers and declarative HTTP clients.

RouteMatch

The RouteMatch API provides information about an executable Route.

Given a request you can retrieve a RouteMatch with:

String index(HttpRequest<?> request) {
    RouteMatch<?> routeMatch = request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class)
            .orElse(null);
String index(HttpRequest<?> request) {
    RouteMatch<?> routeMatch = request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch.class)
            .orElse(null)
fun index(request: HttpRequest<*>): String? {
    val routeMatch = request.getAttribute(HttpAttributes.ROUTE_MATCH, RouteMatch::class.java)
        .orElse(null)

6.4 Simple Request Binding

The examples in the previous section demonstrate how the Micronaut framework lets you bind method parameters from URI path variables. This section shows how to bind arguments from other parts of the request.

Binding Annotations

All binding annotations support customization of the name of the variable being bound from with their name member.

The following table summarizes the annotations and their purpose, and provides examples:

Table 1. Parameter Binding Annotations
Annotation Description Example

@Body

Binds from the body of the request

@Body String body

@CookieValue

Binds a parameter from a cookie

@CookieValue String myCookie

@Header

Binds a parameter from an HTTP header

@Header String requestId

@QueryValue

Binds from a request query parameter

@QueryValue String myParam

@Part

Binds from a part of a multipart request

@Part CompletedFileUpload file

@RequestAttribute

Binds from an attribute of the request. Attributes are typically created in filters

@RequestAttribute String myAttribute

@PathVariable

Binds from the path of the request

@PathVariable String id

@RequestBean

Binds any Bindable value to single Bean object

@RequestBean MyBean bean

The method parameter name is used when a value is not specified in a binding annotation. In other words the following two methods are equivalent and both bind from a cookie named myCookie:

@Get("/cookieName")
public String cookieName(@CookieValue("myCookie") String myCookie) {
    // ...
}

@Get("/cookieInferred")
public String cookieInferred(@CookieValue String myCookie) {
    // ...
}
@Get("/cookieName")
String cookieName(@CookieValue("myCookie") String myCookie) {
    // ...
}

@Get("/cookieInferred")
String cookieInferred(@CookieValue String myCookie) {
    // ...
}
@Get("/cookieName")
fun cookieName(@CookieValue("myCookie") myCookie: String): String {
    // ...
}

@Get("/cookieInferred")
fun cookieInferred(@CookieValue myCookie: String): String {
    // ...
}

Because hyphens are not allowed in variable names, it may be necessary to set the name in the annotation. The following definitions are equivalent:

@Get("/headerName")
public String headerName(@Header("Content-Type") String contentType) {
    // ...
}

@Get("/headerInferred")
public String headerInferred(@Header String contentType) {
    // ...
}
@Get("/headerName")
String headerName(@Header("Content-Type") String contentType) {
    // ...
}

@Get("/headerInferred")
String headerInferred(@Header String contentType) {
    // ...
}
@Get("/headerName")
fun headerName(@Header("Content-Type") contentType: String): String {
    // ...
}

@Get("/headerInferred")
fun headerInferred(@Header contentType: String): String {
    // ...
}

Stream Support

The Micronaut framework also supports binding the body to an InputStream. If the method is reading the stream, the method execution must be offloaded to another thread pool to avoid blocking the event loop.

Performing Blocking I/O With InputStream
@Post(value = "/read", processes = MediaType.TEXT_PLAIN)
@ExecuteOn(TaskExecutors.IO) // (1)
String read(@Body InputStream inputStream) throws IOException { // (2)
    return IOUtils.readText(new BufferedReader(new InputStreamReader(inputStream))); // (3)
}
Performing Blocking I/O With InputStream
@Post(value = "/read", processes = MediaType.TEXT_PLAIN)
@ExecuteOn(TaskExecutors.IO) // (1)
String read(@Body InputStream inputStream) throws IOException { // (2)
    IOUtils.readText(new BufferedReader(new InputStreamReader(inputStream))) // (3)
}
Performing Blocking I/O With InputStream
@Post(value = "/read", processes = [MediaType.TEXT_PLAIN])
@ExecuteOn(TaskExecutors.IO) // (1)
fun read(@Body inputStream: InputStream): String { // (2)
    return IOUtils.readText(BufferedReader(InputStreamReader(inputStream))) // (3)
}
1 The controller method is executed on the IO thread pool
2 The body is passed to the method as an input stream
3 The stream is read

Binding from Multiple Query values

Instead of binding from a single section of the request, it may be desirable to bind all query values for example to a POJO. This can be achieved by using the exploded operator (?pojo*) in the URI template. For example:

Binding Request parameters to POJO
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.core.annotation.Nullable;

import jakarta.validation.Valid;

@Controller("/api")
public class BookmarkController {

    @Get("/bookmarks/list{?paginationCommand*}")
    public HttpStatus list(@Valid @Nullable PaginationCommand paginationCommand) {
        return HttpStatus.OK;
    }
}
Binding Request parameters to POJO
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

import jakarta.annotation.Nullable
import jakarta.validation.Valid

@Controller("/api")
class BookmarkController {

    @Get("/bookmarks/list{?paginationCommand*}")
    HttpStatus list(@Valid @Nullable PaginationCommand paginationCommand) {
        HttpStatus.OK
    }
}
Binding Request parameters to POJO
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import jakarta.validation.Valid

@Controller("/api")
open class BookmarkController {

    @Get("/bookmarks/list{?paginationCommand*}")
    open fun list(@Valid paginationCommand: PaginationCommand): HttpStatus {
        return HttpStatus.OK
    }
}

Binding from Multiple Bindable values

Instead of binding just query values, it is also possible to bind any Bindable value to a POJO (e.g. to bind HttpRequest, @PathVariable, @QueryValue and @Header to a single POJO). This can be achieved with the @RequestBean annotation and a custom Bean class with fields with Bindable annotations, or fields that can be bound by type (e.g. HttpRequest, BasicAuth, Authentication, etc).

For example:

Binding Bindable values to POJO
@Controller("/api")
public class MovieTicketController {

    // You can also omit query parameters like:
    // @Get("/movie/ticket/{movieId}
    @Get("/movie/ticket/{movieId}{?minPrice,maxPrice}")
    public HttpStatus list(@Valid @RequestBean MovieTicketBean bean) {
        return HttpStatus.OK;
    }
}
Binding Bindable values to POJO
@Controller("/api")
class MovieTicketController {

    // You can also omit query parameters like:
    // @Get("/movie/ticket/{movieId}
    @Get("/movie/ticket/{movieId}{?minPrice,maxPrice}")
    HttpStatus list(@Valid @RequestBean MovieTicketBean bean) {
        HttpStatus.OK
    }
}
Binding Bindable values to POJO
@Controller("/api")
open class MovieTicketController {

    // You can also omit query parameters like:
    // @Get("/movie/ticket/{movieId}
    @Get("/movie/ticket/{movieId}{?minPrice,maxPrice}")
    open fun list(@Valid @RequestBean bean: MovieTicketBean): HttpStatus {
        return HttpStatus.OK
    }

}

which uses this bean class:

Bean definition
@Introspected
public class MovieTicketBean {

    private HttpRequest<?> httpRequest;

    @PathVariable
    private String movieId;

    @Nullable
    @QueryValue
    @PositiveOrZero
    private Double minPrice;

    @Nullable
    @QueryValue
    @PositiveOrZero
    private Double maxPrice;

    public MovieTicketBean(HttpRequest<?> httpRequest,
                           String movieId,
                           Double minPrice,
                           Double maxPrice) {
        this.httpRequest = httpRequest;
        this.movieId = movieId;
        this.minPrice = minPrice;
        this.maxPrice = maxPrice;
    }

    public HttpRequest<?> getHttpRequest() {
        return httpRequest;
    }

    public String getMovieId() {
        return movieId;
    }

    @Nullable
    public Double getMaxPrice() {
        return maxPrice;
    }

    @Nullable
    public Double getMinPrice() {
        return minPrice;
    }
}
Bean definition
@Introspected
class MovieTicketBean {

    private HttpRequest<?> httpRequest

    @PathVariable
    String movieId

    @Nullable
    @QueryValue
    @PositiveOrZero
    Double minPrice

    @Nullable
    @QueryValue
    @PositiveOrZero
    Double maxPrice
}
Bean definition
@Introspected
data class MovieTicketBean(
    val httpRequest: HttpRequest<Any>,
    @field:PathVariable val movieId: String,
    @field:QueryValue @field:PositiveOrZero @field:Nullable val minPrice: Double,
    @field:QueryValue @field:PositiveOrZero @field:Nullable val maxPrice: Double
)

The bean class has to be introspected with @Introspected. It can be one of:

  1. Mutable Bean class with setters and getters

  2. Immutable Bean class with getters and an all-argument constructor (or @Creator annotation on a constructor or static method). Arguments of the constructor must match field names so the object can be instantiated without reflection.

Since Java does not retain argument names in bytecode, you must compile code with -parameters to use an immutable bean class from another jar. Another option is to extend Bean class in your source.

Bindable Types

Generally any type that can be converted from a String representation to a Java type via the ConversionService API can be bound to.

This includes most common Java types. However, you can simply add additional TypeConverter either by defining it as a bean or by registering it in a TypeConverterRegistrar via the service loader.

The handling of nullability deserves special mention. Consider for example the following example:

@Get("/headerInferred")
public String headerInferred(@Header String contentType) {
    // ...
}
@Get("/headerInferred")
String headerInferred(@Header String contentType) {
    // ...
}
@Get("/headerInferred")
fun headerInferred(@Header contentType: String): String {
    // ...
}

In this case, if the HTTP header Content-Type is not present in the request, the route is considered invalid, since it cannot be satisfied, and an HTTP 400 BAD REQUEST is returned.

To make the Content-Type header optional, you can instead write:

@Get("/headerNullable")
public String headerNullable(@Nullable @Header String contentType) {
    // ...
}
@Get("/headerNullable")
String headerNullable(@Nullable @Header String contentType) {
    // ...
}
@Get("/headerNullable")
fun headerNullable(@Header contentType: String?): String? {
    // ...
}

A null string is passed if the header is absent from the request.

java.util.Optional can also be used, but that is discouraged for method parameters.

Additionally, any DateTime that conforms to RFC-1123 can be bound to a parameter. Alternatively the format can be customized with the Format annotation:

@Get("/date")
public String date(@Header ZonedDateTime date) {
    // ...
}

@Get("/dateFormat")
public String dateFormat(@Format("dd/MM/yyyy hh:mm:ss a z") @Header ZonedDateTime date) {
    // ...
}
@Get("/date")
String date(@Header ZonedDateTime date) {
    // ...
}

@Get("/dateFormat")
String dateFormat(@Format("dd/MM/yyyy hh:mm:ss a z") @Header ZonedDateTime date) {
    // ...
}
@Get("/date")
fun date(@Header date: ZonedDateTime): String {
    // ...
}

@Get("/dateFormat")
fun dateFormat(@Format("dd/MM/yyyy hh:mm:ss a z") @Header date: ZonedDateTime): String {
    // ...
}

Type-Based Binding Parameters

Some parameters are recognized by their type instead of their annotation. The following table summarizes the parameter types, their purpose, and provides an example:

Type Description Example

BasicAuth

Allows binding of basic authorization credentials

BasicAuth basicAuth

Variable resolution

The Micronaut framework tries to populate method arguments in the following order:

  1. URI variables like /{id}.

  2. From query parameters if the request is a GET request (e.g. ?foo=bar).

  3. If there is a @Body and request allows the body, bind the body to it.

  4. If the request can have a body and no @Body is defined then try to parse the body (either JSON or form data) and bind the method arguments from the body (see the example).

  5. Finally, if the method arguments cannot be populated return 400 BAD REQUEST.

Binding Method Arguments From Body with no @Body
@Controller("/point")
public class PointController {

    @Post(uri = "/no-body-json")
    @Status(HttpStatus.CREATED)
    Point noBodyJson(Integer x, Integer y) { // (1)
        return new Point(x,y);
    }

    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Post("/no-body-form")
    @Status(HttpStatus.CREATED)
    Point noBodyForm(Integer x, Integer y) {  // (2)
        return new Point(x,y);
    }
}
Binding Method Arguments From Body with no @Body
@Controller("/point")
class PointController {

    @Post(uri = "/no-body-json")
    @Status(HttpStatus.CREATED)
    Point noBodyJson(Integer x, Integer y) { // (1)
        new Point(x,y)
    }

    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Post("/no-body-form")
    @Status(HttpStatus.CREATED)
    Point noBodyForm(Integer x, Integer y) {  // (2)
        new Point(x,y)
    }
}
Binding Method Arguments From Body with no @Body
@Controller("/point")
class PointController {

    @Post(uri = "/no-body-json")
    @Status(HttpStatus.CREATED)
    fun noBodyJson(x: Int, y: Int) = Point(x,y) // (1)

    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Post("/no-body-form")
    @Status(HttpStatus.CREATED)
    fun noBodyForm(x: Int, y: Int) = Point(x,y)  // (2)
}
1 JSON request body binds to method controller arguments, e.g. '{"x":10,"y":20}' (with "application/json")
2 Form data also works, e.g. 'x=10&y=20' (with "application/x-www-form-urlencoded")

6.5 Custom Argument Binding

The Micronaut framework uses an ArgumentBinderRegistry to look up ArgumentBinder beans capable of binding to the arguments in controller methods. The default implementation looks for an annotation on the argument that is meta-annotated with @Bindable. If one exists the argument binder registry searches for an argument binder that supports that annotation.

If no fitting annotation is found, the Micronaut framework tries to find an argument binder that supports the argument type.

An argument binder returns a ArgumentBinder.BindingResult. The binding result gives the Micronaut framework more information than just the value. Binding results are either satisfied or unsatisfied, and either empty or not empty. If an argument binder returns an unsatisfied result, the binder may be called again at different times in request processing. Argument binders are initially called before the body is read and before any filters are executed. If a binder relies on any of that data, and it is not present, return a ArgumentBinder.BindingResult#UNSATISFIED result. Returning an ArgumentBinder.BindingResult#EMPTY or satisfied result will be the final result and the binder will not be called again for that request.

At the end of processing if the result is still ArgumentBinder.BindingResult#UNSATISFIED, it is considered ArgumentBinder.BindingResult#EMPTY.

Key interfaces are:

AnnotatedRequestArgumentBinder

Argument binders that bind based on the presence of an annotation must implement AnnotatedRequestArgumentBinder, and can be used by creating an annotation that is annotated with Bindable. For example:

An example of a binding annotation
import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.bind.annotation.Bindable;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({FIELD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Bindable //(1)
public @interface ShoppingCart {

    @AliasFor(annotation = Bindable.class, member = "value")
    String value() default "";
}
An example of a binding annotation
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.AliasFor
import io.micronaut.core.bind.annotation.Bindable

import java.lang.annotation.Retention
import java.lang.annotation.Target

import static java.lang.annotation.ElementType.ANNOTATION_TYPE
import static java.lang.annotation.ElementType.FIELD
import static java.lang.annotation.ElementType.PARAMETER
import static java.lang.annotation.RetentionPolicy.RUNTIME

@CompileStatic
@Target([FIELD, PARAMETER, ANNOTATION_TYPE])
@Retention(RUNTIME)
@Bindable //(1)
@interface ShoppingCart {
    @AliasFor(annotation = Bindable, member = "value")
    String value() default ""
}
An example of a binding annotation
import io.micronaut.core.bind.annotation.Bindable
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
import kotlin.annotation.AnnotationTarget.FIELD
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER

@Target(FIELD, VALUE_PARAMETER, ANNOTATION_CLASS)
@Retention(RUNTIME)
@Bindable //(1)
annotation class ShoppingCart(val value: String = "")
1 The binding annotation must itself be annotated as Bindable
Example of annotated data binding
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.jackson.serialize.JacksonObjectSerializer;

import jakarta.inject.Singleton;
import java.util.Map;
import java.util.Optional;

@Singleton
public class ShoppingCartRequestArgumentBinder
        implements AnnotatedRequestArgumentBinder<ShoppingCart, Object> { //(1)

    private final ConversionService conversionService;
    private final JacksonObjectSerializer objectSerializer;

    public ShoppingCartRequestArgumentBinder(ConversionService conversionService,
                                             JacksonObjectSerializer objectSerializer) {
        this.conversionService = conversionService;
        this.objectSerializer = objectSerializer;
    }

    @Override
    public Class<ShoppingCart> getAnnotationType() {
        return ShoppingCart.class;
    }

    @Override
    public BindingResult<Object> bind(
            ArgumentConversionContext<Object> context,
            HttpRequest<?> source) { //(2)

        String parameterName = context.getAnnotationMetadata()
                .stringValue(ShoppingCart.class)
                .orElse(context.getArgument().getName());

        Cookie cookie = source.getCookies().get("shoppingCart");
        if (cookie == null) {
            return BindingResult.EMPTY;
        }

        Optional<Map<String, Object>> cookieValue = objectSerializer.deserialize(
                cookie.getValue().getBytes(),
                Argument.mapOf(String.class, Object.class));

        return () -> cookieValue.flatMap(map -> {
            Object obj = map.get(parameterName);
            return conversionService.convert(obj, context);
        });
    }
}
Example of annotated data binding
import groovy.transform.CompileStatic
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.convert.ConversionService
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder
import io.micronaut.http.cookie.Cookie
import io.micronaut.jackson.serialize.JacksonObjectSerializer

import jakarta.inject.Singleton

@CompileStatic
@Singleton
class ShoppingCartRequestArgumentBinder
        implements AnnotatedRequestArgumentBinder<ShoppingCart, Object> { //(1)

    private final ConversionService conversionService
    private final JacksonObjectSerializer objectSerializer

    ShoppingCartRequestArgumentBinder(
            ConversionService conversionService,
            JacksonObjectSerializer objectSerializer) {
        this.conversionService = conversionService
        this.objectSerializer = objectSerializer
    }

    @Override
    Class<ShoppingCart> getAnnotationType() {
        ShoppingCart
    }

    @Override
    BindingResult<Object> bind(
            ArgumentConversionContext<Object> context,
            HttpRequest<?> source) { //(2)

        String parameterName = context.annotationMetadata
                .stringValue(ShoppingCart)
                .orElse(context.argument.name)

        Cookie cookie = source.cookies.get("shoppingCart")
        if (!cookie) {
            return BindingResult.EMPTY
        }

        Optional<Map<String, Object>> cookieValue = objectSerializer.deserialize(
                cookie.value.bytes,
                Argument.mapOf(String, Object))

        return (BindingResult) { ->
            cookieValue.flatMap({value ->
                conversionService.convert(value.get(parameterName), context)
            })
        }
    }
}
Example of annotated data binding
import io.micronaut.core.bind.ArgumentBinder.BindingResult
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.convert.ConversionService
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder
import io.micronaut.jackson.serialize.JacksonObjectSerializer
import java.util.Optional
import jakarta.inject.Singleton

@Singleton
class ShoppingCartRequestArgumentBinder(
        private val conversionService: ConversionService,
        private val objectSerializer: JacksonObjectSerializer
) : AnnotatedRequestArgumentBinder<ShoppingCart, Any> { //(1)

    override fun getAnnotationType(): Class<ShoppingCart> {
        return ShoppingCart::class.java
    }

    override fun bind(context: ArgumentConversionContext<Any>,
                      source: HttpRequest<*>): BindingResult<Any> { //(2)

        val parameterName = context.annotationMetadata
            .stringValue(ShoppingCart::class.java)
            .orElse(context.argument.name)

        val cookie = source.cookies.get("shoppingCart") ?: return BindingResult.EMPTY

        val cookieValue: Optional<Map<String, Any>> = objectSerializer.deserialize(
                cookie.value.toByteArray(),
                Argument.mapOf(String::class.java, Any::class.java))

        return BindingResult {
            cookieValue.flatMap { map: Map<String, Any> ->
                conversionService.convert(map[parameterName], context)
            }
        }
    }
}
1 The custom argument binder must implement AnnotatedRequestArgumentBinder, including both the annotation type to trigger the binder (in this case, MyBindingAnnotation) and the type of the argument expected (in this case, Object)
2 Override the bind method with the custom argument binding logic - in this case, we resolve the name of the annotated argument, extract a value from a cookie with that same name, and convert that value to the argument type
It is common to use ConversionService to convert the data to the type of the argument.

Once the binder is created, we can annotate an argument in our controller method which will be bound using the custom logic we’ve specified.

A controller operation with this annotated binding
    @Get("/annotated")
    HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { //(1)
        return HttpResponse.ok("Session:" + sessionId);
    }
    // end::method
}
A controller operation with this annotated binding
    @Get("/annotated")
    HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { //(1)
        HttpResponse.ok("Session:" + sessionId)
    }
    // end::method
}
A controller operation with this annotated binding
@Get("/annotated")
fun checkSession(@ShoppingCart sessionId: Long): HttpResponse<String> { //(1)
    return HttpResponse.ok("Session:$sessionId")
}
1 The parameter is bound with the binder associated with MyBindingAnnotation. This takes precedence over a type-based binder, if applicable.

TypedRequestArgumentBinder

Argument binders that bind based on the type of the argument must implement TypedRequestArgumentBinder. For example, given this class:

Example of POJO
import io.micronaut.core.annotation.Introspected;

@Introspected
public class ShoppingCart {

    private String sessionId;
    private Integer total;

    public String getSessionId() {
        return sessionId;
    }

    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }

    public Integer getTotal() {
        return total;
    }

    public void setTotal(Integer total) {
        this.total = total;
    }
}
Example of POJO
import io.micronaut.core.annotation.Introspected

@Introspected
class ShoppingCart {
    String sessionId
    Integer total
}
Example of POJO
import io.micronaut.core.annotation.Introspected

@Introspected
class ShoppingCart {
    var sessionId: String? = null
    var total: Int? = null
}

We can define a TypedRequestArgumentBinder for this class, as seen below:

Example of typed data binding
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.jackson.serialize.JacksonObjectSerializer;

import jakarta.inject.Singleton;
import java.util.Optional;

@Singleton
public class ShoppingCartRequestArgumentBinder
        implements TypedRequestArgumentBinder<ShoppingCart> {

    private final JacksonObjectSerializer objectSerializer;

    public ShoppingCartRequestArgumentBinder(JacksonObjectSerializer objectSerializer) {
        this.objectSerializer = objectSerializer;
    }

    @Override
    public BindingResult<ShoppingCart> bind(ArgumentConversionContext<ShoppingCart> context,
                                            HttpRequest<?> source) { //(1)

        Cookie cookie = source.getCookies().get("shoppingCart");
        if (cookie == null) {
            return Optional::empty;
        }

        return () -> objectSerializer.deserialize( //(2)
                cookie.getValue().getBytes(),
                ShoppingCart.class);
    }

    @Override
    public Argument<ShoppingCart> argumentType() {
        return Argument.of(ShoppingCart.class); //(3)
    }
}
Example of typed data binding
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
import io.micronaut.http.cookie.Cookie
import io.micronaut.jackson.serialize.JacksonObjectSerializer

import jakarta.inject.Singleton

@Singleton
class ShoppingCartRequestArgumentBinder
        implements TypedRequestArgumentBinder<ShoppingCart> {

    private final JacksonObjectSerializer objectSerializer

    ShoppingCartRequestArgumentBinder(JacksonObjectSerializer objectSerializer) {
        this.objectSerializer = objectSerializer
    }

    @Override
    BindingResult<ShoppingCart> bind(ArgumentConversionContext<ShoppingCart> context,
                                     HttpRequest<?> source) { //(1)

        Cookie cookie = source.cookies.get("shoppingCart")
        if (!cookie) {
            return BindingResult.EMPTY
        }

        return () -> objectSerializer.deserialize( //(2)
                cookie.value.bytes,
                ShoppingCart)
    }

    @Override
    Argument<ShoppingCart> argumentType() {
        Argument.of(ShoppingCart) //(3)
    }
}
Example of typed data binding
import io.micronaut.core.bind.ArgumentBinder
import io.micronaut.core.bind.ArgumentBinder.BindingResult
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
import io.micronaut.jackson.serialize.JacksonObjectSerializer
import java.util.Optional
import jakarta.inject.Singleton

@Singleton
class ShoppingCartRequestArgumentBinder(private val objectSerializer: JacksonObjectSerializer) :
    TypedRequestArgumentBinder<ShoppingCart> {

    override fun bind(
        context: ArgumentConversionContext<ShoppingCart>,
        source: HttpRequest<*>
    ): BindingResult<ShoppingCart> { //(1)

        val cookie = source.cookies["shoppingCart"]

        return if (cookie == null)
            BindingResult {
                Optional.empty()
            }
        else {
            BindingResult {
                objectSerializer.deserialize( // (2)
                    cookie.value.toByteArray(),
                    ShoppingCart::class.java
                )
            }
        }
    }

    override fun argumentType(): Argument<ShoppingCart> {
        return Argument.of(ShoppingCart::class.java) //(3)
    }
}
1 Override the bind method with the data type to bind, in this example the ShoppingCart type
2 After retrieving the data (in this case, by deserializing JSON text from a cookie), return as a ArgumentBinder.BindingResult
3 Also override the argumentType method, which is used by the ArgumentBinderRegistry.

Once the binder is created, it is used for any controller argument of the associated type:

A controller operation with this typed binding
@Get("/typed")
public HttpResponse<?> loadCart(ShoppingCart shoppingCart) { //(1)
    Map<String, Object> responseMap = new HashMap<>();
    responseMap.put("sessionId", shoppingCart.getSessionId());
    responseMap.put("total", shoppingCart.getTotal());

    return HttpResponse.ok(responseMap);
}
A controller operation with this typed binding
@Get("/typed")
HttpResponse<Map<String, Object>> loadCart(ShoppingCart shoppingCart) { //(1)
    HttpResponse.ok(
            sessionId: shoppingCart.sessionId,
            total: shoppingCart.total)
}
A controller operation with this typed binding
@Get("/typed")
fun loadCart(shoppingCart: ShoppingCart): HttpResponse<*> { //(1)
    return HttpResponse.ok(mapOf(
        "sessionId" to shoppingCart.sessionId,
        "total" to shoppingCart.total))
}
1 The parameter is bound using the custom logic defined for this type in our TypedRequestArgumentBinder

6.6 Host Resolution

You may need to resolve the host name of the current server. The Micronaut framework includes an implementation of the HttpHostResolver interface.

The default implementation looks for host information in the following places in order:

  1. The supplied configuration

  2. The Forwarded header

  3. X-Forwarded- headers. If the X-Forwarded-Host header is not present, the other X-Forwarded headers are ignored.

  4. The Host header

  5. The properties on the request URI

  6. The properties on the embedded server URI

The behavior of which headers to pull the relevant data can be changed with the following configuration:

🔗
Table 1. Configuration Properties for HttpServerConfiguration$HostResolutionConfiguration
Property Type Description

micronaut.server.host-resolution.host-header

java.lang.String

micronaut.server.host-resolution.protocol-header

java.lang.String

micronaut.server.host-resolution.port-header

java.lang.String

micronaut.server.host-resolution.port-in-host

boolean

micronaut.server.host-resolution.allowed-hosts

java.util.List

The above configuration also supports an allowed host list. Configuring this list ensures any resolved host matches one of the supplied regular expression patterns. That is useful to prevent host cache poisoning attacks and is recommended to be configured.

6.7 Locale Resolution

The Micronaut framework supports several strategies for resolving locales for a given request. The getLocale-- method is available on the request, however it only supports parsing the Accept-Language header. For other use cases where the locale can be in a cookie, the user’s session, or should be set to a fixed value, HttpLocaleResolver can be used to determine the current locale.

The LocaleResolver API does not need to be used directly. Simply define a parameter to a controller method of type java.util.Locale and the locale will be resolved and injected automatically.

There are several configuration options to control how to resolve the locale:

🔗
Table 1. Configuration Properties for HttpServerConfiguration$HttpLocaleResolutionConfigurationProperties
Property Type Description

micronaut.server.locale-resolution.fixed

java.util.Locale

micronaut.server.locale-resolution.session-attribute

java.lang.String

micronaut.server.locale-resolution.default-locale

java.util.Locale

micronaut.server.locale-resolution.cookie-name

java.lang.String

micronaut.server.locale-resolution.header

boolean

Locales can be configured in the "en_GB" format, or in the BCP 47 (Language tag) format. If multiple methods are configured, the fixed locale takes precedence, followed by session/cookie, then header.

If any of the built-in methods do not meet your use case, create a bean of type HttpLocaleResolver and set its order (through the getOrder method) relative to the existing resolvers.

6.8 Client IP Address

You may need to resolve the originating IP address of an HTTP Request. The Micronaut framework includes an implementation of HttpClientAddressResolver.

The default implementation resolves the client address in the following places in order:

  1. The configured header

  2. The Forwarded header

  3. The X-Forwarded-For header

  4. The remote address on the request

The first priority header name can be configured with micronaut.server.client-address-header.

6.9 The HttpRequest and HttpResponse

If you need more control over request processing you can write a method that receives the complete HttpRequest.

In fact, there are several higher-level interfaces that can be bound to controller method parameters. These include:

Table 1. Bindable Micronaut Interfaces
Interface Description Example

HttpRequest

The full HttpRequest

String hello(HttpRequest request)

HttpHeaders

All HTTP headers present in the request

String hello(HttpHeaders headers)

HttpParameters

All HTTP parameters (either from URI variables or request parameters) present in the request

String hello(HttpParameters params)

Cookies

All Cookies present in the request

String hello(Cookies cookies)

The HttpRequest should be declared parametrized with a concrete generic type if the request body is needed, e.g. HttpRequest<MyClass> request. The body may not be available from the request otherwise.

In addition, for full control over the emitted HTTP response you can use the static factory methods of the HttpResponse class which return a MutableHttpResponse.

The following example implements the previous MessageController example using the HttpRequest and HttpResponse objects:

Request and Response Example
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.context.ServerRequestContext;
import reactor.core.publisher.Mono;

@Controller("/request")
public class MessageController {

@Get("/hello") // (1)
public HttpResponse<String> hello(HttpRequest<?> request) {
    String name = request.getParameters()
                         .getFirst("name")
                         .orElse("Nobody"); // (2)

    return HttpResponse.ok("Hello " + name + "!!")
             .header("X-My-Header", "Foo"); // (3)
}

}
Request and Response Example
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.context.ServerRequestContext
import reactor.core.publisher.Mono


@Controller("/request")
class MessageController {

@Get("/hello") // (1)
HttpResponse<String> hello(HttpRequest<?> request) {
    String name = request.parameters
                         .getFirst("name")
                         .orElse("Nobody") // (2)

    HttpResponse.ok("Hello " + name + "!!")
             .header("X-My-Header", "Foo") // (3)
}

}
Request and Response Example
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.context.ServerRequestContext
import reactor.core.publisher.Mono
import reactor.util.context.ContextView


@Controller("/request")
class MessageController {

@Get("/hello") // (1)
fun hello(request: HttpRequest<*>): HttpResponse<String> {
    val name = request.parameters
                      .getFirst("name")
                      .orElse("Nobody") // (2)

    return HttpResponse.ok("Hello $name!!")
            .header("X-My-Header", "Foo") // (3)
}

}
1 The method is mapped to the URI /hello and accepts a HttpRequest
2 The HttpRequest is used to obtain the value of a query parameter named name.
3 The HttpResponse.ok(T) method returns a MutableHttpResponse with a text body. A header named X-My-Header is also added to the response.

The HttpRequest is also available from a static context via ServerRequestContext.

Using the ServerRequestContext
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.context.ServerRequestContext;
import reactor.core.publisher.Mono;

@Controller("/request")
public class MessageController {

@Get("/hello-static") // (1)
public HttpResponse<String> helloStatic() {
    HttpRequest<?> request = ServerRequestContext.currentRequest() // (1)
            .orElseThrow(() -> new RuntimeException("No request present"));
    String name = request.getParameters()
            .getFirst("name")
            .orElse("Nobody");

    return HttpResponse.ok("Hello " + name + "!!")
            .header("X-My-Header", "Foo");
}

}
Using the ServerRequestContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.context.ServerRequestContext
import reactor.core.publisher.Mono


@Controller("/request")
class MessageController {

@Get("/hello-static") // (1)
HttpResponse<String> helloStatic() {
    HttpRequest<?> request = ServerRequestContext.currentRequest() // (1)
            .orElseThrow(() -> new RuntimeException("No request present"))
    String name = request.parameters
            .getFirst("name")
            .orElse("Nobody")

    HttpResponse.ok("Hello " + name + "!!")
            .header("X-My-Header", "Foo")
}

}
Using the ServerRequestContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.context.ServerRequestContext
import reactor.core.publisher.Mono
import reactor.util.context.ContextView


@Controller("/request")
class MessageController {

@Get("/hello-static") // (1)
fun helloStatic(): HttpResponse<String> {
    val request: HttpRequest<*> = ServerRequestContext.currentRequest<Any>() // (1)
        .orElseThrow { RuntimeException("No request present") }
    val name = request.parameters
        .getFirst("name")
        .orElse("Nobody")
    return HttpResponse.ok("Hello $name!!")
        .header("X-My-Header", "Foo")
}

}
1 The ServerRequestContext is used to retrieve the request.
Generally ServerRequestContext is available within reactive flow, but the recommended approach is consumed the request as an argument as shown in the previous example. If the request is needed in downstream methods it should be passed as an argument to those methods. There are cases where the context is not propagated because other threads are used to emit the data.

An alternative for users of Project Reactor to using the ServerRequestContext is to use the contextual features of Project Reactor to retrieve the request. Because the Micronaut Framework uses Project Reactor as it’s default reactive streams implementation, users of Project Reactor can benefit by being able to access the request in the context. For example:

Using the Project Reactor context
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.context.ServerRequestContext;
import reactor.core.publisher.Mono;

@Controller("/request")
public class MessageController {

@Get("/hello-reactor")
public Mono<HttpResponse<String>> helloReactor() {
    return Mono.deferContextual(ctx -> { // (1)
        HttpRequest<?> request = ctx.get(ServerRequestContext.KEY); // (2)
        String name = request.getParameters()
                .getFirst("name")
                .orElse("Nobody");

        return Mono.just(HttpResponse.ok("Hello " + name + "!!")
                .header("X-My-Header", "Foo"));
    });
}

}
Using the Project Reactor context
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.context.ServerRequestContext
import reactor.core.publisher.Mono


@Controller("/request")
class MessageController {

@Get("/hello-reactor")
Mono<HttpResponse<String>> helloReactor() {
    Mono.deferContextual(ctx -> { // (1)
        HttpRequest<?> request = ctx.get(ServerRequestContext.KEY) // (2)
        String name = request.parameters
                .getFirst("name")
                .orElse("Nobody")

        Mono.just(HttpResponse.ok("Hello " + name + "!!")
                .header("X-My-Header", "Foo"))
    })
}

}
Using the Project Reactor context
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.context.ServerRequestContext
import reactor.core.publisher.Mono
import reactor.util.context.ContextView


@Controller("/request")
class MessageController {

@Get("/hello-reactor")
fun helloReactor(): Mono<HttpResponse<String>?>? {
    return Mono.deferContextual { ctx: ContextView ->  // (1)
        val request = ctx.get<HttpRequest<*>>(ServerRequestContext.KEY) // (2)
        val name = request.parameters
            .getFirst("name")
            .orElse("Nobody")
        Mono.just(HttpResponse.ok("Hello $name!!")
                .header("X-My-Header", "Foo"))
    }
}

}
1 The Mono is created with a reference to the context
2 The request is retrieved from the context

Using the context to retrieve the request is the best approach for reactive flows because Project Reactor propagates the context, and it does not rely on a thread local like ServerRequestContext.

6.10 Response Status

A Micronaut controller action responds with a 200 HTTP status code by default.

If the action returns an HttpResponse, configure the status code for the response with the status method.

@Get(value = "/http-response", produces = MediaType.TEXT_PLAIN)
public HttpResponse httpResponse() {
    return HttpResponse.status(HttpStatus.CREATED).body("success");
}
@Get(value = "/http-response", produces = MediaType.TEXT_PLAIN)
HttpResponse httpResponse() {
    HttpResponse.status(HttpStatus.CREATED).body("success")
}
@Get(value = "/http-response", produces = [MediaType.TEXT_PLAIN])
fun httpResponse(): HttpResponse<String> {
    return HttpResponse.status<String>(HttpStatus.CREATED).body("success")
}

You can also use the @Status annotation.

@Status(HttpStatus.CREATED)
@Get(produces = MediaType.TEXT_PLAIN)
public String index() {
    return "success";
}
@Status(HttpStatus.CREATED)
@Get(produces = MediaType.TEXT_PLAIN)
String index() {
    "success"
}
@Status(HttpStatus.CREATED)
@Get(produces = [MediaType.TEXT_PLAIN])
fun index(): String {
    return "success"
}

or even respond with an HttpStatus

@Get("/http-status")
public HttpStatus httpStatus() {
    return HttpStatus.CREATED;
}
@Get("/http-status")
HttpStatus httpStatus() {
    HttpStatus.CREATED
}
@Get("/http-status")
fun httpStatus(): HttpStatus {
    return HttpStatus.CREATED
}

6.11 Response Content-Type

A Micronaut controller action produces application/json by default. However, you can change the Content-Type of the response with the @Produces annotation or the produces member of the HTTP method annotations.

import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;

@Controller("/produces")
public class ProducesController {

    @Get // (1)
    public HttpResponse index() {
        return HttpResponse.ok().body("{\"msg\":\"This is JSON\"}");
    }

    @Produces(MediaType.TEXT_HTML)
    @Get("/html") // (2)
    public String html() {
        return "<html><title><h1>HTML</h1></title><body></body></html>";
    }

    @Get(value = "/xml", produces = MediaType.TEXT_XML) // (3)
    public String xml() {
        return "<html><title><h1>XML</h1></title><body></body></html>";
    }
}
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces

@Controller("/produces")
class ProducesController {

    @Get // (1)
    HttpResponse index() {
        HttpResponse.ok().body('{"msg":"This is JSON"}')
    }

    @Produces(MediaType.TEXT_HTML) // (2)
    @Get("/html")
    String html() {
        "<html><title><h1>HTML</h1></title><body></body></html>"
    }

    @Get(value = "/xml", produces = MediaType.TEXT_XML) // (3)
    String xml() {
        "<html><title><h1>XML</h1></title><body></body></html>"
    }
}
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces

@Controller("/produces")
class ProducesController {

    @Get // (1)
    fun index(): HttpResponse<*> {
        return HttpResponse.ok<Any>().body("{\"msg\":\"This is JSON\"}")
    }

    @Produces(MediaType.TEXT_HTML)
    @Get("/html") // (2)
    fun html(): String {
        return "<html><title><h1>HTML</h1></title><body></body></html>"
    }

    @Get(value = "/xml", produces = [MediaType.TEXT_XML]) // (3)
    fun xml(): String {
        return "<html><title><h1>XML</h1></title><body></body></html>"
    }
}
1 The default content type is JSON
2 Annotate a controller action with @Produces to change the response content type.
3 Setting the produces member of the method annotation also changes the content type.

6.12 Accepted Request Content-Type

A Micronaut controller action consumes application/json by default. Consuming other content types is supported with the @Consumes annotation, or the consumes member of any HTTP method annotation.

import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Consumes;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;

@Controller("/consumes")
public class ConsumesController {

    @Post // (1)
    public HttpResponse index() {
        return HttpResponse.ok();
    }

    @Consumes({MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON}) // (2)
    @Post("/multiple")
    public HttpResponse multipleConsumes() {
        return HttpResponse.ok();
    }

    @Post(value = "/member", consumes = MediaType.TEXT_PLAIN) // (3)
    public HttpResponse consumesMember() {
        return HttpResponse.ok();
    }
}
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Consumes
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post

@Controller("/consumes")
class ConsumesController {

    @Post // (1)
    HttpResponse index() {
        HttpResponse.ok()
    }

    @Consumes([MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON]) // (2)
    @Post("/multiple")
    HttpResponse multipleConsumes() {
        HttpResponse.ok()
    }

    @Post(value = "/member", consumes = MediaType.TEXT_PLAIN) // (3)
    HttpResponse consumesMember() {
        HttpResponse.ok()
    }
}
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Consumes
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post

@Controller("/consumes")
class ConsumesController {

    @Post // (1)
    fun index(): HttpResponse<*> {
        return HttpResponse.ok<Any>()
    }

    @Consumes(MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON) // (2)
    @Post("/multiple")
    fun multipleConsumes(): HttpResponse<*> {
        return HttpResponse.ok<Any>()
    }

    @Post(value = "/member", consumes = [MediaType.TEXT_PLAIN]) // (3)
    fun consumesMember(): HttpResponse<*> {
        return HttpResponse.ok<Any>()
    }
}
1 By default, a controller action consumes request with Content-Type of type application/json.
2 The @Consumes annotation takes a String[] of supported media types for an incoming request.
3 Content types can also be specified with the consumes member of the method annotation.

Customizing Processed Content Types

Normally JSON parsing only happens if the content type is application/json. The other MediaTypeCodec classes behave similarly in that they have predefined content types they can process. To extend the list of media types that a given codec processes, provide configuration that will be stored in CodecConfiguration:

micronaut.codec.json.additional-types[0]=text/javascript
micronaut.codec.json.additional-types[1]=...
micronaut:
  codec:
    json:
      additional-types:
        - text/javascript
        - ...
[micronaut]
  [micronaut.codec]
    [micronaut.codec.json]
      additional-types=[
        "text/javascript",
        "..."
      ]
micronaut {
  codec {
    json {
      additionalTypes = ["text/javascript", "..."]
    }
  }
}
{
  micronaut {
    codec {
      json {
        additional-types = ["text/javascript", "..."]
      }
    }
  }
}
{
  "micronaut": {
    "codec": {
      "json": {
        "additional-types": ["text/javascript", "..."]
      }
    }
  }
}

The currently supported configuration prefixes are json, json-stream, text, and text-stream.

6.13 Reactive HTTP Request Processing

As mentioned previously, Micronaut framework is built on Netty which is designed around an Event loop model and non-blocking I/O. Micronaut executes code defined in @Controller beans in the same thread as the request thread (an Event Loop thread).

This makes it critical that if you do any blocking I/O operations (for example interactions with Hibernate/JPA or JDBC) that you offload those tasks to a separate thread pool that does not block the Event loop.

For example the following configuration configures the I/O thread pool as a fixed thread pool with 75 threads (similar to what a traditional blocking server such as Tomcat uses in the thread-per-request model):

Configuring the IO thread pool
micronaut.executors.io.type=fixed
micronaut.executors.io.nThreads=75
micronaut:
  executors:
    io:
      type: fixed
      nThreads: 75
[micronaut]
  [micronaut.executors]
    [micronaut.executors.io]
      type="fixed"
      nThreads=75
micronaut {
  executors {
    io {
      type = "fixed"
      nThreads = 75
    }
  }
}
{
  micronaut {
    executors {
      io {
        type = "fixed"
        nThreads = 75
      }
    }
  }
}
{
  "micronaut": {
    "executors": {
      "io": {
        "type": "fixed",
        "nThreads": 75
      }
    }
  }
}

To use this thread pool in a @Controller bean you have a number of options. The simplest is to use the @ExecuteOn annotation, which can be applied only to either a @Controller or @Filter at the type or method level. It indicates the configured thread pool to run method(s) of the controller or filter on:

Using @ExecuteOn
import io.micronaut.docs.http.server.reactive.PersonService;
import io.micronaut.docs.ioc.beans.Person;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;

@Controller("/executeOn/people")
public class PersonController {

    private final PersonService personService;

    PersonController(PersonService personService) {
        this.personService = personService;
    }

    @Get("/{name}")
    @ExecuteOn(TaskExecutors.IO) // (1)
    Person byName(String name) {
        return personService.findByName(name);
    }
}
Using @ExecuteOn
import io.micronaut.docs.http.server.reactive.PersonService
import io.micronaut.docs.ioc.beans.Person
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn

@Controller("/executeOn/people")
class PersonController {

    private final PersonService personService

    PersonController(PersonService personService) {
        this.personService = personService
    }

    @Get("/{name}")
    @ExecuteOn(TaskExecutors.IO) // (1)
    Person byName(String name) {
        personService.findByName(name)
    }
}
Using @ExecuteOn
import io.micronaut.docs.http.server.reactive.PersonService
import io.micronaut.docs.ioc.beans.Person
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn

@Controller("/executeOn/people")
class PersonController (private val personService: PersonService) {

    @Get("/{name}")
    @ExecuteOn(TaskExecutors.IO) // (1)
    fun byName(name: String): Person {
        return personService.findByName(name)
    }
}
1 The @ExecuteOn annotation is used to execute the operation on the I/O thread pool

The value of the @ExecuteOn annotation can be any named executor defined under micronaut.executors.

Generally speaking for database operations you want a thread pool configured that matches the maximum number of connections specified in the database connection pool.

An alternative to the @ExecuteOn annotation is to use the facility provided by the reactive library you have chosen. Reactive implementations such as Project Reactor or RxJava feature a subscribeOn method which lets you alter which thread executes user code. For example:

Reactive subscribeOn Example
import io.micronaut.docs.ioc.beans.Person;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.scheduling.TaskExecutors;
import jakarta.inject.Named;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import io.micronaut.core.async.annotation.SingleResult;
import java.util.concurrent.ExecutorService;

@Controller("/subscribeOn/people")
public class PersonController {

    private final Scheduler scheduler;
    private final PersonService personService;

    PersonController(
            @Named(TaskExecutors.IO) ExecutorService executorService, // (1)
            PersonService personService) {
        this.scheduler = Schedulers.fromExecutorService(executorService);
        this.personService = personService;
    }

    @Get("/{name}")
    @SingleResult
    Publisher<Person> byName(String name) {
        return Mono
                .fromCallable(() -> personService.findByName(name)) // (2)
                .subscribeOn(scheduler); // (3)
    }
}
Reactive subscribeOn Example
import io.micronaut.docs.ioc.beans.Person
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.scheduling.TaskExecutors
import jakarta.inject.Named
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
import reactor.core.scheduler.Schedulers
import java.util.concurrent.ExecutorService

@Controller("/subscribeOn/people")
class PersonController {

    private final Scheduler scheduler
    private final PersonService personService

    PersonController(
            @Named(TaskExecutors.IO) ExecutorService executorService, // (1)
            PersonService personService) {
        this.scheduler = Schedulers.fromExecutorService(executorService)
        this.personService = personService
    }

    @Get("/{name}")
    Mono<Person> byName(String name) {
        return Mono
                .fromCallable({ -> personService.findByName(name) }) // (2)
                .subscribeOn(scheduler) // (3)
    }
}
Reactive subscribeOn Example
import io.micronaut.docs.ioc.beans.Person
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.scheduling.TaskExecutors
import java.util.concurrent.ExecutorService
import jakarta.inject.Named
import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
import reactor.core.scheduler.Schedulers


@Controller("/subscribeOn/people")
class PersonController internal constructor(
    @Named(TaskExecutors.IO) executorService: ExecutorService, // (1)
    private val personService: PersonService) {

    private val scheduler: Scheduler = Schedulers.fromExecutorService(executorService)

    @Get("/{name}")
    fun byName(name: String): Mono<Person> {
        return Mono
            .fromCallable { personService.findByName(name) } // (2)
            .subscribeOn(scheduler) // (3)
    }
}
1 The configured I/O executor service is injected
2 The Mono::fromCallable method wraps the blocking operation
3 The Project Reactor subscribeOn method schedules the operation on the I/O thread pool

6.13.1 Using the @Body Annotation

To parse the request body, you first indicate to the Micronaut framework which parameter receives the data with the Body annotation.

The following example implements a simple echo server that echoes the body sent in the request:

Using the @Body annotation
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import jakarta.validation.constraints.Size;

@Controller("/receive")
public class MessageController {

@Post(value = "/echo", consumes = MediaType.TEXT_PLAIN) // (1)
String echo(@Size(max = 1024) @Body String text) { // (2)
    return text; // (3)
}

}
Using the @Body annotation
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import jakarta.validation.constraints.Size

@Controller("/receive")
class MessageController {

@Post(value = "/echo", consumes = MediaType.TEXT_PLAIN) // (1)
String echo(@Size(max = 1024) @Body String text) { // (2)
    text // (3)
}

}
Using the @Body annotation
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import jakarta.validation.constraints.Size

@Controller("/receive")
open class MessageController {

@Post(value = "/echo", consumes = [MediaType.TEXT_PLAIN]) // (1)
open fun echo(@Size(max = 1024) @Body text: String): String { // (2)
    return text // (3)
}

}
1 The Post annotation is used with a MediaType of text/plain (the default is application/json).
2 The Body annotation is used with a jakarta.validation.constraints.Size that limits the size of the body to at most 1KB. This constraint does not limit the amount of data read/buffered by the server.
3 The body is returned as the result of the method

Note that reading the request body is done in a non-blocking manner in that the request contents are read as the data becomes available and accumulated into the String passed to the method.

The micronaut.server.maxRequestSize setting in your configuration file (e.g. application.yml) limits the size of the data (the default maximum request size is 10MB) read/buffered by the server. @Size is not a replacement for this setting.

Regardless of the limit, for a large amount of data accumulating the data into a String in-memory may lead to memory strain on the server. A better approach is to include a Reactive library in your project (such as Reactor, RxJava,or Akka) that supports the Reactive streams implementation and stream the data it becomes available:

Using Reactive Streams to Read the request body
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import jakarta.validation.constraints.Size;

import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import io.micronaut.core.async.annotation.SingleResult;

@Controller("/receive")
public class MessageController {

@Post(value = "/echo-publisher", consumes = MediaType.TEXT_PLAIN) // (1)
@SingleResult
Publisher<HttpResponse<String>> echoFlow(@Body Publisher<String> text) { //(2)
    return Flux.from(text)
            .collect(StringBuffer::new, StringBuffer::append) // (3)
            .map(buffer -> HttpResponse.ok(buffer.toString()));
}

}
Using Reactive Streams to Read the request body
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import jakarta.validation.constraints.Size

import org.reactivestreams.Publisher
import io.micronaut.core.async.annotation.SingleResult
import reactor.core.publisher.Flux

@Controller("/receive")
class MessageController {

@Post(value = "/echo-publisher", consumes = MediaType.TEXT_PLAIN) // (1)
@SingleResult
Publisher<HttpResponse<String>> echoFlow(@Body Publisher<String> text) { // (2)
    return Flux.from(text)
            .collect({ x -> new StringBuffer() }, { StringBuffer sb, String s -> sb.append(s) }) // (3)
            .map({ buffer -> HttpResponse.ok(buffer.toString()) });
}

}
Using Reactive Streams to Read the request body
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import jakarta.validation.constraints.Size

import org.reactivestreams.Publisher
import io.micronaut.core.async.annotation.SingleResult
import reactor.core.publisher.Flux

@Controller("/receive")
open class MessageController {

@Post(value = "/echo-publisher", consumes = [MediaType.TEXT_PLAIN]) // (1)
@SingleResult
open fun echoFlow(@Body text: Publisher<String>): Publisher<HttpResponse<String>> { //(2)
    return Flux.from(text)
        .collect({ StringBuffer() }, { obj, str -> obj.append(str) }) // (3)
        .map { buffer -> HttpResponse.ok(buffer.toString()) }
}

}
1 In this case the method is altered to receive and return an Publisher type.
2 This example uses Project Reactor and returns a single item. Because of that the response type is annotated also with SingleResult. The Micronaut framework only emits the response once the operation completes without blocking.
3 The collect method is used to accumulate the data in this simulated example, but it could for example write the data to a logging service, database, etc. chunk by chunk
Body arguments of types that do not require conversion cause the Micronaut framework to skip decoding of the request!

6.13.2 Reactive Responses

The previous section introduced the notion of Reactive programming using Project Reactor and Micronaut.

The Micronaut framework supports returning common reactive types such as Mono (or Single Maybe Observable types from RxJava), an instance of Publisher or CompletableFuture from any controller method.

To use Project Reactor's Flux or Mono you need to add the Micronaut Reactor dependency to your project to include the necessary converters.
To use RxJava's Flowable, Single or Maybe you need to add the Micronaut RxJava dependency to your project to include the necessary converters.

The argument designated as the body of the request using the Body annotation can also be a reactive type or a CompletableFuture.

When returning a reactive type, The Micronaut framework subscribes to the returned reactive type on the same thread as the request (a Netty Event Loop thread). It is therefore important that if you perform any blocking operations, you offload those operations to an appropriately configured thread pool, for example using the Project Reactor or RxJava subscribeOn(..) facility or @ExecuteOn.

See the section on Configuring Thread Pools for information on the thread pools that the Micronaut framework sets up and how to configure them.

To summarize, the following table illustrates some common response types and their handling:

Table 1. Micronaut Response Types
Type Description Example Signature

Publisher

Any type that implements the Publisher interface

Publisher<String> hello()

CompletableFuture

A Java CompletableFuture instance

CompletableFuture<String> hello()

HttpResponse

An HttpResponse and optional response body

HttpResponse<Publisher<String>> hello()

CharSequence

Any implementation of CharSequence

String hello()

T

Any simple POJO type

Book show()

When returning a Reactive type, its type affects the returned response. For example, when returning a Flux, the Micronaut framework cannot know the size of the response, so Transfer-Encoding type of Chunked is used. Whilst for types that emit a single result such as Mono the Content-Length header is populated.

6.14 JSON Binding

The most common data interchange format nowadays is JSON.

By default, the Controller annotation specifies that the controllers in Micronaut framework consume and produce JSON by default.

Since Micronaut Framework 4.0, users must choose how they want to serialize (Jackson Databind or Micronaut Serialization). Both approaches allow the usage of Jackson Annotations.

With either approach, the Micronaut framework reads incoming JSON in a non-blocking manner.

Serialize using Micronaut Serialization

Micronaut Serialization offers reflection-free serialization using build-time Bean Introspections. It supports alternative formats such as JSON-P or JSON-B. You need to add the following dependencies:

annotationProcessor("io.micronaut.serde:micronaut-serde-processor")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.serde</groupId>
        <artifactId>micronaut-serde-processor</artifactId>
    </path>
</annotationProcessorPaths>
implementation("io.micronaut.serde:micronaut-serde-jackson")
<dependency>
    <groupId>io.micronaut.serde</groupId>
    <artifactId>micronaut-serde-jackson</artifactId>
</dependency>

Serialization using Jackson Databind

To serialize using Jackson Databind include the following dependency:

implementation("io.micronaut:micronaut-jackson-databind")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-jackson-databind</artifactId>
</dependency>

JsonMapper

You may be used to work with Jackson’s ObjectMapper. However, we don’t recommend using Jackson’s ObjectMapper directly; instead you should use JsonMapper, an API almost identical to Jackson’s ObjectMapper. Moreover, both Micronaut Serialization and Micronaut Jackson Databind implement JsonMapper.

You can inject a bean of type JsonMapper or manually instantiate one via JsonMapper.createDefault().

Binding using Reactive Frameworks

From a developer perspective however, you can generally just work with Plain Old Java Objects (POJOs) and can optionally use a Reactive framework such as RxJava or Project Reactor. The following is an example of a controller that reads and saves an incoming POJO in a non-blocking way from JSON:

Using Reactive Streams to Read the JSON
@Controller("/people")
public class PersonController {

    Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>();

@Post("/saveReactive")
@SingleResult
public Publisher<HttpResponse<Person>> save(@Body Publisher<Person> person) { // (1)
    return Mono.from(person).map(p -> {
                inMemoryDatastore.put(p.getFirstName(), p); // (2)
                return HttpResponse.created(p); // (3)
            }
    );
}

}
Using Reactive Streams to Read the JSON
@Controller("/people")
class PersonController {

    Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>()

@Post("/saveReactive")
@SingleResult
Publisher<HttpResponse<Person>> save(@Body Publisher<Person> person) { // (1)
    Mono.from(person).map({ p ->
        inMemoryDatastore.put(p.getFirstName(), p) // (2)
        HttpResponse.created(p) // (3)
    })
}

}
Using Reactive Streams to Read the JSON
@Controller("/people")
class PersonController {

    internal var inMemoryDatastore: MutableMap<String, Person> = ConcurrentHashMap()

@Post("/saveReactive")
@SingleResult
fun save(@Body person: Publisher<Person>): Publisher<HttpResponse<Person>> { // (1)
    return Mono.from(person).map { p ->
        inMemoryDatastore[p.firstName] = p // (2)
        HttpResponse.created(p) // (3)
    }
}

}
1 The method receives a Publisher which emits the POJO once the JSON has been read
2 The map method stores the instance in a Map
3 An HttpResponse is returned

Using cURL from the command line, you can POST JSON to the /people URI:

Using cURL to Post JSON
$ curl -X POST localhost:8080/people -d '{"firstName":"Fred","lastName":"Flintstone","age":45}'

Binding Using CompletableFuture

The same method as the previous example can also be written with the CompletableFuture API instead:

Using CompletableFuture to Read the JSON
@Controller("/people")
public class PersonController {

    Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>();

@Post("/saveFuture")
public CompletableFuture<HttpResponse<Person>> save(@Body CompletableFuture<Person> person) {
    return person.thenApply(p -> {
                inMemoryDatastore.put(p.getFirstName(), p);
                return HttpResponse.created(p);
            }
    );
}

}
Using CompletableFuture to Read the JSON
@Controller("/people")
class PersonController {

    Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>()

@Post("/saveFuture")
CompletableFuture<HttpResponse<Person>> save(@Body CompletableFuture<Person> person) {
    person.thenApply({ p ->
        inMemoryDatastore.put(p.getFirstName(), p)
        HttpResponse.created(p)
    })
}

}
Using CompletableFuture to Read the JSON
@Controller("/people")
class PersonController {

    internal var inMemoryDatastore: MutableMap<String, Person> = ConcurrentHashMap()

@Post("/saveFuture")
fun save(@Body person: CompletableFuture<Person>): CompletableFuture<HttpResponse<Person>> {
    return person.thenApply { p ->
        inMemoryDatastore[p.firstName] = p
        HttpResponse.created(p)
    }
}

}

The above example uses the thenApply method to achieve the same as the previous example.

Binding using POJOs

Note however you can just as easily write:

Binding JSON POJOs
@Controller("/people")
public class PersonController {

    Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>();

@Post
public HttpResponse<Person> save(@Body Person person) {
    inMemoryDatastore.put(person.getFirstName(), person);
    return HttpResponse.created(person);
}

}
Binding JSON POJOs
@Controller("/people")
class PersonController {

    Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>()

@Post
HttpResponse<Person> save(@Body Person person) {
    inMemoryDatastore.put(person.getFirstName(), person)
    HttpResponse.created(person)
}

}
Binding JSON POJOs
@Controller("/people")
class PersonController {

    internal var inMemoryDatastore: MutableMap<String, Person> = ConcurrentHashMap()

@Post
fun save(@Body person: Person): HttpResponse<Person> {
    inMemoryDatastore[person.firstName] = person
    return HttpResponse.created(person)
}

}

The Micronaut framework only executes your method once the data has been read in a non-blocking manner.

You can customize the output in various ways, such as using Jackson annotations.

Jackson Configuration

If you use micronaut-jackson-databind, the Jackson’s ObjectMapper can be configured through configuration with the JacksonConfiguration class.

All Jackson configuration keys start with jackson.

dateFormat

String

The date format

locale

String

Uses Locale.forLanguageTag. Example: en-US

timeZone

String

Uses TimeZone.getTimeZone. Example: PST

serializationInclusion

String

One of JsonInclude.Include. Example: ALWAYS

propertyNamingStrategy

String

Name of an instance of PropertyNamingStrategy. Example: SNAKE_CASE

defaultTyping

String

The global defaultTyping for polymorphic type handling from enum ObjectMapper.DefaultTyping. Example: NON_FINAL

Example:

jackson.serializationInclusion=ALWAYS
jackson:
  serializationInclusion: ALWAYS
[jackson]
  serializationInclusion="ALWAYS"
jackson {
  serializationInclusion = "ALWAYS"
}
{
  jackson {
    serializationInclusion = "ALWAYS"
  }
}
{
  "jackson": {
    "serializationInclusion": "ALWAYS"
  }
}

Features

If you use micronaut-jackson-databind, all Jackson’s features can be configured with their name as the key and a boolean to indicate enabled or disabled.

serialization

Map

SerializationFeature

deserialization

Map

DeserializationFeature

mapper

Map

MapperFeature

parser

Map

JsonParser.Feature

generator

Map

JsonGenerator.Feature

factory

Map

JsonFactory.Feature

Example:

jackson.serialization.indentOutput=true
jackson.serialization.writeDatesAsTimestamps=false
jackson.deserialization.useBigIntegerForInts=true
jackson.deserialization.failOnUnknownProperties=false
jackson:
  serialization:
    indentOutput: true
    writeDatesAsTimestamps: false
  deserialization:
    useBigIntegerForInts: true
    failOnUnknownProperties: false
[jackson]
  [jackson.serialization]
    indentOutput=true
    writeDatesAsTimestamps=false
  [jackson.deserialization]
    useBigIntegerForInts=true
    failOnUnknownProperties=false
jackson {
  serialization {
    indentOutput = true
    writeDatesAsTimestamps = false
  }
  deserialization {
    useBigIntegerForInts = true
    failOnUnknownProperties = false
  }
}
{
  jackson {
    serialization {
      indentOutput = true
      writeDatesAsTimestamps = false
    }
    deserialization {
      useBigIntegerForInts = true
      failOnUnknownProperties = false
    }
  }
}
{
  "jackson": {
    "serialization": {
      "indentOutput": true,
      "writeDatesAsTimestamps": false
    },
    "deserialization": {
      "useBigIntegerForInts": true,
      "failOnUnknownProperties": false
    }
  }
}

Further customising JsonFactory

If you use micronaut-jackson-databind, there may be situations where you wish to customise the JsonFactory used by the ObjectMapper beyond the configuration of features (for example to allow custom character escaping). This can be achieved by providing your own JsonFactory bean, or by providing a BeanCreatedEventListener<JsonFactory> which configures the default bean on startup.

Support for @JsonView

If you use micronaut-jackson-databind, you can use the @JsonView annotation on controller methods if you set jackson.json-view.enabled to true in your configuration file (e.g application.yml).

Jackson’s @JsonView annotation lets you control which properties are exposed on a per-response basis. See Jackson JSON Views for more information.

Beans

If you use micronaut-jackson-databind, in addition to configuration, beans can be registered to customize Jackson. All beans that extend any of the following classes are registered with the object mapper:

Service Loader

Any modules registered via the service loader are also added to the default object mapper.

Number Precision

During JSON parsing, the framework may convert any incoming data to an intermediate object model. By default, this model uses BigInteger, long and double for numeric values. This means some information that could be represented by BigDecimal may be lost. For example, numbers with many decimal places that cannot be represented by double may be truncated, even if the target type for deserialization uses BigDecimal. Metadata on the number of trailing zeroes (BigDecimal.precision()), e.g. the difference between 0.12 and 0.120, is also discarded.

If you need full accuracy for number types, use the following configuration:

jackson.deserialization.useBigIntegerForInts=true
jackson.deserialization.useBigDecimalForFloats=true
jackson:
  deserialization:
    useBigIntegerForInts: true
    useBigDecimalForFloats: true
[jackson]
  [jackson.deserialization]
    useBigIntegerForInts=true
    useBigDecimalForFloats=true
jackson {
  deserialization {
    useBigIntegerForInts = true
    useBigDecimalForFloats = true
  }
}
{
  jackson {
    deserialization {
      useBigIntegerForInts = true
      useBigDecimalForFloats = true
    }
  }
}
{
  "jackson": {
    "deserialization": {
      "useBigIntegerForInts": true,
      "useBigDecimalForFloats": true
    }
  }
}

6.15 Plain Text Responses

By default, a Micronaut Controller responds with content-type application/json. However, you can respond with content type text/plain by annotating the controller method with the @Produces annotation.

HTTP Response with text/plain Content-Type
@Controller("/txt")
public class TextPlainController {

    @Get("/date")
    @Produces(MediaType.TEXT_PLAIN) // (1)
    String date() {
        return new Calendar.Builder().setDate(2023,7,4).build().toString(); // (2)
    }

}
HTTP Response with text/plain Content-Type
@Controller('/txt')
class TextPlainController {

    @Get('/date')
    @Produces(MediaType.TEXT_PLAIN) // (1)
    String date() {
        new Calendar.Builder().setDate(2023,7,4).build().toString() // (2)
    }

}
HTTP Response with text/plain Content-Type
@Controller("/txt")
class TextPlainController {

    @Get("/date")
    @Produces(MediaType.TEXT_PLAIN) // (1)
    fun date(): String = Calendar.Builder().setDate(2023, 7, 4).build().toString() // (2)

}
1 The Controller endpoint specifies a response’s Content-Type of text/plain.
2 The endpoint returns type String, and the implementation explicitly converts the data to a string using the toString() method.
Micronaut Framework 4.x text/plain responses are more restrictive about allowed types than Micronaut Framework 3.x. To return plain text responses for answers other than java.lang.String, manually call the object toString() method. Alternatively, set the micronaut.http.legacy-text-conversion configuration option to true to restore the old – but not recommended – Micronaut Framework 3.x behavior.

6.16 Data Validation

It is easy to validate incoming data with Micronaut controllers using Validation Advice.

The Micronaut framework provides native support for the jakarta.validation annotations with the micronaut-validation dependency:

annotationProcessor("io.micronaut.validation:micronaut-validation-processor")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.validation</groupId>
        <artifactId>micronaut-validation-processor</artifactId>
    </path>
</annotationProcessorPaths>

implementation("io.micronaut.validation:micronaut-validation")
<dependency>
    <groupId>io.micronaut.validation</groupId>
    <artifactId>micronaut-validation</artifactId>
</dependency>

Or full JSR 380 compliance with the micronaut-hibernate-validator dependency:

implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")
<dependency>
    <groupId>io.micronaut.beanvalidation</groupId>
    <artifactId>micronaut-hibernate-validator</artifactId>
</dependency>

We can validate parameters using jakarta.validation annotations and the Validated annotation at the class level.

Example
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.validation.Validated;

import jakarta.validation.constraints.NotBlank;
import java.util.Collections;

@Validated // (1)
@Controller("/email")
public class EmailController {

    @Get("/send")
    public HttpResponse send(@NotBlank String recipient, // (2)
                             @NotBlank String subject) { // (2)
        return HttpResponse.ok(Collections.singletonMap("msg", "OK"));
    }
}
Example
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.validation.Validated

import jakarta.validation.constraints.NotBlank

@Validated // (1)
@Controller("/email")
class EmailController {

    @Get("/send")
    HttpResponse send(@NotBlank String recipient, // (2)
                      @NotBlank String subject) { // (2)
        HttpResponse.ok(msg: "OK")
    }
}
Example
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.validation.Validated
import jakarta.validation.constraints.NotBlank

@Validated // (1)
@Controller("/email")
open class EmailController {

    @Get("/send")
    open fun send(@NotBlank recipient: String, // (2)
                  @NotBlank subject: String): HttpResponse<*> { // (2)
        return HttpResponse.ok(mapOf("msg" to "OK"))
    }
}
1 Annotate controller with Validated
2 subject and recipient cannot be blank.

If a validation error occurs a jakarta.validation.ConstraintViolationException is thrown. By default, the integrated io.micronaut.validation.exception.ConstraintExceptionHandler handles the exception, leading to a behaviour as shown in the following test:

Example Test
@Test
void testParametersAreValidated() {
    HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () ->
        client.toBlocking().exchange("/email/send?subject=Hi&recipient="));
    HttpResponse<?> response = e.getResponse();

    assertEquals(HttpStatus.BAD_REQUEST, response.getStatus());

    response = client.toBlocking().exchange("/email/send?subject=Hi&recipient=me@micronaut.example");

    assertEquals(HttpStatus.OK, response.getStatus());
}
Example Test
def "test parameter validation"() {
    when:
    client.toBlocking().exchange('/email/send?subject=Hi&recipient=')

    then:
    def e = thrown(HttpClientResponseException)
    def response = e.response
    response.status == HttpStatus.BAD_REQUEST

    when:
    response = client.toBlocking().exchange('/email/send?subject=Hi&recipient=me@micronaut.example')

    then:
    response.status == HttpStatus.OK
}
Example Test
"test params are validated"() {
    val e = shouldThrow<HttpClientResponseException> {
        client.toBlocking().exchange<Any>("/email/send?subject=Hi&recipient=")
    }
    var response = e.response

    response.status shouldBe HttpStatus.BAD_REQUEST

    response = client.toBlocking().exchange<Any>("/email/send?subject=Hi&recipient=me@micronaut.example")

    response.status shouldBe HttpStatus.OK
}

To use your own ExceptionHandler to handle the constraint exceptions, annotate it with @Replaces(ConstraintExceptionHandler.class)

Often you may want to use POJOs as controller method parameters.

import io.micronaut.core.annotation.Introspected;

import jakarta.validation.constraints.NotBlank;

@Introspected
public class Email {

    @NotBlank // (1)
    String subject;

    @NotBlank // (1)
    String recipient;

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getRecipient() {
        return recipient;
    }

    public void setRecipient(String recipient) {
        this.recipient = recipient;
    }
}
import io.micronaut.core.annotation.Introspected

import jakarta.validation.constraints.NotBlank

@Introspected
class Email {

    @NotBlank // (1)
    String subject

    @NotBlank // (1)
    String recipient
}
import io.micronaut.core.annotation.Introspected
import jakarta.validation.constraints.NotBlank

@Introspected
open class Email {

    @NotBlank // (1)
    var subject: String? = null

    @NotBlank // (1)
    var recipient: String? = null
}
1 You can use jakarta.validation annotations in your POJOs.

Annotate your controller with Validated, and annotate the binding POJO with @Valid.

Example
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.validation.Validated;

import jakarta.validation.Valid;
import java.util.Collections;

@Validated // (1)
@Controller("/email")
public class EmailController {

    @Post("/send")
    public HttpResponse send(@Body @Valid Email email) { // (2)
        return HttpResponse.ok(Collections.singletonMap("msg", "OK"));
    }
}
Example
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.validation.Validated

import jakarta.validation.Valid

@Validated // (1)
@Controller("/email")
class EmailController {

    @Post("/send")
    HttpResponse send(@Body @Valid Email email) { // (2)
        HttpResponse.ok(msg: "OK")
    }
}
Example
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.validation.Validated
import jakarta.validation.Valid

@Validated // (1)
@Controller("/email")
open class EmailController {

    @Post("/send")
    open fun send(@Body @Valid email: Email): HttpResponse<*> { // (2)
        return HttpResponse.ok(mapOf("msg" to "OK"))
    }
}
1 Annotate the controller with Validated
2 Annotate the POJO to validate with @Valid

Validation of POJOs is shown in the following test:

@Test
void testPojoValidation() {
    HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> {
        Email email = new Email();
        email.subject = "Hi";
        email.recipient = "";
        client.toBlocking().exchange(HttpRequest.POST("/email/send", email));
    });
    HttpResponse<?> response = e.getResponse();

    assertEquals(HttpStatus.BAD_REQUEST, response.getStatus());

    Email email = new Email();
    email.subject = "Hi";
    email.recipient = "me@micronaut.example";
    response = client.toBlocking().exchange(HttpRequest.POST("/email/send", email));

    assertEquals(HttpStatus.OK, response.getStatus());
}
def "invoking /email/send parse parameters in a POJO and validates"() {
    when:
    Email email = new Email(subject: 'Hi', recipient: '')
    client.toBlocking().exchange(HttpRequest.POST('/email/send', email))

    then:
    def e = thrown(HttpClientResponseException)
    def response = e.response
    response.status == HttpStatus.BAD_REQUEST

    when:
    email = new Email(subject: 'Hi', recipient: 'me@micronaut.example')
    response = client.toBlocking().exchange(HttpRequest.POST('/email/send', email))

    then:
    response.status == HttpStatus.OK
}
"test pojo validation" {
    val e = shouldThrow<HttpClientResponseException> {
        val email = Email()
        email.subject = "Hi"
        email.recipient = ""
        client.toBlocking().exchange<Email, Any>(HttpRequest.POST("/email/send", email))
    }
    var response = e.response

    response.status shouldBe HttpStatus.BAD_REQUEST

    val email = Email()
    email.subject = "Hi"
    email.recipient = "me@micronaut.example"
    response = client.toBlocking().exchange<Email, Any>(HttpRequest.POST("/email/send", email))

    response.status shouldBe HttpStatus.OK
}
Bean injection is supported in custom constraints with the Hibernate Validator configuration.

6.16.1 Validation Groups

You can enforce a subset of constraints using validation groups using groups on Validated. More information is available in the Bean Validation specification

import jakarta.validation.groups.Default;

public interface FinalValidation extends Default {} // (1)
import jakarta.validation.groups.Default

interface FinalValidation extends Default {} // (1)
import jakarta.validation.groups.Default

interface FinalValidation : Default {} // (1)
1 Define a custom validation group. This one extends Default so any validations done with this group will include constraints in the Default group.
import io.micronaut.core.annotation.Introspected;

import jakarta.validation.constraints.NotBlank;

@Introspected
public class Email {

    @NotBlank // (1)
    String subject;

    @NotBlank(groups = FinalValidation.class) // (2)
    String recipient;

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getRecipient() {
        return recipient;
    }

    public void setRecipient(String recipient) {
        this.recipient = recipient;
    }
}
import io.micronaut.core.annotation.Introspected

import jakarta.validation.constraints.NotBlank

@Introspected
class Email {

    @NotBlank // (1)
    String subject

    @NotBlank(groups = FinalValidation) // (2)
    String recipient
}
import io.micronaut.core.annotation.Introspected
import jakarta.validation.constraints.NotBlank

@Introspected
open class Email {

    @NotBlank // (1)
    var subject: String? = null

    @NotBlank(groups = [FinalValidation::class]) // (2)
    var recipient: String? = null
}
1 Specify a constraint using the Default validation group. This constraint will only be enforced when Default is active.
2 Specify a constraint using the custom FinalValidation validation group. This constraint will only be enforced when FinalValidation is active.

Annotate your controller with Validated, specifying the validation groups that will be active or letting it default to Default. Also annotate the binding POJO with @Valid.

Example
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.validation.Validated;

import jakarta.validation.Valid;
import java.util.Collections;

@Validated // (1)
@Controller("/email")
public class EmailController {

    @Post("/createDraft")
    public HttpResponse createDraft(@Body @Valid Email email) { // (2)
        return HttpResponse.ok(Collections.singletonMap("msg", "OK"));
    }

    @Post("/send")
    @Validated(groups = FinalValidation.class) // (3)
    public HttpResponse send(@Body @Valid Email email) { // (4)
        return HttpResponse.ok(Collections.singletonMap("msg", "OK"));
    }
}
Example
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.validation.Validated

import jakarta.validation.Valid

@Validated // (1)
@Controller("/email")
class EmailController {

    @Post("/createDraft")
    HttpResponse createDraft(@Body @Valid Email email) { // (2)
        HttpResponse.ok(msg: "OK")
    }

    @Post("/send")
    @Validated(groups = [FinalValidation]) // (3)
    HttpResponse send(@Body @Valid Email email) { // (4)
        HttpResponse.ok(msg: "OK")
    }
}
Example
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.validation.Validated
import jakarta.validation.Valid

@Validated // (1)
@Controller("/email")
open class EmailController {

    @Post("/createDraft")
    open fun createDraft(@Body @Valid email: Email): HttpResponse<*> { // (2)
        return HttpResponse.ok(mapOf("msg" to "OK"))
    }

    @Post("/send")
    @Validated(groups = [FinalValidation::class]) // (3)
    open fun send(@Body @Valid email: Email): HttpResponse<*> { // (4)
        return HttpResponse.ok(mapOf("msg" to "OK"))
    }
}
1 Annotating with Validated without specifying groups means that the Default group will be active. Since this is defined on the class, it will apply to all methods.
2 Constraints in the Default validation group will be enforced, inheriting from the class. The effect is that @NotBlank on email.recipient will not be enforced when this method is called.
3 Specifying groups means that these validation groups will be enforced when this method is called. Note that FinalValidation extends Default so constraints from both groups will be enforced.
4 Constraints in the Default and FinalValidation validation groups will be enforced, since FinalValidation extends Default. The effect is that both @NotBlank constraints in email will be enforced when this method is called.

Validation of POJOs using the default validation group is shown in the following test:

@Test
void testPojoValidation_defaultGroup() {
    HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> {
        Email email = new Email();
        email.subject = "";
        email.recipient = "";
        client.toBlocking().exchange(HttpRequest.POST("/email/createDraft", email));
    });
    HttpResponse<?> response = e.getResponse();

    assertEquals(HttpStatus.BAD_REQUEST, response.getStatus());

    Email email = new Email();
    email.subject = "Hi";
    email.recipient = "";
    response = client.toBlocking().exchange(HttpRequest.POST("/email/createDraft", email));

    assertEquals(HttpStatus.OK, response.getStatus());
}
def "invoking /email/createDraft parse parameters in a POJO and validates using default validation groups"() {
    when:
    Email email = new Email(subject: '', recipient: '')
    client.toBlocking().exchange(HttpRequest.POST('/email/createDraft', email))

    then:
    def e = thrown(HttpClientResponseException)
    def response = e.response
    response.status == HttpStatus.BAD_REQUEST

    when:
    email = new Email(subject: 'Hi', recipient: '')
    response = client.toBlocking().exchange(HttpRequest.POST('/email/createDraft', email))

    then:
    response.status == HttpStatus.OK
}
"test pojo validation using default validation groups" {
    val e = shouldThrow<HttpClientResponseException> {
        val email = Email()
        email.subject = ""
        email.recipient = ""
        client.toBlocking().exchange<Email, Any>(HttpRequest.POST("/email/createDraft", email))
    }
    var response = e.response

    response.status shouldBe HttpStatus.BAD_REQUEST

    val email = Email()
    email.subject = "Hi"
    email.recipient = ""
    response = client.toBlocking().exchange<Email, Any>(HttpRequest.POST("/email/createDraft", email))

    response.status shouldBe HttpStatus.OK
}

Validation of POJOs using the custom FinalValidation validation group is shown in the following test:

@Test
void testPojoValidation_finalValidationGroup() {
    HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> {
        Email email = new Email();
        email.subject = "Hi";
        email.recipient = "";
        client.toBlocking().exchange(HttpRequest.POST("/email/send", email));
    });
    HttpResponse<?> response = e.getResponse();

    assertEquals(HttpStatus.BAD_REQUEST, response.getStatus());

    Email email = new Email();
    email.subject = "Hi";
    email.recipient = "me@micronaut.example";
    response = client.toBlocking().exchange(HttpRequest.POST("/email/send", email));

    assertEquals(HttpStatus.OK, response.getStatus());
}
def "invoking /email/send parse parameters in a POJO and validates using FinalValidation validation group"() {
    when:
    Email email = new Email(subject: 'Hi', recipient: '')
    client.toBlocking().exchange(HttpRequest.POST('/email/send', email))

    then:
    def e = thrown(HttpClientResponseException)
    def response = e.response
    response.status == HttpStatus.BAD_REQUEST

    when:
    email = new Email(subject: 'Hi', recipient: 'me@micronaut.example')
    response = client.toBlocking().exchange(HttpRequest.POST('/email/send', email))

    then:
    response.status == HttpStatus.OK
}
"test pojo validation using FinalValidation validation group" {
    val e = shouldThrow<HttpClientResponseException> {
        val email = Email()
        email.subject = "Hi"
        email.recipient = ""
        client.toBlocking().exchange<Email, Any>(HttpRequest.POST("/email/send", email))
    }
    var response = e.response

    response.status shouldBe HttpStatus.BAD_REQUEST

    val email = Email()
    email.subject = "Hi"
    email.recipient = "me@micronaut.example"
    response = client.toBlocking().exchange<Email, Any>(HttpRequest.POST("/email/send", email))

    response.status shouldBe HttpStatus.OK
}

6.17 Serving Static Resources

Static resource resolution is enabled by default. The Micronaut framework supports resolving resources from the classpath or the file system.

See the information below for available configuration options:

🔗
Table 1. Configuration Properties for StaticResourceConfiguration
Property Type Description

micronaut.router.static-resources.*.enabled

boolean

micronaut.router.static-resources.*.mapping

java.lang.String

The static resource mapping.

micronaut.router.static-resources.*.paths

java.util.List

A list of paths either starting with classpath: or file:. You can serve files from anywhere on disk or the classpath. For example to serve static resources from src/main/resources/public, you would use classpath:public as the path.

Read the Serving static resources in a Micronaut Application guide, a step-by-step tutorial, to learn how to expose static resources such as CSS or images in a Micronaut Framework application.

6.18 Error Handling

Sometimes with distributed applications, bad things happen. Having a good way to handle errors is important.

6.18.1 Status Handlers

The @Error annotation supports defining either an exception class or an HTTP status. Methods annotated with @Error must be defined within a class annotated with @Controller. The annotation also supports the notion of global and local, local being the default.

Local error handlers only respond to exceptions thrown as a result of the route being matched to another method in the same controller. Global error handlers can be invoked as a result of any thrown exception. A local error handler is always searched for first when resolving which handler to execute.

When defining an error handler for an exception, you can specify the exception instance as an argument to the method and omit the exception property of the annotation.
See the guide for Error Handling to learn more.

6.18.2 Local Error Handling

For example, the following method handles JSON parse exceptions from Jackson for the scope of the declaring controller:

Local exception handler
@Error
public HttpResponse<JsonError> jsonError(HttpRequest request, JsonSyntaxException e) { // (1)
    JsonError error = new JsonError("Invalid JSON: " + e.getMessage()) // (2)
            .link(Link.SELF, Link.of(request.getUri()));

    return HttpResponse.<JsonError>status(HttpStatus.BAD_REQUEST, "Fix Your JSON")
            .body(error); // (3)
}
Local exception handler
@Error
HttpResponse<JsonError> jsonError(HttpRequest request, JsonSyntaxException e) { // (1)
    JsonError error = new JsonError("Invalid JSON: " + e.message) // (2)
            .link(Link.SELF, Link.of(request.uri))

    HttpResponse.<JsonError>status(HttpStatus.BAD_REQUEST, "Fix Your JSON")
            .body(error) // (3)
}
Local exception handler
@Error
fun jsonError(request: HttpRequest<*>, e: JsonSyntaxException): HttpResponse<JsonError> { // (1)
    val error = JsonError("Invalid JSON: ${e.message}") // (2)
            .link(Link.SELF, Link.of(request.uri))

    return HttpResponse.status<JsonError>(HttpStatus.BAD_REQUEST, "Fix Your JSON")
            .body(error) // (3)
}
1 A method that explicitly handles JsonSyntaxException is declared
2 An instance of JsonError is returned.
3 A custom response is returned to handle the error
Local status handler
@Error(status = HttpStatus.NOT_FOUND)
public HttpResponse<JsonError> notFound(HttpRequest request) { // (1)
    JsonError error = new JsonError("Person Not Found") // (2)
            .link(Link.SELF, Link.of(request.getUri()));

    return HttpResponse.<JsonError>notFound()
            .body(error); // (3)
}
Local status handler
@Error(status = HttpStatus.NOT_FOUND)
HttpResponse<JsonError> notFound(HttpRequest request) { // (1)
    JsonError error = new JsonError("Person Not Found") // (2)
            .link(Link.SELF, Link.of(request.uri))

    HttpResponse.<JsonError>notFound()
            .body(error) // (3)
}
Local status handler
@Error(status = HttpStatus.NOT_FOUND)
fun notFound(request: HttpRequest<*>): HttpResponse<JsonError> { // (1)
    val error = JsonError("Person Not Found") // (2)
            .link(Link.SELF, Link.of(request.uri))

    return HttpResponse.notFound<JsonError>()
            .body(error) // (3)
}
1 The Error declares which HttpStatus error code to handle (in this case 404)
2 A JsonError instance is returned for all 404 responses
3 An NOT_FOUND response is returned

Similar to other controller methods, error handlers can use request binding annotations on parameters e.g. to access header values. However, binding the request body comes with additional restrictions depending on the HTTP server implementation used. If the body has already been bound to a parameter of the original controller method, it may not be possible to bind the body to a different type, as the original bytes may already have been discarded.

6.18.3 Global Error Handling

Global error handler
@Error(global = true) // (1)
public HttpResponse<JsonError> error(HttpRequest request, Throwable e) {
    JsonError error = new JsonError("Bad Things Happened: " + e.getMessage()) // (2)
            .link(Link.SELF, Link.of(request.getUri()));

    return HttpResponse.<JsonError>serverError()
            .body(error); // (3)
}
Global error handler
@Error(global = true) // (1)
HttpResponse<JsonError> error(HttpRequest request, Throwable e) {
    JsonError error = new JsonError("Bad Things Happened: " + e.message) // (2)
            .link(Link.SELF, Link.of(request.uri))

    HttpResponse.<JsonError>serverError()
            .body(error) // (3)
}
Global error handler
@Error(global = true) // (1)
fun error(request: HttpRequest<*>, e: Throwable): HttpResponse<JsonError> {
    val error = JsonError("Bad Things Happened: ${e.message}") // (2)
            .link(Link.SELF, Link.of(request.uri))

    return HttpResponse.serverError<JsonError>()
            .body(error) // (3)
}
1 The @Error declares the method a global error handler
2 A JsonError instance is returned for all errors
3 An INTERNAL_SERVER_ERROR response is returned
Global status handler
@Error(status = HttpStatus.NOT_FOUND)
public HttpResponse<JsonError> notFound(HttpRequest request) { // (1)
    JsonError error = new JsonError("Person Not Found") // (2)
            .link(Link.SELF, Link.of(request.getUri()));

    return HttpResponse.<JsonError>notFound()
            .body(error); // (3)
}
Global status handler
@Error(status = HttpStatus.NOT_FOUND)
HttpResponse<JsonError> notFound(HttpRequest request) { // (1)
    JsonError error = new JsonError("Person Not Found") // (2)
            .link(Link.SELF, Link.of(request.uri))

    HttpResponse.<JsonError>notFound()
            .body(error) // (3)
}
Global status handler
@Error(status = HttpStatus.NOT_FOUND)
fun notFound(request: HttpRequest<*>): HttpResponse<JsonError> { // (1)
    val error = JsonError("Person Not Found") // (2)
            .link(Link.SELF, Link.of(request.uri))

    return HttpResponse.notFound<JsonError>()
            .body(error) // (3)
}
1 The @Error declares which HttpStatus error code to handle (in this case 404)
2 A JsonError instance is returned for all 404 responses
3 An NOT_FOUND response is returned
A few things to note about the @Error annotation. You cannot declare identical global @Error annotations. Identical non-global @Error annotations cannot be declared in the same controller. If an @Error annotation with the same parameter exists as global and another as local, the local one takes precedence.

6.18.4 ExceptionHandler

Alternatively, you can implement an ExceptionHandler, a generic hook for handling exceptions that occur during execution of an HTTP request.

An @Error annotation capturing an exception has precedence over an implementation of ExceptionHandler capturing the same exception.

6.18.4.1 Built-In Exception Handlers

The Micronaut framework ships with several built-in handlers:

Exception

Handler

jakarta.validation.ConstraintViolationException

ConstraintExceptionHandler

ContentLengthExceededException

ContentLengthExceededHandler

ConversionErrorException

ConversionErrorHandler

DuplicateRouteException

DuplicateRouteHandler

HttpStatusException

HttpStatusHandler

com.fasterxml.jackson.core.JsonProcessingException

JsonExceptionHandler

java.net.URISyntaxException

URISyntaxHandler

UnsatisfiedArgumentException

UnsatisfiedArgumentHandler

UnsatisfiedRouteException

UnsatisfiedRouteHandler

org.grails.datastore.mapping.validation.ValidationException

ValidationExceptionHandler

6.18.4.2 Custom Exception Handler

Imagine your e-commerce app throws an OutOfStockException when a book is out of stock:

public class OutOfStockException extends RuntimeException {
}
class OutOfStockException extends RuntimeException {
}
class OutOfStockException : RuntimeException()

Along with BookController:

@Controller("/books")
public class BookController {

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/stock/{isbn}")
    Integer stock(String isbn) {
        throw new OutOfStockException();
    }
}
@Controller("/books")
class BookController {

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/stock/{isbn}")
    Integer stock(String isbn) {
        throw new OutOfStockException()
    }
}
@Controller("/books")
class BookController {

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/stock/{isbn}")
    internal fun stock(isbn: String): Int? {
        throw OutOfStockException()
    }
}

The server returns a 500 (Internal Server Error) status code if you don’t handle the exception.

To respond with 400 Bad Request as the response when the OutOfStockException is thrown, you can register a ExceptionHandler:

@Produces
@Singleton
@Requires(classes = {OutOfStockException.class, ExceptionHandler.class})
public class OutOfStockExceptionHandler implements ExceptionHandler<OutOfStockException, HttpResponse> {

    private final ErrorResponseProcessor<?> errorResponseProcessor;

    public OutOfStockExceptionHandler(ErrorResponseProcessor<?> errorResponseProcessor) {
        this.errorResponseProcessor = errorResponseProcessor;
    }

    @Override
    public HttpResponse handle(HttpRequest request, OutOfStockException e) {
        return errorResponseProcessor.processResponse(ErrorContext.builder(request)
                .cause(e)
                .errorMessage("No stock available")
                .build(), HttpResponse.badRequest()); // (1)
    }
}
@Produces
@Singleton
@Requires(classes = [OutOfStockException, ExceptionHandler])
class OutOfStockExceptionHandler implements ExceptionHandler<OutOfStockException, HttpResponse> {

    private final ErrorResponseProcessor<?> errorResponseProcessor

    OutOfStockExceptionHandler(ErrorResponseProcessor<?> errorResponseProcessor) {
        this.errorResponseProcessor = errorResponseProcessor
    }

    @Override
    HttpResponse handle(HttpRequest request, OutOfStockException e) {
        errorResponseProcessor.processResponse(ErrorContext.builder(request)
                .cause(e)
                .errorMessage("No stock available")
                .build(), HttpResponse.badRequest()) // (1)
    }
}
@Produces
@Singleton
@Requirements(
    Requires(classes = [OutOfStockException::class, ExceptionHandler::class])
)
class OutOfStockExceptionHandler(private val errorResponseProcessor: ErrorResponseProcessor<Any>) :
    ExceptionHandler<OutOfStockException, HttpResponse<*>> {

    override fun handle(request: HttpRequest<*>, exception: OutOfStockException): HttpResponse<*> {
        return errorResponseProcessor.processResponse(
                ErrorContext.builder(request)
                    .cause(exception)
                    .errorMessage("No stock available")
                    .build(), HttpResponse.badRequest<Any>()) // (1)
    }
}
1 The default ErrorResponseProcessor is used to create the body of the response

6.18.5 Error Formatting

The Micronaut framework produces error response bodies via beans of type ErrorResponseProcessor.

The default response body is vnd.error, however you can create your own implementation of type ErrorResponseProcessor to control the responses.

If customization of the response other than items related to the errors is desired, the exception handler that is handling the exception needs to be overridden.

6.19 API Versioning

Since 1.1.x, the Micronaut framework supports API versioning via a dedicated @Version annotation.

The following example demonstrates how to version an API:

Versioning an API
import io.micronaut.core.version.annotation.Version;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/versioned")
class VersionedController {

    @Version("1") // (1)
    @Get("/hello")
    String helloV1() {
        return "helloV1";
    }

    @Version("2") // (2)
    @Get("/hello")
    String helloV2() {
        return "helloV2";
    }
Versioning an API
import io.micronaut.core.version.annotation.Version
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/versioned")
class VersionedController {

    @Version("1") // (1)
    @Get("/hello")
    String helloV1() {
        "helloV1"
    }

    @Version("2") // (2)
    @Get("/hello")
    String helloV2() {
        "helloV2"
    }
Versioning an API
import io.micronaut.core.version.annotation.Version
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/versioned")
internal class VersionedController {

    @Version("1") // (1)
    @Get("/hello")
    fun helloV1(): String {
        return "helloV1"
    }

    @Version("2") // (2)
    @Get("/hello")
    fun helloV2(): String {
        return "helloV2"
    }
1 The helloV1 method is declared as version 1
2 The helloV2 method is declared as version 2

Then enable versioning by setting micronaut.router.versioning.enabled to true in your configuration file (e.g application.yml):

Enabling Versioning
micronaut.router.versioning.enabled=true
micronaut:
  router:
    versioning:
      enabled: true
[micronaut]
  [micronaut.router]
    [micronaut.router.versioning]
      enabled=true
micronaut {
  router {
    versioning {
      enabled = true
    }
  }
}
{
  micronaut {
    router {
      versioning {
        enabled = true
      }
    }
  }
}
{
  "micronaut": {
    "router": {
      "versioning": {
        "enabled": true
      }
    }
  }
}

By default, the Micronaut framework has two strategies for resolving the version based on an HTTP header named X-API-VERSION or a request parameter named api-version, however this is configurable. A full configuration example can be seen below:

Configuring Versioning
micronaut.router.versioning.enabled=true
micronaut.router.versioning.parameter.enabled=false
micronaut.router.versioning.parameter.names=v,api-version
micronaut.router.versioning.header.enabled=true
micronaut.router.versioning.header.names[0]=X-API-VERSION
micronaut.router.versioning.header.names[1]=Accept-Version
micronaut:
  router:
    versioning:
      enabled: true
      parameter:
        enabled: false
        names: 'v,api-version'
      header:
        enabled: true
        names:
          - 'X-API-VERSION'
          - 'Accept-Version'
[micronaut]
  [micronaut.router]
    [micronaut.router.versioning]
      enabled=true
      [micronaut.router.versioning.parameter]
        enabled=false
        names="v,api-version"
      [micronaut.router.versioning.header]
        enabled=true
        names=[
          "X-API-VERSION",
          "Accept-Version"
        ]
micronaut {
  router {
    versioning {
      enabled = true
      parameter {
        enabled = false
        names = "v,api-version"
      }
      header {
        enabled = true
        names = ["X-API-VERSION", "Accept-Version"]
      }
    }
  }
}
{
  micronaut {
    router {
      versioning {
        enabled = true
        parameter {
          enabled = false
          names = "v,api-version"
        }
        header {
          enabled = true
          names = ["X-API-VERSION", "Accept-Version"]
        }
      }
    }
  }
}
{
  "micronaut": {
    "router": {
      "versioning": {
        "enabled": true,
        "parameter": {
          "enabled": false,
          "names": "v,api-version"
        },
        "header": {
          "enabled": true,
          "names": ["X-API-VERSION", "Accept-Version"]
        }
      }
    }
  }
}
  • This example enables versioning

  • parameter.enabled enables or disables parameter-based versioning

  • parameter.names specifies the parameter names as a comma-separated list

  • header.enabled enables or disables header-based versioning

  • header.names specifies the header names as a list

If this is not enough you can also implement the RequestVersionResolver interface which receives the HttpRequest and can implement any strategy you choose.

Default Version

It is possible to supply a default version through configuration.

Configuring Default Version
micronaut.router.versioning.enabled=true
micronaut.router.versioning.default-version=3
micronaut:
  router:
    versioning:
      enabled: true
      default-version: 3
[micronaut]
  [micronaut.router]
    [micronaut.router.versioning]
      enabled=true
      default-version=3
micronaut {
  router {
    versioning {
      enabled = true
      defaultVersion = 3
    }
  }
}
{
  micronaut {
    router {
      versioning {
        enabled = true
        default-version = 3
      }
    }
  }
}
{
  "micronaut": {
    "router": {
      "versioning": {
        "enabled": true,
        "default-version": 3
      }
    }
  }
}
  • This example enables versioning and sets the default version

A route is not matched if the following conditions are met:

  • The default version is configured

  • No version is found in the request

  • The route defines a version

  • The route version does not match the default version

If the incoming request specifies a version, the default version has no effect.

Versioning Client Requests

Micronaut’s Declarative HTTP client also supports automatic versioning of outgoing requests via the @Version annotation.

By default, if you annotate a client interface with @Version, the value supplied to the annotation is included using the X-API-VERSION header.

For example:

import io.micronaut.core.version.annotation.Version;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import org.reactivestreams.Publisher;
import io.micronaut.core.async.annotation.SingleResult;

@Client("/hello")
@Version("1") // (1)
public  interface HelloClient {

    @Get("/greeting/{name}")
    String sayHello(String name);

    @Version("2")
    @Get("/greeting/{name}")
    @SingleResult
    Publisher<String> sayHelloTwo(String name); // (2)
}
import io.micronaut.core.version.annotation.Version
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import reactor.core.publisher.Mono


@Client("/hello")
@Version("1") // (1)
interface HelloClient {

    @Get("/greeting/{name}")
    String sayHello(String name)

    @Version("2")
    @Get("/greeting/{name}")
    Mono<String> sayHelloTwo(String name) // (2)
}
import io.micronaut.core.version.annotation.Version
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import reactor.core.publisher.Mono

@Client("/hello")
@Version("1") // (1)
interface HelloClient {

    @Get("/greeting/{name}")
    fun sayHello(name : String) : String

    @Version("2")
    @Get("/greeting/{name}")
    fun sayHelloTwo(name : String) : Mono<String>  // (2)
}
1 The @Version annotation can be used at the type level to specify the version to use for all methods
2 When defined at the method level it is used only for that method

The default behaviour for how the version is sent for each call can be configured with DefaultClientVersioningConfiguration:

🔗
Table 1. Configuration Properties for DefaultClientVersioningConfiguration
Property Type Description

micronaut.http.client.versioning.default.headers

java.util.List

micronaut.http.client.versioning.default.parameters

java.util.List

For example to use Accept-Version as the header name:

Configuring Client Versioning
micronaut.http.client.versioning.default.headers[0]=Accept-Version
micronaut.http.client.versioning.default.headers[1]=X-API-VERSION
micronaut:
  http:
    client:
      versioning:
        default:
          headers:
            - 'Accept-Version'
            - 'X-API-VERSION'
[micronaut]
  [micronaut.http]
    [micronaut.http.client]
      [micronaut.http.client.versioning]
        [micronaut.http.client.versioning.default]
          headers=[
            "Accept-Version",
            "X-API-VERSION"
          ]
micronaut {
  http {
    client {
      versioning {
        'default' {
          headers = ["Accept-Version", "X-API-VERSION"]
        }
      }
    }
  }
}
{
  micronaut {
    http {
      client {
        versioning {
          default {
            headers = ["Accept-Version", "X-API-VERSION"]
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "client": {
        "versioning": {
          "default": {
            "headers": ["Accept-Version", "X-API-VERSION"]
          }
        }
      }
    }
  }
}

The default key refers to the default configuration. You can specify client-specific configuration by using the value passed to @Client (typically the service ID). For example:

Configuring Versioning
micronaut.http.client.versioning.greeting-service.headers[0]=Accept-Version
micronaut.http.client.versioning.greeting-service.headers[1]=X-API-VERSION
micronaut:
  http:
    client:
      versioning:
        greeting-service:
          headers:
            - 'Accept-Version'
            - 'X-API-VERSION'
[micronaut]
  [micronaut.http]
    [micronaut.http.client]
      [micronaut.http.client.versioning]
        [micronaut.http.client.versioning.greeting-service]
          headers=[
            "Accept-Version",
            "X-API-VERSION"
          ]
micronaut {
  http {
    client {
      versioning {
        greetingService {
          headers = ["Accept-Version", "X-API-VERSION"]
        }
      }
    }
  }
}
{
  micronaut {
    http {
      client {
        versioning {
          greeting-service {
            headers = ["Accept-Version", "X-API-VERSION"]
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "client": {
        "versioning": {
          "greeting-service": {
            "headers": ["Accept-Version", "X-API-VERSION"]
          }
        }
      }
    }
  }
}

The above uses a key named greeting-service which can be used to configure a client annotated with @Client('greeting-service').

6.20 Handling Form Data

To make data binding model customizations consistent between form data and JSON, the Micronaut framework uses Jackson to implement binding data from form submissions.

The advantage of this approach is that the same Jackson annotations used for customizing JSON binding can be used for form submissions.

In practice this means that to bind regular form data, the only change required to the previous JSON binding code is updating the MediaType consumed:

Binding Form Data to POJOs
@Controller("/people")
public class PersonController {

    Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>();

@Post
public HttpResponse<Person> save(@Body Person person) {
    inMemoryDatastore.put(person.getFirstName(), person);
    return HttpResponse.created(person);
}

}
Binding Form Data to POJOs
@Controller("/people")
class PersonController {

    Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>()

@Post
HttpResponse<Person> save(@Body Person person) {
    inMemoryDatastore.put(person.getFirstName(), person)
    HttpResponse.created(person)
}

}
Binding Form Data to POJOs
@Controller("/people")
class PersonController {

    internal var inMemoryDatastore: MutableMap<String, Person> = ConcurrentHashMap()

@Post
fun save(@Body person: Person): HttpResponse<Person> {
    inMemoryDatastore[person.firstName] = person
    return HttpResponse.created(person)
}

}
To avoid denial of service attacks, collection types and arrays created during binding are limited by the setting jackson.arraySizeThreshold in your configuration file (e.g application.yml)

Alternatively, instead of using a POJO you can bind form data directly to method parameters (which works with JSON too!):

Binding Form Data to Parameters
@Controller("/people")
public class PersonController {

    Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>();

@Post("/saveWithArgs")
public HttpResponse<Person> save(String firstName, String lastName, Optional<Integer> age) {
    Person p = new Person(firstName, lastName);
    age.ifPresent(p::setAge);
    inMemoryDatastore.put(p.getFirstName(), p);
    return HttpResponse.created(p);
}

}
Binding Form Data to Parameters
@Controller("/people")
class PersonController {

    Map<String, Person> inMemoryDatastore = new ConcurrentHashMap<>()

@Post("/saveWithArgs")
HttpResponse<Person> save(String firstName, String lastName, Optional<Integer> age) {
    Person p = new Person(firstName, lastName)
    age.ifPresent({ a -> p.setAge(a)})
    inMemoryDatastore.put(p.getFirstName(), p)
    HttpResponse.created(p)
}

}
Binding Form Data to Parameters
@Controller("/people")
class PersonController {

    internal var inMemoryDatastore: MutableMap<String, Person> = ConcurrentHashMap()

@Post("/saveWithArgs")
fun save(firstName: String, lastName: String, age: Optional<Int>): HttpResponse<Person> {
    val p = Person(firstName, lastName)
    age.ifPresent { p.age = it }
    inMemoryDatastore[p.firstName] = p
    return HttpResponse.created(p)
}

}

As you can see from the example above, this approach lets you use features such as support for Optional types and restrict the parameters to be bound. When using POJOs you must be careful to use Jackson annotations to exclude properties that should not be bound.

6.21 Writing Response Data

Reactively Writing Response Data

Micronaut’s HTTP server supports writing chunks of response data by returning a Publisher that emits objects that can be encoded to the HTTP response.

The following table summarizes example return type signatures and the behaviour the server exhibits to handle them:

Return Type Description

Publisher<String>

A Publisher that emits each chunk of content as a String

Flux<byte[]>

A Flux that emits each chunk of content as a byte[] without blocking

Flux<ByteBuf>

A Reactor Flux that emits each chunk as a Netty ByteBuf

Flux<Book>

When emitting a POJO, each emitted object is encoded as JSON by default without blocking

Flowable<byte[]>

A Flux that emits each chunk of content as a byte[] without blocking

Flowable<ByteBuf>

A Reactor Flux that emits each chunk as a Netty ByteBuf

Flowable<Book>

When emitting a POJO, each emitted object is encoded as JSON by default without blocking

When returning a reactive type, the server uses a Transfer-Encoding of chunked and keeps writing data until the Publisher onComplete method is called.

The server requests a single item from the Publisher, writes it, and requests the next, controlling back pressure.

It is up to the implementation of the Publisher to schedule any blocking I/O work that may be done as a result of subscribing to the publisher.
To use Project Reactor's Flux or Mono you need to add the Micronaut Reactor dependency to your project to include the necessary converters.
To use RxJava's Flowable, Single or Maybe you need to add the Micronaut RxJava dependency to your project to include the necessary converters.

Performing Blocking I/O

In some cases you may wish to integrate a library that does not support non-blocking I/O.

Writable

In this case you can return a Writable object from any controller method. The Writable interface has various signatures that allow writing to traditional blocking streams like Writer or OutputStream.

When returning a Writable, the blocking I/O operation is shifted to the I/O thread pool so the Netty event loop is not blocked.

See the section on configuring Server Thread Pools for details on how to configure the I/O thread pool to meet your application requirements.

The following example demonstrates how to use this API with Groovy’s SimpleTemplateEngine to write a server side template:

Performing Blocking I/O With Writable
import groovy.text.SimpleTemplateEngine;
import groovy.text.Template;
import io.micronaut.core.io.Writable;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.server.exceptions.HttpServerException;

@Controller("/template")
public class TemplateController {

    private final SimpleTemplateEngine templateEngine = new SimpleTemplateEngine();
    private final Template template = initTemplate(); // (1)

    @Get(value = "/welcome", produces = MediaType.TEXT_PLAIN)
    Writable render() { // (2)
        return writer -> template.make( // (3)
            CollectionUtils.mapOf(
                    "firstName", "Fred",
                    "lastName", "Flintstone"
            )
        ).writeTo(writer);
    }

    private Template initTemplate() {
        try {
            return templateEngine.createTemplate(
                    "Dear $firstName $lastName. Nice to meet you."
            );
        } catch (Exception e) {
            throw new HttpServerException("Cannot create template");
        }
    }
}
Performing Blocking I/O With Writable
import groovy.text.SimpleTemplateEngine
import groovy.text.Template
import io.micronaut.core.io.Writable
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.server.exceptions.HttpServerException

@Controller("/template")
class TemplateController {

    private final SimpleTemplateEngine templateEngine = new SimpleTemplateEngine()
    private final Template template = initTemplate() // (1)

    @Get(value = "/welcome", produces = MediaType.TEXT_PLAIN)
    Writable render() { // (2)
        { writer ->
            template.make( // (3)
                    firstName: "Fred",
                    lastName: "Flintstone"
            ).writeTo(writer)
        }
    }

    private Template initTemplate() {
        try {
            return templateEngine.createTemplate(
                    'Dear $firstName $lastName. Nice to meet you.'
            )
        } catch (Exception e) {
            throw new HttpServerException("Cannot create template")
        }
    }
}
Performing Blocking I/O With Writable
import groovy.text.SimpleTemplateEngine
import groovy.text.Template
import io.micronaut.core.io.Writable
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.server.exceptions.HttpServerException
import java.io.Writer

@Controller("/template")
class TemplateController {

    private val templateEngine = SimpleTemplateEngine()
    private val template = initTemplate() // (1)

    @Get(value = "/welcome", produces = [MediaType.TEXT_PLAIN])
    internal fun render(): Writable { // (2)
        return { writer: Writer ->
            template.make( // (3)
                    mapOf(
                        "firstName" to "Fred",
                        "lastName" to "Flintstone"
                    )
            ).writeTo(writer)
        } as Writable
    }

    private fun initTemplate(): Template {
        return try {
            templateEngine.createTemplate(
                "Dear \$firstName \$lastName. Nice to meet you."
            )
        } catch (e: Exception) {
            throw HttpServerException("Cannot create template")
        }
    }
}
1 The controller creates a simple template
2 The controller method returns a Writable
3 The returned function receives a Writer and calls writeTo on the template.

InputStream

Another option is to return an input stream. This is useful for many scenarios that interact with other APIs that expose a stream.

Performing Blocking I/O With InputStream
@Get(value = "/write", produces = MediaType.TEXT_PLAIN)
InputStream write() {
    byte[] bytes = "test".getBytes(StandardCharsets.UTF_8);
    return new ByteArrayInputStream(bytes); // (1)
}
Performing Blocking I/O With InputStream
@Get(value = "/write", produces = MediaType.TEXT_PLAIN)
InputStream write() {
    byte[] bytes = "test".getBytes(StandardCharsets.UTF_8);
    new ByteArrayInputStream(bytes) // (1)
}
Performing Blocking I/O With InputStream
@Get(value = "/write", produces = [MediaType.TEXT_PLAIN])
fun write(): InputStream {
    val bytes = "test".toByteArray(StandardCharsets.UTF_8)
    return ByteArrayInputStream(bytes) // (1)
}
1 The input stream is returned and its contents will be the response body
The reading of the stream will be offloaded to the IO thread pool if the controller method is executed on the event loop.

404 Responses

Often, you want to respond 404 (Not Found) when you don’t find an item in your persistence layer or in similar scenarios.

See the following example:

@Controller("/books")
public class BooksController {

    @Get("/stock/{isbn}")
    public Map stock(String isbn) {
        return null; //(1)
    }

    @Get("/maybestock/{isbn}")
    @SingleResult
    public Publisher<Map> maybestock(String isbn) {
        return Mono.empty(); //(2)
    }
}
@Controller("/books")
class BooksController {

    @Get("/stock/{isbn}")
    Map stock(String isbn) {
        null //(1)
    }

    @Get("/maybestock/{isbn}")
    Mono<Map> maybestock(String isbn) {
        Mono.empty() //(2)
    }
}
@Controller("/books")
class BooksController {

    @Get("/stock/{isbn}")
    fun stock(isbn: String): Map<*, *>? {
        return null //(1)
    }

    @Get("/maybestock/{isbn}")
    fun maybestock(isbn: String): Mono<Map<*, *>> {
        return Mono.empty() //(2)
    }
}
1 Returning null triggers a 404 (Not Found) response.
2 Returning an empty Mono triggers a 404 (Not Found) response.
Responding with an empty Publisher or Flux results in an empty array being returned if the content type is JSON.

6.22 File Uploads

Handling of file uploads has special treatment in Micronaut. Support is provided for streaming of uploads in a non-blocking manner through streaming uploads or completed uploads.

To receive data from a multipart request, set the consumes argument of the method annotation to MULTIPART_FORM_DATA. For example:

@Post(consumes = MediaType.MULTIPART_FORM_DATA)
HttpResponse upload( ... )

Route Arguments

Method argument types determine how files are received. Data can be received a chunk at a time or when an upload is completed.

If the route argument name cannot or should not match the name of the part in the request, add the Part annotation to the argument and specify the expected name in the request.

Chunk Data Types

PartData represents a chunk of data received in a multipart request. PartData interface methods convert the data to a byte[], InputStream, or a ByteBuffer.

Data can only be retrieved from a PartData once. The underlying buffer is released, causing further attempts to fail.

Route arguments of type Publisher are treated as intended to receive a single file, and each chunk of the received file will be sent downstream. If the generic type is other than PartData, conversion will be attempted using Micronaut’s conversion service. Conversions to String and byte[] are supported by default.

If you need knowledge about the metadata of an uploaded file, the StreamingFileUpload class is a Publisher that also has file information such as the content type and file name.

Streaming file upload
import io.micronaut.core.async.annotation.SingleResult;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.multipart.StreamingFileUpload;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

import static io.micronaut.http.HttpStatus.CONFLICT;
import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA;
import static io.micronaut.http.MediaType.TEXT_PLAIN;

@Controller("/upload")
public class UploadController {

    @Post(value = "/", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // (1)
    @SingleResult
    public Publisher<HttpResponse<String>> upload(StreamingFileUpload file) { // (2)

        File tempFile;
        try {
            tempFile = File.createTempFile(file.getFilename(), "temp");
        } catch (IOException e) {
            return Mono.error(e);
        }
        Publisher<Boolean> uploadPublisher = file.transferTo(tempFile); // (3)

        return Mono.from(uploadPublisher)  // (4)
            .map(success -> {
                if (success) {
                    return HttpResponse.ok("Uploaded");
                } else {
                    return HttpResponse.<String>status(CONFLICT)
                                       .body("Upload Failed");
                }
            });
    }

}
Streaming file upload
import io.micronaut.core.async.annotation.SingleResult
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.multipart.StreamingFileUpload
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono

import static io.micronaut.http.HttpStatus.CONFLICT
import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA
import static io.micronaut.http.MediaType.TEXT_PLAIN

@Controller("/upload")
class UploadController {

    @Post(value = "/", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // (1)
    Mono<HttpResponse<String>> upload(StreamingFileUpload file) { // (2)

        File tempFile = File.createTempFile(file.filename, "temp")
        Publisher<Boolean> uploadPublisher = file.transferTo(tempFile) // (3)

        Mono.from(uploadPublisher)  // (4)
            .map({ success ->
                if (success) {
                    HttpResponse.ok("Uploaded")
                } else {
                    HttpResponse.<String>status(CONFLICT)
                            .body("Upload Failed")
                }
            })
    }

}
Streaming file upload
import io.micronaut.core.async.annotation.SingleResult
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus.CONFLICT
import io.micronaut.http.MediaType.MULTIPART_FORM_DATA
import io.micronaut.http.MediaType.TEXT_PLAIN
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.multipart.StreamingFileUpload
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.OutputStream

@Controller("/upload")
class UploadController {

    @Post(value = "/", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // (1)
    fun upload(file: StreamingFileUpload): Mono<HttpResponse<String>> { // (2)

        val tempFile = File.createTempFile(file.filename, "temp")
        val uploadPublisher = file.transferTo(tempFile) // (3)

        return Mono.from(uploadPublisher)  // (4)
            .map { success ->
                if (success) {
                    HttpResponse.ok("Uploaded")
                } else {
                    HttpResponse.status<String>(CONFLICT)
                        .body("Upload Failed")
                }
            }
    }

}
1 The method consumes MULTIPART_FORM_DATA
2 The method parameters match form attribute names. In this case file will match for example an <input type="file" name="file">
3 The StreamingFileUpload.transferTo(File) method transfers the file to the server. The method returns a Publisher
4 The returned Mono subscribes to the Publisher and outputs a response once the upload is complete, without blocking.

It is also possible to pass an output stream with the transferTo method.

The reading of the file or stream will be offloaded to the IO thread pool to prevent the possibility of blocking the event loop.
Streaming file upload
import io.micronaut.core.async.annotation.SingleResult;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.multipart.StreamingFileUpload;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

import static io.micronaut.http.HttpStatus.CONFLICT;
import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA;
import static io.micronaut.http.MediaType.TEXT_PLAIN;

@Controller("/upload")
public class UploadController {

    @Post(value = "/outputStream", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // (1)
    @SingleResult
    public Mono<HttpResponse<String>> uploadOutputStream(StreamingFileUpload file) { // (2)

        OutputStream outputStream = new ByteArrayOutputStream(); // (3)

        Publisher<Boolean> uploadPublisher = file.transferTo(outputStream); // (4)

        return Mono.from(uploadPublisher)  // (5)
                .map(success -> {
                    if (success) {
                        return HttpResponse.ok("Uploaded");
                    } else {
                        return HttpResponse.<String>status(CONFLICT)
                                .body("Upload Failed");
                    }
                });
    }

}
Streaming file upload
import io.micronaut.core.async.annotation.SingleResult
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.multipart.StreamingFileUpload
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono

import static io.micronaut.http.HttpStatus.CONFLICT
import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA
import static io.micronaut.http.MediaType.TEXT_PLAIN

@Controller("/upload")
class UploadController {

    @Post(value = "/outputStream", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // (1)
    @SingleResult
    Mono<HttpResponse<String>> uploadOutputStream(StreamingFileUpload file) { // (2)

        OutputStream outputStream = new ByteArrayOutputStream() // (3)

        Publisher<Boolean> uploadPublisher = file.transferTo(outputStream) // (4)

        Mono.from(uploadPublisher)  // (5)
                .map({ success ->
                    if (success) {
                        HttpResponse.ok("Uploaded")
                    } else {
                        HttpResponse.<String>status(CONFLICT)
                                .body("Upload Failed")
                    }
                })
    }

}
Streaming file upload
import io.micronaut.core.async.annotation.SingleResult
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus.CONFLICT
import io.micronaut.http.MediaType.MULTIPART_FORM_DATA
import io.micronaut.http.MediaType.TEXT_PLAIN
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.multipart.StreamingFileUpload
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.OutputStream

@Controller("/upload")
class UploadController {

    @Post(value = "/outputStream", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // (1)
    @SingleResult
    fun uploadOutputStream(file: StreamingFileUpload): Mono<HttpResponse<String>> { // (2)
        val outputStream  = ByteArrayOutputStream() // (3)
        val uploadPublisher = file.transferTo(outputStream) // (4)

        return Mono.from(uploadPublisher) // (5)
            .map { success: Boolean ->
                return@map if (success) {
                    HttpResponse.ok("Uploaded")
                } else {
                    HttpResponse.status<String>(CONFLICT)
                        .body("Upload Failed")
                }
            }
    }

}
1 The method consumes MULTIPART_FORM_DATA
2 The method parameters match form attribute names. In this case file will match for example an <input type="file" name="file">
3 A stream is created to output the data to. In real world scenarios this would come from some other source.
4 The StreamingFileUpload.transferTo(OutputStream) method transfers the file to the server. The method returns a Publisher
5 The returned Mono subscribes to the Publisher and outputs a response once the upload is complete, without blocking.

Whole Data Types

Route arguments that are not publishers cause route execution to be delayed until the upload has finished. The received data will attempt to be converted to the requested type. Conversions to a String or byte[] are supported by default. In addition, the file can be converted to a POJO if a media type codec is registered that supports the media type of the file. A media type codec is included by default that allows conversion of JSON files to POJOs.

Receiving a byte array
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA;
import static io.micronaut.http.MediaType.TEXT_PLAIN;

@Controller("/upload")
public class BytesUploadController {

    @Post(value = "/bytes", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // (1)
    public HttpResponse<String> uploadBytes(byte[] file, String fileName) { // (2)
        try {
            File tempFile = File.createTempFile(fileName, "temp");
            Path path = Paths.get(tempFile.getAbsolutePath());
            Files.write(path, file); // (3)
            return HttpResponse.ok("Uploaded");
        } catch (IOException e) {
            return HttpResponse.badRequest("Upload Failed");
        }
    }
}
Receiving a byte array
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post

import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA
import static io.micronaut.http.MediaType.TEXT_PLAIN

@Controller("/upload")
class BytesUploadController {

    @Post(value = "/bytes", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // (1)
    HttpResponse<String> uploadBytes(byte[] file, String fileName) { // (2)
        try {
            File tempFile = File.createTempFile(fileName, "temp")
            Path path = Paths.get(tempFile.absolutePath)
            Files.write(path, file) // (3)
            HttpResponse.ok("Uploaded")
        } catch (IOException e) {
            HttpResponse.badRequest("Upload Failed")
        }
    }
}
Receiving a byte array
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType
import io.micronaut.http.MediaType.MULTIPART_FORM_DATA
import io.micronaut.http.MediaType.TEXT_PLAIN
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import java.io.File
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths

@Controller("/upload")
class BytesUploadController {

    @Post(value = "/bytes", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // (1)
    fun uploadBytes(file: ByteArray, fileName: String): HttpResponse<String> { // (2)
        return try {
            val tempFile = File.createTempFile(fileName, "temp")
            val path = Paths.get(tempFile.absolutePath)
            Files.write(path, file) // (3)
            HttpResponse.ok("Uploaded")
        } catch (e: IOException) {
            HttpResponse.badRequest("Upload Failed")
        }
    }
}

If you need knowledge about the metadata of an uploaded file, the CompletedFileUpload class has methods to retrieve the data of the file, and also file information such as the content type and file name.

File upload with metadata
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.multipart.CompletedFileUpload;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA;
import static io.micronaut.http.MediaType.TEXT_PLAIN;

@Controller("/upload")
public class CompletedUploadController {

    @Post(value = "/completed", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // (1)
    public HttpResponse<String> uploadCompleted(CompletedFileUpload file) { // (2)
        try {
            File tempFile = File.createTempFile(file.getFilename(), "temp"); //(3)
            Path path = Paths.get(tempFile.getAbsolutePath());
            Files.write(path, file.getBytes()); //(3)
            return HttpResponse.ok("Uploaded");
        } catch (IOException e) {
            return HttpResponse.badRequest("Upload Failed");
        }
    }
}
File upload with metadata
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.multipart.CompletedFileUpload

import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA
import static io.micronaut.http.MediaType.TEXT_PLAIN

@Controller("/upload")
class CompletedUploadController {

    @Post(value = "/completed", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // (1)
    HttpResponse<String> uploadCompleted(CompletedFileUpload file) { // (2)
        try {
            File tempFile = File.createTempFile(file.filename, "temp") //(3)
            Path path = Paths.get(tempFile.absolutePath)
            Files.write(path, file.bytes) //(3)
            HttpResponse.ok("Uploaded")
        } catch (IOException e) {
            HttpResponse.badRequest("Upload Failed")
        }
    }
}
File upload with metadata
import io.micronaut.http.HttpResponse
import io.micronaut.http.MediaType.MULTIPART_FORM_DATA
import io.micronaut.http.MediaType.TEXT_PLAIN
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.multipart.CompletedFileUpload
import java.io.File
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths

@Controller("/upload")
class CompletedUploadController {

    @Post(value = "/completed", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // (1)
    fun uploadCompleted(file: CompletedFileUpload): HttpResponse<String> { // (2)
        return try {
            val tempFile = File.createTempFile(file.filename, "temp") //(3)
            val path = Paths.get(tempFile.absolutePath)
            Files.write(path, file.bytes) //(3)
            HttpResponse.ok("Uploaded")
        } catch (e: IOException) {
            HttpResponse.badRequest("Upload Failed")
        }
    }
}
1 The method consumes MULTIPART_FORM_DATA
2 The method parameters match form attribute names. In this case the file will match for example an <input type="file" name="file">
3 The CompletedFileUpload instance gives access to metadata about the upload as well as access to the file contents.
If a file will not be read, the discard method on the file object must be called to prevent memory leaks.

Multiple Uploads

Different Names

If a multipart request has multiple uploads that have different part names, create an argument to your route that receives each part. For example:

HttpResponse upload(String title, String name)

A route method signature like the above expects two different parts, one named "title" and the other "name".

Same Name

To receive multiple parts with the same part name, the argument must be a Publisher. When used in one of the following ways, the publisher emits one item per part found with the specified name. The publisher must accept one of the following types:

For example:

HttpResponse upload(Publisher<StreamingFileUpload> files)
HttpResponse upload(Publisher<CompletedFileUpload> files)
HttpResponse upload(Publisher<MyObject> files)
HttpResponse upload(Publisher<Publisher<PartData>> files)
HttpResponse upload(Publisher<CompletedPart> attributes)

Whole Body Binding

When request part names aren’t known ahead of time, or to read the entire body, a special type can be used to indicate the entire body is desired.

If a route has an argument of type MultipartBody (not to be confused with the class for the client) annotated with @Body, each part of the request will be emitted through the argument. A MultipartBody is a publisher of CompletedPart instances.

For example:

Binding to the entire multipart body
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.multipart.CompletedFileUpload;
import io.micronaut.http.multipart.CompletedPart;
import io.micronaut.http.server.multipart.MultipartBody;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.publisher.Mono;
import io.micronaut.core.async.annotation.SingleResult;
import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA;
import static io.micronaut.http.MediaType.TEXT_PLAIN;

@Controller("/upload")
public class WholeBodyUploadController {

    @Post(value = "/whole-body", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // (1)
    @SingleResult
    public Publisher<String> uploadBytes(@Body MultipartBody body) { // (2)

        return Mono.create(emitter -> {
            body.subscribe(new Subscriber<CompletedPart>() {
                private Subscription s;

                @Override
                public void onSubscribe(Subscription s) {
                    this.s = s;
                    s.request(1);
                }

                @Override
                public void onNext(CompletedPart completedPart) {
                    String partName = completedPart.getName();
                    if (completedPart instanceof CompletedFileUpload upload) {
                        String originalFileName = upload.getFilename();
                    }
                }

                @Override
                public void onError(Throwable t) {
                    emitter.error(t);
                }

                @Override
                public void onComplete() {
                    emitter.success("Uploaded");
                }
            });
        });
    }
}
Binding to the entire multipart body
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.multipart.CompletedFileUpload
import io.micronaut.http.multipart.CompletedPart
import io.micronaut.http.server.multipart.MultipartBody
import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
import reactor.core.publisher.Mono

import static io.micronaut.http.MediaType.MULTIPART_FORM_DATA
import static io.micronaut.http.MediaType.TEXT_PLAIN

@Controller("/upload")
class WholeBodyUploadController {

    @Post(value = "/whole-body", consumes = MULTIPART_FORM_DATA, produces = TEXT_PLAIN) // (1)
    Mono<String> uploadBytes(@Body MultipartBody body) { // (2)

        Mono.<String>create({ emitter ->
            body.subscribe(new Subscriber<CompletedPart>() {
                private Subscription s

                @Override
                void onSubscribe(Subscription s) {
                    this.s = s
                    s.request(1)
                }

                @Override
                void onNext(CompletedPart completedPart) {
                    String partName = completedPart.name
                    if (completedPart instanceof CompletedFileUpload) {
                        String originalFileName = completedPart.filename
                    }
                }

                @Override
                void onError(Throwable t) {
                    emitter.error(t)
                }

                @Override
                void onComplete() {
                    emitter.success("Uploaded")
                }
            })
        })
    }
}
Binding to the entire multipart body
import io.micronaut.http.MediaType.MULTIPART_FORM_DATA
import io.micronaut.http.MediaType.TEXT_PLAIN
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.multipart.CompletedFileUpload
import io.micronaut.http.multipart.CompletedPart
import io.micronaut.http.server.multipart.MultipartBody
import org.reactivestreams.Subscriber
import org.reactivestreams.Subscription
import reactor.core.publisher.Mono

@Controller("/upload")
class WholeBodyUploadController {

    @Post(value = "/whole-body", consumes = [MULTIPART_FORM_DATA], produces = [TEXT_PLAIN]) // (1)
    fun uploadBytes(@Body body: MultipartBody): Mono<String> { // (2)
        return Mono.create { emitter ->
            body.subscribe(object : Subscriber<CompletedPart> {
                private var s: Subscription? = null

                override fun onSubscribe(s: Subscription) {
                    this.s = s
                    s.request(1)
                }

                override fun onNext(completedPart: CompletedPart) {
                    val partName = completedPart.name
                    if (completedPart is CompletedFileUpload) {
                        val originalFileName = completedPart.filename
                    }
                }

                override fun onError(t: Throwable) {
                    emitter.error(t)
                }

                override fun onComplete() {
                    emitter.success("Uploaded")
                }
            })
        }
    }
}

6.23 File Transfers

The Micronaut framework supports sending files to the client in a couple of easy ways.

Sending File Objects

It is possible to return a File object from your controller method, and the data will be returned to the client. The Content-Type header of file responses is calculated based on the name of the file.

To control either the media type of the file being sent, or to set the file to be downloaded (i.e. using the Content-Disposition header), instead construct a SystemFile with the file to use. For example:

Sending a SystemFile
@Get
public SystemFile download() {
    File file = ...
    return new SystemFile(file).attach("myfile.txt");
    // or new SystemFile(file, MediaType.TEXT_HTML_TYPE)
}

Sending an InputStream

For cases where a reference to a File object is not possible (for example resources in JAR files), the Micronaut framework supports transferring input streams. To return a stream of data from the controller method, construct a StreamedFile.

The constructor for StreamedFile also accepts a java.net.URL for your convenience.
Sending a StreamedFile
@Get
public StreamedFile download() {
    InputStream inputStream = ...
    return new StreamedFile(inputStream, MediaType.TEXT_PLAIN_TYPE)
    // An attach(String filename) method is also available to set the Content-Disposition
}

The server supports returning 304 (Not Modified) responses if the files being transferred have not changed, and the request contains the appropriate header. In addition, if the client accepts encoded responses, the Micronaut framework encodes the file if appropriate. Encoding happens if the file is text-based and larger than 1KB by default. The threshold at which data is encoded is configurable. See the server configuration reference for details.

To use a custom data source to send data through an input stream, construct a PipedInputStream and PipedOutputStream to write data from the output stream to the input. Make sure to do the work on a separate thread so the file can be returned immediately.

Cache Configuration

By default, file responses include caching headers. The following options determine how the Cache-Control header is built.

🔗
Table 1. Configuration Properties for NettyHttpServerConfiguration$FileTypeHandlerConfiguration
Property Type Description

micronaut.server.netty.responses.file.cache-seconds

int

🔗
Table 2. Configuration Properties for NettyHttpServerConfiguration$FileTypeHandlerConfiguration$CacheControlConfiguration
Property Type Description

micronaut.server.netty.responses.file.cache-control.public

boolean

6.24 HTTP Filters

The Micronaut HTTP server supports applying filters to request/response processing in a similar (but reactive) way to Servlet filters in traditional Java applications.

Filters support the following use cases:

  • Decoration of the incoming HttpRequest

  • Modification of the outgoing HttpResponse

  • Implementation of cross-cutting concerns such as security, tracing, etc.

There are two ways to implement a filter:

We recommend Micronaut developers use Filter methods introduced in Micronaut Framework 4.0 to implement filters.

6.24.1 Filter Patterns

Filter patterns can be defined on the filter class (in Filter, the ServerFilter or ClientFilter annotation), or on the filter method (in the RequestFilter or ResponseFilter annotation).

You can use different styles of pattern for path matching by setting patternStyle. By default, AntPathMatcher is used for path matching. When using Ant, the mapping matches URLs using the following rules:

  • ? matches one character

  • * matches zero or more characters

  • ** matches zero or more subdirectories in a path

Table 1. @Filter Annotation Path Matching Examples
Pattern Example Matched Paths

/**

any path

customer/j?y

customer/joy, customer/jay

customer/*/id

customer/adam/id, customer/amy/id

customer/**

customer/adam, customer/adam/id, customer/adam/name

customer/*/.html

customer/index.html, customer/adam/profile.html, customer/adam/job/description.html

The other option is regular expression based matching. To use regular expressions, set patternStyle = FilterPatternStyle.REGEX. The pattern attribute is expected to contain a regular expression which will be expected to match the provided URLs exactly (using Matcher#matches).

Using FilterPatternStyle.ANT is preferred as the pattern matching is more performant than using regular expressions. FilterPatternStyle.REGEX should be used when your pattern cannot be written properly using Ant.

6.24.2 Filter Methods

A filter method must be declared in a bean annotated with ServerFilter, or ClientFilter if it should instead intercept requests made by the HTTP client. Each filter method must also be annotated with RequestFilter, to run before the request is processed, or ResponseFilter, to run after the request has completed to process the response.

A filter method can take various parameters, such as the HttpRequest and the HttpResponse (only for response filters). The return type can be void or null to continue execution as normal, or an updated HttpRequest (only for request filters) or HttpResponse. The different supported parameter and return types are described in the documentation of RequestFilter and ResponseFilter.

Request filters can bind the full request body using the Body annotation. This is useful e.g. to verify a signed request body.

Accessing the full body should be done with care. It forces the body to be fully buffered, even if the controller supports streaming (e.g. @Body InputStream parameter), leading to potential negative performance impact.
Body binding is supported by the netty-based HTTP server, but may not be supported by all other HTTP server backends.

To write asynchronous filters, you can return a reactive publisher.

To put these concepts into practice lets look at an example.

Filter methods execute in the event loop by default. If you need to perform blocking operations, you can annotate the filter with ExecuteOn.

6.24.2.1 Server Filter with Filter Methods

Suppose you wish to trace each request to the "Hello World" example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations.

You should not block the underlying Netty event loop in your filter; instead the filter should proceed with execution once any I/O is complete.

As an example, consider this TraceService that performs an I/O operation:

A TraceService Example using Reactive Streams
import io.micronaut.http.HttpRequest;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
public class TraceService {

    private static final Logger LOG = LoggerFactory.getLogger(TraceService.class);

    public void trace(HttpRequest<?> request) {
        LOG.debug("Tracing request: {}", request.getUri());
        // trace logic here, potentially performing I/O (1)
    }
}
A TraceService Example using Reactive Streams
import io.micronaut.http.HttpRequest
import org.slf4j.Logger
import org.slf4j.LoggerFactory

import jakarta.inject.Singleton
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers


@Singleton
class TraceService {
    private static final Logger LOG = LoggerFactory.getLogger(TraceService.class)

    void trace(HttpRequest<?> request) {
        LOG.debug('Tracing request: {}', request.uri)
        // trace logic here, potentially performing I/O (2)
    }
}
A TraceService Example using Reactive Streams
import io.micronaut.http.HttpRequest
import org.slf4j.LoggerFactory
import jakarta.inject.Singleton
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers


@Singleton
class TraceService {

    private val LOG = LoggerFactory.getLogger(TraceService::class.java)

    internal fun trace(request: HttpRequest<*>) {
        LOG.debug("Tracing request: {}", request.uri)
        // trace logic here, potentially performing I/O (2)
    }
}
1 Since this is just an example, the logic does nothing yet

The following code sample shows how to write a filter using filter methods:

An Example ServerFilter
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.RequestFilter;
import io.micronaut.http.annotation.ResponseFilter;
import io.micronaut.http.annotation.ServerFilter;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;

@ServerFilter("/hello/**") // (1)
public class TraceFilter {

    private final TraceService traceService;

    public TraceFilter(TraceService traceService) { // (2)
        this.traceService = traceService;
    }

    @RequestFilter
    @ExecuteOn(TaskExecutors.BLOCKING) // (3)
    public void filterRequest(HttpRequest<?> request) {
        traceService.trace(request); // (4)
    }

    @ResponseFilter // (5)
    public void filterResponse(MutableHttpResponse<?> res) {
        res.getHeaders().add("X-Trace-Enabled", "true");
    }
}
An Example ServerFilter
import io.micronaut.http.HttpRequest
import io.micronaut.context.annotation.Requires
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.RequestFilter
import io.micronaut.http.annotation.ResponseFilter
import io.micronaut.http.annotation.ServerFilter
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn


@ServerFilter("/hello/**") // (1)
class TraceFilter {

    private final TraceService traceService

    TraceFilter(TraceService traceService) { // (2)
        this.traceService = traceService
    }

    @RequestFilter
    @ExecuteOn(TaskExecutors.BLOCKING) // (3)
    void filterRequest(HttpRequest<?> request) {
        traceService.trace(request) // (4)
    }

    @ResponseFilter // (5)
    void filterResponse(MutableHttpResponse<?> res) {
        res.headers.add("X-Trace-Enabled", "true")
    }
}
An Example ServerFilter
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpRequest
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.RequestFilter
import io.micronaut.http.annotation.ResponseFilter
import io.micronaut.http.annotation.ServerFilter
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn

@ServerFilter("/hello/**") // (1)
class TraceFilter(private val traceService: TraceService) { // (2)

    @RequestFilter
    @ExecuteOn(TaskExecutors.BLOCKING) // (3)
    fun filterRequest(request: HttpRequest<*>) {
        traceService.trace(request) // (4)
    }

    @ResponseFilter // (5)
    fun filterResponse(res: MutableHttpResponse<*>) {
        res.headers.add("X-Trace-Enabled", "true")
    }
}
1 The ServerFilter annotation defines the URI pattern(s) the filter matches
2 The previously defined TraceService is injected via constructor
3 The request filter is marked to execute on a separate thread so that the blocking code in TraceService does not cause problems
4 TraceService is invoked to trace the request
5 Finally, a separate response filter method adds a X-Trace-Enabled header to the response.

The previous example demonstrates some key concepts such as executing blocking logic in a worker thread before proceeding with the request and modifying the outgoing response.

6.24.2.2 Error States

In principle, downstream filters and controllers can produce exceptions, and response filters should be prepared to handle them. For a response filter to be called when there is an exception, it must declare the exception type as a parameter.

Table 1. @Filter Response filter declaration
Declaration Called when?

void responseFilter(HttpResponse<?> response)

Only called on non-exception response

void responseFilter(Throwable failure)

Only called on exception response

void responseFilter(IOException failure)

Only called on exception response, if the exception is an IOException

void responseFilter(HttpResponse<?> response, @Nullable Throwable failure)

Always called. failure will be null if there was no error. If there was an error, response will be null.

Whether errors appear as exceptions depends on the context of the filter. For the Micronaut HTTP server, any exception is mapped to a non-exceptional HttpResponse with an error status code. This mapping happens before each filter, so a server filter will never actually see an exception. If you still want to access the original cause of the response, it is stored as the attribute EXCEPTION.

6.24.2.3 Continuations

Request filters can define a special FilterContinuation parameter to get more control of the downstream execution, and to be run further actions after it completes. For example, the above TraceFilter can be expressed using a single request filter:

Single request filter
@RequestFilter
@ExecuteOn(TaskExecutors.BLOCKING) // (4)
public void filterRequest(HttpRequest<?> request, FilterContinuation<MutableHttpResponse<?>> continuation) { // (1)
    traceService.trace(request);
    MutableHttpResponse<?> res = continuation.proceed(); // (2)
    res.getHeaders().add("X-Trace-Enabled", "true"); // (3)
}
Single request filter
@RequestFilter
@ExecuteOn(TaskExecutors.BLOCKING) // (4)
void filterRequest(HttpRequest<?> request, FilterContinuation<MutableHttpResponse<?>> continuation) { // (1)
    traceService.trace(request)
    MutableHttpResponse<?> res = continuation.proceed(); // (2)
    res.headers.add("X-Trace-Enabled", "true") // (3)
}
Single request filter
@RequestFilter
@ExecuteOn(TaskExecutors.BLOCKING) // (4)
fun filterRequest(request: HttpRequest<*>, continuation: FilterContinuation<MutableHttpResponse<*>>) { // (1)
    traceService.trace(request)
    val res = continuation.proceed() // (2)
    res.headers.add("X-Trace-Enabled", "true") // (3)
}
1 The request filter declares a FilterContinuation parameter. The continuation will return a MutableHttpResponse
2 After the request processing is done, the filter calls the blocking proceed to run downstream filters and the controller
3 When downstream processing completes, the filter adds a X-Trace-Enabled header to the response returned by the continuation
4 The whole filter is executed on a worker thread to avoid blocking the event loop in the proceed call
The call to FilterContinuation.proceed is blocking by default, so it should never be done on the event loop. Such filters should be run on a worker thread as described above. Alternatively, the continuation can also be declared to return a reactive type (Publisher<HttpResponse<?>>) to proceed in an asynchronous manner, similar to the old FilterChain API.

6.24.2.4 Filter Order

Filters can be ordered in one of three ways:

  1. An Order annotation on the filter method

  2. Implementing Ordered in the filter class

  3. An Order annotation on the filter class

Request filters are executed in order from the highest precedence (the smallest integer value, as defined by Ordered.HIGHEST_PRECEDENCE) to the lowest precedence (the biggest integer value, as defined by Ordered.LOWEST_PRECEDENCE). Response filters are executed in reverse order.

filter order

Request filter A is executed first, because it has the higher precedence (-100), followed by request filter B with the lower precedence (100). Then the controller is executed. Response filter C is executed first, because it has the lower precedence (100), and finally response filter D with the higher precedence (-100) is executed last.

6.24.3 HttpServerFilter

We recommend Micronaut developers use Filter methods instead of HttpServerFilter introduced in Micronaut Framework 4.0 to implement filters.

For a server application, the HttpServerFilter interface doFilter method can be implemented.

The doFilter method accepts the HttpRequest and an instance of ServerFilterChain.

The ServerFilterChain interface contains a resolved chain of filters where the final entry in the chain is the matched route. The ServerFilterChain.proceed(io.micronaut.http.HttpRequest) method resumes processing of the request.

The proceed(..) method returns a Reactive Streams Publisher that emits the response to be returned to the client. Implementors of filters can subscribe to the Publisher and mutate the emitted MutableHttpResponse to modify the response prior to returning the response to the client.

To put these concepts into practice lets look at an example.

Filters execute in the event loop, so blocking operations must be offloaded to another thread pool.

6.24.3.1 HttpServerFilter Example

Suppose you wish to trace each request to the "Hello World" example using some external system. This system could be a database or a distributed tracing service, and may require I/O operations.

You should not block the underlying Netty event loop in your filter; instead the filter should proceed with execution once any I/O is complete.

As an example, consider this TraceService that uses Project Reactor to compose an I/O operation:

A TraceService Example using Reactive Streams
import io.micronaut.http.HttpRequest;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.inject.Singleton;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

@Singleton
public class TraceService {
    private static final Logger LOG = LoggerFactory.getLogger(TraceService.class);
    public Publisher<Boolean> trace(HttpRequest<?> request) {
        return Mono.fromCallable(() -> { // (1)
            LOG.debug("Tracing request: {}", request.getUri());
            // trace logic here, potentially performing I/O (2)
            return true;
        }).subscribeOn(Schedulers.boundedElastic()) // (3)
                .flux();
    }
}
A TraceService Example using Reactive Streams
import io.micronaut.http.HttpRequest
import org.slf4j.Logger
import org.slf4j.LoggerFactory

import jakarta.inject.Singleton
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers

import java.util.concurrent.Callable


@Singleton
class TraceService {

    private static final Logger LOG = LoggerFactory.getLogger(TraceService.class)

    Flux<Boolean> trace(HttpRequest<?> request) {
        Mono.fromCallable(() ->  {  // (1)
            LOG.debug('Tracing request: {}', request.uri)
            // trace logic here, potentially performing I/O (2)
            return true
        }).flux().subscribeOn(Schedulers.boundedElastic()) // (3)

    }
}
A TraceService Example using Reactive Streams
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpRequest
import org.slf4j.LoggerFactory
import jakarta.inject.Singleton
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers

@Singleton
class TraceService {

    private val LOG = LoggerFactory.getLogger(TraceService::class.java)

    internal fun trace(request: HttpRequest<*>): Flux<Boolean> {
        return Mono.fromCallable {
            // (1)
            LOG.debug("Tracing request: {}", request.uri)
            // trace logic here, potentially performing I/O (2)
            true
        }.subscribeOn(Schedulers.boundedElastic()) // (3)
            .flux()
    }
}
1 The Mono type creates logic that executes potentially blocking operations to write the trace data from the request
2 Since this is just an example, the logic does nothing yet
3 The Schedulers.boundedElastic executes the logic

The following code sample shows how to implement the HttpServerFilter interface.

An Example HttpServerFilter
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.filter.HttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;

@Filter("/hello/**") // (1)
public class TraceFilter implements HttpServerFilter { // (2)
    private final TraceService traceService;
    public TraceFilter(TraceService traceService) { // (3)
        this.traceService = traceService;
    }
    @Override
    public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
                                                      ServerFilterChain chain) {
        return Flux.from(traceService
                .trace(request)) // (4)
                .switchMap(aBoolean -> chain.proceed(request)) // (5)
                .doOnNext(res ->
                    res.getHeaders().add("X-Trace-Enabled", "true") // (6)
                );
    }
}
An Example HttpServerFilter
import io.micronaut.http.HttpRequest
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.Filter
import io.micronaut.http.filter.HttpServerFilter
import io.micronaut.http.filter.ServerFilterChain
import org.reactivestreams.Publisher

@Filter("/hello/**") // (1)
class TraceFilter implements HttpServerFilter { // (2)

    private final TraceService traceService

    TraceFilter(TraceService traceService) { // (3)
        this.traceService = traceService
    }

    @Override
    Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
                                               ServerFilterChain chain) {
        traceService
                .trace(request) // (3)
                .switchMap({ aBoolean -> chain.proceed(request) }) // (4)
                .doOnNext({ res ->
                    res.headers.add("X-Trace-Enabled", "true") // (5)
                })
    }
}
An Example HttpServerFilter
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpRequest
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.Filter
import io.micronaut.http.filter.HttpServerFilter
import io.micronaut.http.filter.ServerFilterChain
import org.reactivestreams.Publisher

@Filter("/hello/**") // (1)
class TraceFilter( // (2)
    private val traceService: TraceService)// (3)
    : HttpServerFilter {

    override fun doFilter(request: HttpRequest<*>,
                          chain: ServerFilterChain): Publisher<MutableHttpResponse<*>> {
        return traceService.trace(request) // (4)
            .switchMap { aBoolean -> chain.proceed(request) } // (5)
            .doOnNext { res ->
                res.headers.add("X-Trace-Enabled", "true") // (6)
            }
    }
}
1 The Filter annotation defines the URI pattern(s) the filter matches
2 The class implements the HttpServerFilter interface
3 The previously defined TraceService is injected via constructor
4 TraceService is invoked to trace the request
5 If the call succeeds, the filter resumes request processing using Project Reactor's switchMap method, which invokes the proceed method of the ServerFilterChain
6 Finally, the Project Reactor's doOnNext method adds a X-Trace-Enabled header to the response.

The previous example demonstrates some key concepts such as executing logic in a non-blocking manner before proceeding with the request and modifying the outgoing response.

The examples use Project Reactor, however you can use any reactive framework that supports the Reactive streams specifications

6.24.3.2 HttpServerFilter Error States

The publisher returned from chain.proceed should never emit an error. In the cases where an upstream filter emitted an error or the route itself threw an exception, the error response should be emitted instead of the exception. In some cases it may be desirable to know the cause of the error response and for this purpose an attribute exists on the response if it was created as a result of an exception being emitted or thrown. The original cause is stored as the attribute EXCEPTION.

6.25 HTTP Sessions

See the documentation for Micronaut Session for information about supporting HTTP sessions in your applications.

6.26 Server Sent Events

The Micronaut HTTP server supports emitting Server Sent Events (SSE) using the Event API.

To emit events from the server, return a Reactive Streams Publisher that emits objects of type Event.

The Publisher itself could publish events from a background task, via an event system, etc.

Imagine for an example an event stream of news headlines; you may define a data class as follows:

Headline
public class Headline {

    private String title;
    private String description;

    public Headline() {}

    public Headline(String title, String description) {
        this.title = title;
        this.description = description;
    }

    public String getTitle() {
        return title;
    }

    public String getDescription() {
        return description;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}
Headline
class Headline {

    String title
    String description

    Headline() {}

    Headline(String title, String description) {
        this.title = title;
        this.description = description;
    }
}
Headline
class Headline {

    var title: String? = null
    var description: String? = null

    constructor()

    constructor(title: String, description: String) {
        this.title = title
        this.description = description
    }
}

To emit news headline events, write a controller that returns a Publisher of Event instances using whichever Reactive library you prefer. The example below uses Project Reactor's Flux via the generate method:

Publishing Server Sent Events from a Controller
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.sse.Event;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;

@Controller("/headlines")
public class HeadlineController {

    @ExecuteOn(TaskExecutors.IO)
    @Get(produces = MediaType.TEXT_EVENT_STREAM)
    public Publisher<Event<Headline>> index() { // (1)
        String[] versions = {"1.0", "2.0"}; // (2)
        return Flux.generate(() -> 0, (i, emitter) -> { // (3)
            if (i < versions.length) {
                emitter.next( // (4)
                    Event.of(new Headline("Micronaut " + versions[i] + " Released", "Come and get it"))
                );
            } else {
                emitter.complete(); // (5)
            }
            return ++i;
        });
    }
}
Publishing Server Sent Events from a Controller
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.sse.Event
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux

@Controller("/headlines")
class HeadlineController {

    @ExecuteOn(TaskExecutors.IO)
    @Get(produces = MediaType.TEXT_EVENT_STREAM)
    Publisher<Event<Headline>> index() { // (1)
        String[] versions = ["1.0", "2.0"] // (2)
        Flux.generate(() -> 0, (i, emitter) -> {
            if (i < versions.length) {
                emitter.next( // (4)
                        Event.of(new Headline("Micronaut ${versions[i]} Released", "Come and get it"))
                )
            } else {
                emitter.complete() // (5)
            }
            return i + 1
        })
    }
}
Publishing Server Sent Events from a Controller
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.sse.Event
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
import reactor.core.publisher.SynchronousSink
import java.util.concurrent.Callable
import java.util.function.BiFunction


@Controller("/headlines")
class HeadlineController {

    @ExecuteOn(TaskExecutors.IO)
    @Get(produces = [MediaType.TEXT_EVENT_STREAM])
    fun index(): Publisher<Event<Headline>> { // (1)
        val versions = arrayOf("1.0", "2.0") // (2)
        return Flux.generate(
            { 0 },
            BiFunction { i: Int, emitter: SynchronousSink<Event<Headline>> ->  // (3)
                if (i < versions.size) {
                    emitter.next( // (4)
                        Event.of(
                            Headline(
                                "Micronaut " + versions[i] + " Released", "Come and get it"
                            )
                        )
                    )
                } else {
                    emitter.complete() // (5)
                }
                return@BiFunction i + 1
            })
    }
}
1 The controller method returns a Publisher of Event
2 A headline is emitted for each version of Micronaut
3 The Flux type’s generate method generates a Publisher. The generate method accepts an initial value and a lambda that accepts the value and a Emitter. Note that this example executes on the same thread as the controller action, but you could use subscribeOn or map an existing "hot" Flux.
4 The Emitter interface onNext method emits objects of type Event. The Event.of(ET) factory method constructs the event.
5 The Emitter interface onComplete method indicates when to finish sending server sent events.
You typically want to schedule SSE event streams on a separate executor. The previous example uses @ExecuteOn to execute the stream on the I/O executor.

The above example sends back a response of type text/event-stream and for each Event emitted the Headline type previously will be converted to JSON resulting in responses such as:

Server Sent Event Response Output
 data: {"title":"Micronaut 1.0 Released","description":"Come and get it"}
 data: {"title":"Micronaut 2.0 Released","description":"Come and get it"}

You can use the methods of the Event interface to customize the Server Sent Event data sent back, including associating event ids, comments, retry timeouts, etc.

6.27 WebSocket Support

The Micronaut framework features dedicated support for creating WebSocket clients and servers. The io.micronaut.websocket.annotation package includes annotations for defining both clients and servers.

Since Micronaut Framework 4.0. io.micronaut:micronaut-http-server no longer exposes micronaut-websocket transitively. To use annotations such as @ServerWebSocket, add the micronaut-websocket dependency to your application classpath:

implementation("io.micronaut:micronaut-websocket")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-websocket</artifactId>
</dependency>

6.27.1 Using @ServerWebSocket

The @ServerWebSocket annotation can be applied to any class that should map to a WebSocket URI. The following example is a simple chat WebSocket implementation:

WebSocket Chat Example
import io.micronaut.websocket.WebSocketBroadcaster;
import io.micronaut.websocket.WebSocketSession;
import io.micronaut.websocket.annotation.OnClose;
import io.micronaut.websocket.annotation.OnMessage;
import io.micronaut.websocket.annotation.OnOpen;
import io.micronaut.websocket.annotation.ServerWebSocket;

import java.util.function.Predicate;

@ServerWebSocket("/chat/{topic}/{username}") // (1)
public class ChatServerWebSocket {

    private final WebSocketBroadcaster broadcaster;

    public ChatServerWebSocket(WebSocketBroadcaster broadcaster) {
        this.broadcaster = broadcaster;
    }

    @OnOpen // (2)
    public void onOpen(String topic, String username, WebSocketSession session) {
        String msg = "[" + username + "] Joined!";
        broadcaster.broadcastSync(msg, isValid(topic, session));
    }

    @OnMessage // (3)
    public void onMessage(String topic, String username,
                          String message, WebSocketSession session) {
        String msg = "[" + username + "] " + message;
        broadcaster.broadcastSync(msg, isValid(topic, session)); // (4)
    }

    @OnClose // (5)
    public void onClose(String topic, String username, WebSocketSession session) {
        String msg = "[" + username + "] Disconnected!";
        broadcaster.broadcastSync(msg, isValid(topic, session));
    }

    private Predicate<WebSocketSession> isValid(String topic, WebSocketSession session) {
        return s -> s != session &&
                topic.equalsIgnoreCase(s.getUriVariables().get("topic", String.class, null));
    }
}
WebSocket Chat Example
import io.micronaut.websocket.WebSocketBroadcaster
import io.micronaut.websocket.WebSocketSession
import io.micronaut.websocket.annotation.OnClose
import io.micronaut.websocket.annotation.OnMessage
import io.micronaut.websocket.annotation.OnOpen
import io.micronaut.websocket.annotation.ServerWebSocket

import java.util.function.Predicate

@ServerWebSocket("/chat/{topic}/{username}") // (1)
class ChatServerWebSocket {

    private final WebSocketBroadcaster broadcaster

    ChatServerWebSocket(WebSocketBroadcaster broadcaster) {
        this.broadcaster = broadcaster
    }

    @OnOpen // (2)
    void onOpen(String topic, String username, WebSocketSession session) {
        String msg = "[$username] Joined!"
        broadcaster.broadcastSync(msg, isValid(topic, session))
    }

    @OnMessage // (3)
    void onMessage(String topic, String username,
                   String message, WebSocketSession session) {
        String msg = "[$username] $message"
        broadcaster.broadcastSync(msg, isValid(topic, session)) // (4)
    }

    @OnClose // (5)
    void onClose(String topic, String username, WebSocketSession session) {
        String msg = "[$username] Disconnected!"
        broadcaster.broadcastSync(msg, isValid(topic, session))
    }

    private Predicate<WebSocketSession> isValid(String topic, WebSocketSession session) {
        return { s -> s != session && topic.equalsIgnoreCase(s.uriVariables.get("topic", String, null)) }
    }
}
WebSocket Chat Example
import io.micronaut.websocket.WebSocketBroadcaster
import io.micronaut.websocket.WebSocketSession
import io.micronaut.websocket.annotation.OnClose
import io.micronaut.websocket.annotation.OnMessage
import io.micronaut.websocket.annotation.OnOpen
import io.micronaut.websocket.annotation.ServerWebSocket

import java.util.function.Predicate

@ServerWebSocket("/chat/{topic}/{username}") // (1)
class ChatServerWebSocket(private val broadcaster: WebSocketBroadcaster) {

    @OnOpen // (2)
    fun onOpen(topic: String, username: String, session: WebSocketSession) {
        val msg = "[$username] Joined!"
        broadcaster.broadcastSync(msg, isValid(topic, session))
    }

    @OnMessage // (3)
    fun onMessage(topic: String, username: String,
                  message: String, session: WebSocketSession) {
        val msg = "[$username] $message"
        broadcaster.broadcastSync(msg, isValid(topic, session)) // (4)
    }

    @OnClose // (5)
    fun onClose(topic: String, username: String, session: WebSocketSession) {
        val msg = "[$username] Disconnected!"
        broadcaster.broadcastSync(msg, isValid(topic, session))
    }

    private fun isValid(topic: String, session: WebSocketSession): Predicate<WebSocketSession> {
        return Predicate<WebSocketSession> {
            (it !== session && topic.equals(it.uriVariables.get("topic", String::class.java, null), ignoreCase = true))
        }
    }
}
1 The @ServerWebSocket annotation defines the path the WebSocket is mapped under. The URI can be a URI template.
2 The @OnOpen annotation declares the method to invoke when the WebSocket is opened.
3 The @OnMessage annotation declares the method to invoke when a message is received.
4 You can use a WebSocketBroadcaster to broadcast messages to every WebSocket session. You can filter which sessions to send to with a Predicate. Also, you could use the WebSocketSession instance to send a message to it with WebSocketSession::send.
5 The @OnClose annotation declares the method to invoke when the WebSocket is closed.
A working example of WebSockets in action can be found at Micronaut Guides.

For binding, method arguments to each WebSocket method can be:

  • A variable from the URI template (in the above example topic and username are URI template variables)

  • An instance of WebSocketSession

The @OnClose Method

The @OnClose method can optionally receive a CloseReason. The @OnClose method is invoked prior to the session closing.

The @OnMessage Method

The @OnMessage method can define a parameter for the message body. The parameter can be one of the following:

  • A Netty WebSocketFrame

  • Any Java primitive or simple type (such as String). In fact, any type that can be converted from ByteBuf (you can register additional TypeConverter beans to support a custom type).

  • A byte[], a ByteBuf or a Java NIO ByteBuffer.

  • A POJO. In this case, it will be decoded by default as JSON using JsonMediaTypeCodec. You can register a custom codec and define the content type of the handler using the @Consumes annotation.

  • A WebSocketPongMessage. This is a special case: The method will not receive regular messages, but instead handle WebSocket pongs that arrive as a reply to a ping sent to the client.

The @OnError Method

A method annotated with @OnError can be added to implement custom error handling. The @OnError method can define a parameter that receives the exception type to be handled. If no @OnError handling is present and an unrecoverable exception occurs, the WebSocket is automatically closed.

Non-Blocking Message Handling

The previous example uses the broadcastSync method of the WebSocketBroadcaster interface which blocks until the broadcast is complete. A similar sendSync method exists in WebSocketSession to send a message to a single receiver in a blocking manner. You can however implement non-blocking WebSocket servers by instead returning a Publisher or a Future from each WebSocket handler method. For example:

WebSocket Chat Example
@OnMessage
public Publisher<Message> onMessage(String topic, String username,
                                    Message message, WebSocketSession session) {
    String text = "[" + username + "] " + message.getText();
    Message newMessage = new Message(text);
    return broadcaster.broadcast(newMessage, isValid(topic, session));
}
WebSocket Chat Example
@OnMessage
Publisher<Message> onMessage(String topic, String username,
                             Message message, WebSocketSession session) {
    String text = "[$username] $message.text"
    Message newMessage = new Message(text)
    broadcaster.broadcast(newMessage, isValid(topic, session))
}
WebSocket Chat Example
@OnMessage
fun onMessage(topic: String, username: String,
              message: Message, session: WebSocketSession): Publisher<Message> {
    val text = "[" + username + "] " + message.text
    val newMessage = Message(text)
    return broadcaster.broadcast(newMessage, isValid(topic, session))
}

The example above uses broadcast, which creates an instance of Publisher and returns the value to Micronaut. The Micronaut framework sends the message asynchronously based on the Publisher interface. The similar send method sends a single message asynchronously via Micronaut return value.

For sending messages asynchronously outside Micronaut annotated handler methods, you can use broadcastAsync and sendAsync methods in their respective WebSocketBroadcaster and WebSocketSession interfaces. For blocking sends, the broadcastSync and sendSync methods can be used.

@ServerWebSocket and Scopes

By default, the @ServerWebSocket instance is shared for all WebSocket connections. Extra care must be taken to synchronize local state to avoid thread safety issues.

If you prefer to have an instance for each connection, annotate the class with @Prototype. This lets you retrieve the WebSocketSession from the @OnOpen handler and assign it to a field of the @ServerWebSocket instance.

Sharing Sessions with the HTTP Session

The WebSocketSession is by default backed by an in-memory map. If you add the session module you can however share sessions between the HTTP server and the WebSocket server.

When sessions are backed by a persistent store such as Redis, after each message is processed the session is updated to the backing store.
Using the CLI

If you created your project using Application Type Micronaut Application, you can use the create-websocket-server command with the Micronaut CLI to create a class annotated with ServerWebSocket.

$ mn create-websocket-server MyChat
| Rendered template WebsocketServer.java to destination src/main/java/example/MyChatServer.java

Connection Timeouts

By default, Micronaut framework times out idle connections with no activity after five minutes. Normally this is not a problem as browsers automatically reconnect WebSocket sessions, however you can control this behaviour by setting the micronaut.server.idle-timeout setting (a negative value results in no timeout):

Setting the Connection Timeout for the Server
micronaut.server.idle-timeout=30m
micronaut:
  server:
    idle-timeout: 30m
[micronaut]
  [micronaut.server]
    idle-timeout="30m"
micronaut {
  server {
    idleTimeout = "30m"
  }
}
{
  micronaut {
    server {
      idle-timeout = "30m"
    }
  }
}
{
  "micronaut": {
    "server": {
      "idle-timeout": "30m"
    }
  }
}

If you use Micronaut’s WebSocket client you may also wish to set the timeout on the client:

Setting the Connection Timeout for the Client
micronaut.http.client.read-idle-timeout=30m
micronaut:
  http:
    client:
      read-idle-timeout: 30m
[micronaut]
  [micronaut.http]
    [micronaut.http.client]
      read-idle-timeout="30m"
micronaut {
  http {
    client {
      readIdleTimeout = "30m"
    }
  }
}
{
  micronaut {
    http {
      client {
        read-idle-timeout = "30m"
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "client": {
        "read-idle-timeout": "30m"
      }
    }
  }
}

6.27.2 Using @ClientWebSocket

The @ClientWebSocket annotation can be used with the WebSocketClient interface to define WebSocket clients.

You can inject a reference to a WebSocketClient using the @Client annotation:

@Inject
@Client("http://localhost:8080")
WebSocketClient webSocketClient;

This lets you use the same service discovery and load balancing features for WebSocket clients.

Once you have a reference to the WebSocketClient interface you can use the connect method to obtain a connected instance of a bean annotated with @ClientWebSocket.

For example consider the following implementation:

WebSocket Chat Example
import io.micronaut.http.HttpRequest;
import io.micronaut.websocket.WebSocketSession;
import io.micronaut.websocket.annotation.ClientWebSocket;
import io.micronaut.websocket.annotation.OnMessage;
import io.micronaut.websocket.annotation.OnOpen;
import org.reactivestreams.Publisher;
import io.micronaut.core.async.annotation.SingleResult;
import java.util.Collection;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Future;

@ClientWebSocket("/chat/{topic}/{username}") // (1)
public abstract class ChatClientWebSocket implements AutoCloseable { // (2)

    private WebSocketSession session;
    private HttpRequest request;
    private String topic;
    private String username;
    private Collection<String> replies = new ConcurrentLinkedQueue<>();

    @OnOpen
    public void onOpen(String topic, String username,
                       WebSocketSession session, HttpRequest request) { // (3)
        this.topic = topic;
        this.username = username;
        this.session = session;
        this.request = request;
    }

    public String getTopic() {
        return topic;
    }

    public String getUsername() {
        return username;
    }

    public Collection<String> getReplies() {
        return replies;
    }

    public WebSocketSession getSession() {
        return session;
    }

    public HttpRequest getRequest() {
        return request;
    }

    @OnMessage
    public void onMessage(String message) {
        replies.add(message); // (4)
    }
WebSocket Chat Example
import io.micronaut.http.HttpRequest
import io.micronaut.websocket.WebSocketSession
import io.micronaut.websocket.annotation.ClientWebSocket
import io.micronaut.websocket.annotation.OnMessage
import io.micronaut.websocket.annotation.OnOpen
import org.reactivestreams.Publisher
import reactor.core.publisher.Mono
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Future
import io.micronaut.core.async.annotation.SingleResult

@ClientWebSocket("/chat/{topic}/{username}") // (1)
abstract class ChatClientWebSocket implements AutoCloseable { // (2)

    private WebSocketSession session
    private HttpRequest request
    private String topic
    private String username
    private Collection<String> replies = new ConcurrentLinkedQueue<>()

    @OnOpen
    void onOpen(String topic, String username,
                WebSocketSession session, HttpRequest request) { // (3)
        this.topic = topic
        this.username = username
        this.session = session
        this.request = request
    }

    String getTopic() {
        topic
    }

    String getUsername() {
        username
    }

    Collection<String> getReplies() {
        replies
    }

    WebSocketSession getSession() {
        session
    }

    HttpRequest getRequest() {
        request
    }

    @OnMessage
    void onMessage(String message) {
        replies << message // (4)
    }
WebSocket Chat Example
import io.micronaut.http.HttpRequest
import io.micronaut.websocket.WebSocketSession
import io.micronaut.websocket.annotation.ClientWebSocket
import io.micronaut.websocket.annotation.OnMessage
import io.micronaut.websocket.annotation.OnOpen
import reactor.core.publisher.Mono
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Future

@ClientWebSocket("/chat/{topic}/{username}") // (1)
abstract class ChatClientWebSocket : AutoCloseable { // (2)

    var session: WebSocketSession? = null
        private set
    var request: HttpRequest<*>? = null
        private set
    var topic: String? = null
        private set
    var username: String? = null
        private set
    private val replies = ConcurrentLinkedQueue<String>()

    @OnOpen
    fun onOpen(topic: String, username: String,
               session: WebSocketSession, request: HttpRequest<*>) { // (3)
        this.topic = topic
        this.username = username
        this.session = session
        this.request = request
    }

    fun getReplies(): Collection<String> {
        return replies
    }

    @OnMessage
    fun onMessage(message: String) {
        replies.add(message) // (4)
    }
1 The class is abstract (more on that later) and is annotated with @ClientWebSocket
2 The client must implement AutoCloseable and you should ensure that the connection is closed at some point.
3 You can use the same annotations as on the server, in this case @OnOpen to obtain a reference to the underlying session.
4 The @OnMessage annotation defines the method that receives responses from the server.

You can also define abstract methods that start with either send or broadcast and these methods will be implemented for you at compile time. For example:

WebSocket Send Methods
public abstract void send(String message);

Note by returning void this tells the Micronaut framework that the method is a blocking send. You can instead define methods that return either futures or a Publisher:

WebSocket Send Methods
public abstract reactor.core.publisher.Mono<String> send(String message);

The above example defines a send method that returns a Mono.

WebSocket Send Methods
public abstract java.util.concurrent.Future<String> sendAsync(String message);

The above example defines a send method that executes asynchronously and returns a Future to access the results.

Once you have defined a client class you can connect to the client socket and start sending messages:

Connecting a Client WebSocket
ChatClientWebSocket chatClient = webSocketClient
    .connect(ChatClientWebSocket.class, "/chat/football/fred")
    .blockFirst();
chatClient.send("Hello World!");
For illustration purposes we use blockFirst() to obtain the client. It is however possible to combine connect (which returns a Flux) to perform non-blocking interaction via WebSocket.
Using the CLI

If you created your project using the Micronaut CLI and the default (service) profile, you can use the create-websocket-client command to create an abstract class with WebSocketClient.

$ mn create-websocket-client MyChat
| Rendered template WebsocketClient.java to destination src/main/java/example/MyChatClient.java

6.28 HTTP/2 Support

Since Micronaut framework 2.x, Micronaut Netty-based HTTP server can be configured to support HTTP/2.

Configuring the Server for HTTP/2

The first step is to set the supported HTTP version in the server configuration:

Enabling HTTP/2 Support
micronaut.server.http-version=2.0
micronaut:
  server:
    http-version: 2.0
[micronaut]
  [micronaut.server]
    http-version=2.0
micronaut {
  server {
    httpVersion = 2.0
  }
}
{
  micronaut {
    server {
      http-version = 2.0
    }
  }
}
{
  "micronaut": {
    "server": {
      "http-version": 2.0
    }
  }
}

With this configuration, the Micronaut framework enables support for the h2c protocol (see HTTP/2 over cleartext) which is fine for development.

Since browsers don’t support h2c and in general HTTP/2 over TLS (the h2 protocol), it is recommended for production that you enable HTTPS support. For development this can be done with:

Enabling h2 Protocol Support
micronaut.server.http-version=2.0
micronaut.server.ssl.enabled=true
micronaut.server.ssl.buildSelfSigned=true
micronaut:
  server:
    http-version: 2.0
    ssl:
      enabled: true
      buildSelfSigned: true
[micronaut]
  [micronaut.server]
    http-version=2.0
    [micronaut.server.ssl]
      enabled=true
      buildSelfSigned=true
micronaut {
  server {
    httpVersion = 2.0
    ssl {
      enabled = true
      buildSelfSigned = true
    }
  }
}
{
  micronaut {
    server {
      http-version = 2.0
      ssl {
        enabled = true
        buildSelfSigned = true
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "http-version": 2.0,
      "ssl": {
        "enabled": true,
        "buildSelfSigned": true
      }
    }
  }
}

For production, see the configuring HTTPS section of the documentation.

Note that if your deployment environment uses JDK 8, or for improved support for OpenSSL, define the following dependencies on Netty Tomcat Native:

runtimeOnly("io.netty:netty-tcnative:2.0.58.Final")
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-tcnative</artifactId>
    <version>2.0.58.Final</version>
    <scope>runtime</scope>
</dependency>

runtimeOnly("io.netty:netty-tcnative-boringssl-static:2.0.58.Final")
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-tcnative-boringssl-static</artifactId>
    <version>2.0.58.Final</version>
    <scope>runtime</scope>
</dependency>

In addition to a dependency on the appropriate native library for your architecture. For example:

Configuring Tomcat Native
runtimeOnly "io.netty:netty-tcnative-boringssl-static:2.0.58.Final:${Os.isFamily(Os.FAMILY_MAC) ? (Os.isArch("aarch64") ? "osx-aarch_64" : "osx-x86_64") : 'linux-x86_64'}"

See the documentation on Tomcat Native for more information.

HTTP/2 Server Push Support

Support for server push has been added in Micronaut framework 3.2. Server push allows for a single request to trigger multiple responses. This is most often used in the case of browser based resources. The goal is to improve latency for the client, because they do not have to request that resource manually anymore, and can save a round trip.

A new interface, PushCapableHttpRequest, has been created to support the feature. Simply add a PushCapableHttpRequest parameter to a controller method and use its API to trigger additional requests.

PushCapableHttpRequest extends HttpRequest so it’s not necessary to have both as arguments in a controller method.

Before triggering additional requests, the isServerPushSupported() method should be called to ensure the feature is available. Once it’s known the feature is supported, use the serverPush(HttpRequest) method to trigger additional requests. For example: request.serverPush(HttpRequest.GET("/static/style.css"))).

6.29 HTTP/3 Support

Since Micronaut framework 4.x, Micronaut’s Netty-based HTTP server can be configured to support HTTP/3. This support is experimental and may change without notice.

Configuring the Server for HTTP/3

Instead of the TCP used for HTTP/1.1 and HTTP/2, HTTP/3 runs on UDP. To expose an HTTP/3 server, you need to define a listener with the special QUIC protocol family:

Enabling HTTP/3 Support
micronaut:
  server:
    netty:
      listeners:
        http3Listener:
          family: QUIC
          port: 8443
that defining this listener will disable the implicit TCP listeners. You can add them manually as described in the listener section.

Additionally, the netty HTTP/3 codec needs to be present on the classpath:

implementation("io.netty.incubator:netty-incubator-codec-http3")
<dependency>
    <groupId>io.netty.incubator</groupId>
    <artifactId>netty-incubator-codec-http3</artifactId>
</dependency>

6.30 Server Events

The HTTP server emits a number of Bean Events, defined in the io.micronaut.runtime.server.event package, that you can write listeners for. The following table summarizes these:

Table 1. Server Events
Event Description

ServerStartupEvent

Emitted when the server completes startup

ServerShutdownEvent

Emitted when the server shuts down

ServiceReadyEvent

Emitted after all ServerStartupEvent listeners have been invoked and exposes the EmbeddedServerInstance

ServiceStoppedEvent

Emitted after all ServerShutdownEvent listeners have been invoked and exposes the EmbeddedServerInstance

Doing significant work within a listener for a ServerStartupEvent will increase startup time.

The following example defines a ApplicationEventListener that listens for ServerStartupEvent:

Listening for Server Startup Events
import io.micronaut.context.event.ApplicationEventListener;
...
@Singleton
public class StartupListener implements ApplicationEventListener<ServerStartupEvent> {
    @Override
    public void onApplicationEvent(ServerStartupEvent event) {
        // logic here
        ...
    }
}

Alternatively, you can also use the @EventListener annotation on a method of any bean that accepts ServerStartupEvent:

Using @EventListener with ServerStartupEvent
import io.micronaut.runtime.event.annotation.EventListener;
import io.micronaut.runtime.server.event.ServerStartupEvent;
import jakarta.inject.Singleton;
...
@Singleton
public class MyBean {

    @EventListener
    public void onStartup(ServerStartupEvent event) {
        // logic here
        ...
    }
}

6.31 Configuring the HTTP Server

The HTTP server features a number of configuration options. They are defined in the NettyHttpServerConfiguration configuration class, which extends HttpServerConfiguration.

The following example shows how to tweak configuration options for the server via your configuration file (e.g application.yml):

Configuring HTTP server settings
micronaut.server.maxRequestSize=1MB
micronaut.server.host=localhost
micronaut.server.netty.maxHeaderSize=500KB
micronaut.server.netty.worker.threads=8
micronaut.server.netty.childOptions.autoRead=true
micronaut:
  server:
    maxRequestSize: 1MB
    host: localhost
    netty:
      maxHeaderSize: 500KB
      worker:
        threads: 8
      childOptions:
        autoRead: true
[micronaut]
  [micronaut.server]
    maxRequestSize="1MB"
    host="localhost"
    [micronaut.server.netty]
      maxHeaderSize="500KB"
      [micronaut.server.netty.worker]
        threads=8
      [micronaut.server.netty.childOptions]
        autoRead=true
micronaut {
  server {
    maxRequestSize = "1MB"
    host = "localhost"
    netty {
      maxHeaderSize = "500KB"
      worker {
        threads = 8
      }
      childOptions {
        autoRead = true
      }
    }
  }
}
{
  micronaut {
    server {
      maxRequestSize = "1MB"
      host = "localhost"
      netty {
        maxHeaderSize = "500KB"
        worker {
          threads = 8
        }
        childOptions {
          autoRead = true
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "maxRequestSize": "1MB",
      "host": "localhost",
      "netty": {
        "maxHeaderSize": "500KB",
        "worker": {
          "threads": 8
        },
        "childOptions": {
          "autoRead": true
        }
      }
    }
  }
}
  • By default, Micronaut framework binds to all network interfaces. Use localhost to bind only to loopback network interface

  • maxHeaderSize sets the maximum size for headers

  • worker.threads specifies the number of Netty worker threads

  • autoRead enables request body auto read

🔗
Table 1. Configuration Properties for NettyHttpServerConfiguration
Property Type Description

micronaut.server.netty.server-type

NettyHttpServerConfiguration$HttpServerType

micronaut.server.netty.close-on-expectation-failed

boolean

If a 100-continue response is detected but the content length is too large then true means close the connection. otherwise the connection will remain open and data will be consumed and discarded until the next request is received.

<p>only relevant when {@link HttpServerType#FULL_CONTENT} is set</p>

micronaut.server.netty.fallback-protocol

java.lang.String

micronaut.server.netty.log-level

io.netty.handler.logging.LogLevel

The server {@link LogLevel} to enable.

micronaut.server.netty.max-initial-line-length

int

The maximum length of the initial HTTP request line. Defaults to 4096.

micronaut.server.netty.max-header-size

int

The maximum size of an individual HTTP setter. Defaults to 8192.

micronaut.server.netty.max-chunk-size

int

The maximum chunk size. Defaults to 8192.

micronaut.server.netty.max-h2c-upgrade-request-size

int

The maximum size of the body of the HTTP1.1 request used to upgrade a connection to HTTP2 clear-text (h2c). This initial request cannot be streamed and is instead buffered in full, so the default value (8192) is relatively small. <i>If this value is too small for your use case, instead consider using an empty initial "upgrade request" (e.g. {@code OPTIONS /}), or switch to normal HTTP2.</i> <p> <i>Does not affect normal HTTP2 (TLS).</i>

micronaut.server.netty.chunked-supported

boolean

Whether chunked requests are supported.

micronaut.server.netty.use-native-transport

boolean

Whether to use netty’s native transport (epoll or kqueue) if available.

micronaut.server.netty.validate-headers

boolean

Whether to validate headers.

micronaut.server.netty.initial-buffer-size

int

The initial buffer size. Defaults to 128.

micronaut.server.netty.compression-threshold

int

The default compression threshold. Defaults to 1024.

micronaut.server.netty.compression-level

int

The default compression level. Default value (6).

micronaut.server.netty.child-options

java.util.Map

micronaut.server.netty.options

java.util.Map

micronaut.server.netty.keep-alive-on-server-error

boolean

micronaut.server.netty.pcap-logging-path-pattern

java.lang.String

The path pattern to use for logging incoming connections to pcap. This is an unsupported option: Behavior may change, or it may disappear entirely, without notice!

micronaut.server.netty.listeners

java.util.List

Get the explicit netty listener configurations, or {@code null} if they should be implicit.

micronaut.server.netty.eager-parsing

boolean

Parse incoming JSON data eagerly, before route binding. Default value {@value DEFAULT_EAGER_PARSING}.

micronaut.server.netty.json-buffer-max-components

int

Maximum number of buffers to keep around in JSON parsing before they should be consolidated. Defaults to 4096.

Using Native Transports

The native Netty transports add features specific to a particular platform, generate less garbage, and generally improve performance when compared to the NIO-based transport.

To enable native transports, first add a dependency:

For macOS on x86:

runtimeOnly("io.netty:netty-transport-native-kqueue::osx-x86_64")
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport-native-kqueue</artifactId>
    <scope>runtime</scope>
    <classifier>osx-x86_64</classifier>
</dependency>

For macOS on M1:

runtimeOnly("io.netty:netty-transport-native-kqueue::osx-aarch_64")
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport-native-kqueue</artifactId>
    <scope>runtime</scope>
    <classifier>osx-aarch_64</classifier>
</dependency>

For Linux on x86:

runtimeOnly("io.netty:netty-transport-native-epoll::linux-x86_64")
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport-native-epoll</artifactId>
    <scope>runtime</scope>
    <classifier>linux-x86_64</classifier>
</dependency>

For Linux on ARM64:

runtimeOnly("io.netty:netty-transport-native-epoll::linux-aarch_64")
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport-native-epoll</artifactId>
    <scope>runtime</scope>
    <classifier>linux-aarch_64</classifier>
</dependency>

Then configure the default event loop group to prefer native transports:

Configuring The Default Event Loop to Prefer Native Transports
micronaut.netty.event-loops.default.prefer-native-transport=true
micronaut:
  netty:
    event-loops:
      default:
        prefer-native-transport: true
[micronaut]
  [micronaut.netty]
    [micronaut.netty.event-loops]
      [micronaut.netty.event-loops.default]
        prefer-native-transport=true
micronaut {
  netty {
    eventLoops {
      'default' {
        preferNativeTransport = true
      }
    }
  }
}
{
  micronaut {
    netty {
      event-loops {
        default {
          prefer-native-transport = true
        }
      }
    }
  }
}
{
  "micronaut": {
    "netty": {
      "event-loops": {
        "default": {
          "prefer-native-transport": true
        }
      }
    }
  }
}

On Linux, Netty also offers an io_uring-based transport in incubator status. On x86_64:

runtimeOnly("io.netty.incubator:netty-incubator-transport-native-io_uring::linux-x86_64")
<dependency>
    <groupId>io.netty.incubator</groupId>
    <artifactId>netty-incubator-transport-native-io_uring</artifactId>
    <scope>runtime</scope>
    <classifier>linux-x86_64</classifier>
</dependency>

On ARM64:

runtimeOnly("io.netty.incubator:netty-incubator-transport-native-io_uring::linux-aarch_64")
<dependency>
    <groupId>io.netty.incubator</groupId>
    <artifactId>netty-incubator-transport-native-io_uring</artifactId>
    <scope>runtime</scope>
    <classifier>linux-aarch_64</classifier>
</dependency>

Netty enables simplistic sampling resource leak detection which reports there is a leak or not, at the cost of small overhead. You can enable it by setting property netty.resource-leak-detector-level to one of: DISABLED (default), SIMPLE, PARANOID or ADVANCED.

6.31.1 Configuring Server Thread Pools

The HTTP server is built on Netty which is designed as a non-blocking I/O toolkit in an event loop model.

The Netty worker event loop uses the "default" named event loop group. This can be configured through micronaut.netty.event-loops.default.

The event loop configuration under micronaut.server.netty.worker is only used if the event-loop-group is set to a name which doesn’t correspond to any micronaut.netty.event-loops configuration. This behavior is deprecated and will be removed in a future version. Use micronaut.netty.event-loops.* for any event loop group configuration beyond setting the name through event-loop-group. This does not apply to the parent event loop configuration (micronaut.server.netty.parent).
🔗
Table 1. Configuration Properties for NettyHttpServerConfiguration$Worker
Property Type Description

micronaut.server.netty.worker.event-loop-group

java.lang.String

Sets the name to use.

micronaut.server.netty.worker.threads

int

Sets the number of threads for the event loop group.

micronaut.server.netty.worker.io-ratio

java.lang.Integer

micronaut.server.netty.worker.executor

java.lang.String

Sets the name of the executor.

micronaut.server.netty.worker.prefer-native-transport

boolean

Set whether to prefer the native transport if available

micronaut.server.netty.worker.shutdown-quiet-period

java.time.Duration

Set the shutdown quiet period

micronaut.server.netty.worker.shutdown-timeout

java.time.Duration

Set the shutdown timeout (must be >= shutdownQuietPeriod)

The parent event loop can be configured with micronaut.server.netty.parent with the same configuration options.

The server can also be configured to use a different named worker event loop:

Using a different event loop for the server
micronaut.server.netty.worker.event-loop-group=other
micronaut.netty.event-loops.other.num-threads=10
micronaut:
  server:
    netty:
      worker:
        event-loop-group: other
  netty:
    event-loops:
      other:
        num-threads: 10
[micronaut]
  [micronaut.server]
    [micronaut.server.netty]
      [micronaut.server.netty.worker]
        event-loop-group="other"
  [micronaut.netty]
    [micronaut.netty.event-loops]
      [micronaut.netty.event-loops.other]
        num-threads=10
micronaut {
  server {
    netty {
      worker {
        eventLoopGroup = "other"
      }
    }
  }
  netty {
    eventLoops {
      other {
        numThreads = 10
      }
    }
  }
}
{
  micronaut {
    server {
      netty {
        worker {
          event-loop-group = "other"
        }
      }
    }
    netty {
      event-loops {
        other {
          num-threads = 10
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "netty": {
        "worker": {
          "event-loop-group": "other"
        }
      }
    },
    "netty": {
      "event-loops": {
        "other": {
          "num-threads": 10
        }
      }
    }
  }
}
The default value for the number of threads is the value of the system property io.netty.eventLoopThreads, or if not specified, the available processors x 2.

See the following table for configuring event loops:

🔗
Table 2. Configuration Properties for DefaultEventLoopGroupConfiguration
Property Type Description

micronaut.netty.event-loops.*.num-threads

int

The number of threads

micronaut.netty.event-loops.*.io-ratio

java.lang.Integer

The IO ratio (optional)

micronaut.netty.event-loops.*.prefer-native-transport

boolean

Whether native transport is to be preferred

micronaut.netty.event-loops.*.executor

java.lang.String

A named executor service to use (optional)

micronaut.netty.event-loops.*.shutdown-quiet-period

java.time.Duration

The shutdown quiet period

micronaut.netty.event-loops.*.shutdown-timeout

java.time.Duration

The shutdown timeout (must be >= shutdownQuietPeriod)

6.31.1.1 Blocking Operations

When dealing with blocking operations, the Micronaut framework shifts the blocking operations to an unbound, caching I/O thread pool by default. You can configure the I/O thread pool using the ExecutorConfiguration named io. For example:

Configuring the Server I/O Thread Pool
micronaut.executors.io.type=fixed
micronaut.executors.io.nThreads=75
micronaut:
  executors:
    io:
      type: fixed
      nThreads: 75
[micronaut]
  [micronaut.executors]
    [micronaut.executors.io]
      type="fixed"
      nThreads=75
micronaut {
  executors {
    io {
      type = "fixed"
      nThreads = 75
    }
  }
}
{
  micronaut {
    executors {
      io {
        type = "fixed"
        nThreads = 75
      }
    }
  }
}
{
  "micronaut": {
    "executors": {
      "io": {
        "type": "fixed",
        "nThreads": 75
      }
    }
  }
}

The above configuration creates a fixed thread pool with 75 threads.

6.31.1.2 Virtual Threads

Since Java 19, the JVM includes experimental support for virtual threads ("project loom"). As it is a preview feature, you need to pass --enable-preview as a JVM parameter to enable it.

The Micronaut framework will detect virtual thread support and use it for the executor named blocking if available. If virtual threads are not supported, this executor will be aliased to the io thread pool.

To use the blocking executor, simply mark e.g. a controller with ExecuteOn:

Configuring the Server I/O Thread Pool
@Controller("/hello")
class HelloWorldController {

    @ExecuteOn(TaskExecutors.BLOCKING)
    @Produces(MediaType.TEXT_PLAIN)
    @Get("/world")
    String index() {
        return "Hello World";
    }
}
Configuring the Server I/O Thread Pool
@Controller("/hello")
class HelloWorldController {

    @ExecuteOn(TaskExecutors.BLOCKING)
    @Produces(MediaType.TEXT_PLAIN)
    @Get("/world")
    String index() {
        "Hello World"
    }
}
Configuring the Server I/O Thread Pool
@Controller("/hello")
class HelloWorldController {
    @ExecuteOn(TaskExecutors.BLOCKING)
    @Produces(MediaType.TEXT_PLAIN)
    @Get("/world")
    fun index() = "Hello World"
}

6.31.1.3 @Blocking

You can use the @Blocking annotation to mark methods as blocking.

If you set micronaut.server.thread-selection to AUTO, the Micronaut framework offloads the execution of methods annotated with @Blocking to the IO thread pool (See: TaskExecutors).

@Blocking only works if you are using AUTO thread selection. Micronaut framework defaults to MANUAL thread selection since Micronaut 2.0. We recommend the usage of @ExecuteOn annotation to execute the blocking operations on a different thread pool. @ExecutesOn works for both MANUAL and AUTO thread selection.

There are some places where the Micronaut framework uses @Blocking internally:

Blocking Type Description

BlockingHttpClient

Intended for testing, provides blocking versions for a subset of HttpClient operations.

IOUtils

Reads the contents of a BufferedReader in a blocking manner, and returns that as a String.

BootstrapPropertySourceLocator

Resolves either remote or local PropertySource instances for the current Environment.

Micronaut Data also utilizes @Blocking internally for some transaction operations, CRUD interceptors, and repositories.

6.31.2 Configuring the Netty Client Pipeline

You can customize the Netty client pipeline by writing a Bean Event Listener that listens for the creation of a Registry.

The ChannelPipelineCustomizer interface defines constants for the names of the various handlers that the Micronaut framework registers.

As an example the following code sample demonstrates registering the Logbook library which includes additional Netty handlers to perform request and response logging:

Customizing the Netty server pipeline for Logbook
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import io.micronaut.http.client.netty.NettyClientCustomizer;
import io.micronaut.http.netty.channel.ChannelPipelineCustomizer;
import io.netty.channel.Channel;
import jakarta.inject.Singleton;
import org.zalando.logbook.Logbook;
import org.zalando.logbook.netty.LogbookClientHandler;

@Requires(beans = Logbook.class)
@Singleton
public class LogbookNettyClientCustomizer
    implements BeanCreatedEventListener<NettyClientCustomizer.Registry> { // (1)
    private final Logbook logbook;

    public LogbookNettyClientCustomizer(Logbook logbook) {
        this.logbook = logbook;
    }

    @Override
    public NettyClientCustomizer.Registry onCreated(
        BeanCreatedEvent<NettyClientCustomizer.Registry> event) {

        NettyClientCustomizer.Registry registry = event.getBean();
        registry.register(new Customizer(null)); // (2)
        return registry;
    }

    private class Customizer implements NettyClientCustomizer { // (3)
        private final Channel channel;

        Customizer(Channel channel) {
            this.channel = channel;
        }

        @Override
        public NettyClientCustomizer specializeForChannel(Channel channel, ChannelRole role) {
            return new Customizer(channel); // (4)
        }

        @Override
        public void onRequestPipelineBuilt() {
            channel.pipeline().addBefore( // (5)
                ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE,
                "logbook",
                new LogbookClientHandler(logbook)
            );
        }
    }
}
Customizing the Netty server pipeline for Logbook
import io.micronaut.http.client.netty.NettyClientCustomizer
import io.micronaut.http.netty.channel.ChannelPipelineCustomizer
import io.netty.channel.Channel
import jakarta.inject.Singleton
import org.zalando.logbook.Logbook
import org.zalando.logbook.netty.LogbookClientHandler


@Requires(beans = Logbook.class)
@Singleton
class LogbookNettyClientCustomizer
        implements BeanCreatedEventListener<NettyClientCustomizer.Registry> { // (1)
    private final Logbook logbook;

    LogbookNettyClientCustomizer(Logbook logbook) {
        this.logbook = logbook
    }

    @Override
    NettyClientCustomizer.Registry onCreated(
            BeanCreatedEvent<NettyClientCustomizer.Registry> event) {

        NettyClientCustomizer.Registry registry = event.getBean()
        registry.register(new Customizer(null)) // (2)
        return registry
    }

    private class Customizer implements NettyClientCustomizer { // (3)
        private final Channel channel

        Customizer(Channel channel) {
            this.channel = channel
        }

        @Override
        NettyClientCustomizer specializeForChannel(Channel channel, ChannelRole role) {
            return new Customizer(channel) // (4)
        }

        @Override
        void onRequestPipelineBuilt() {
            channel.pipeline().addBefore( // (5)
                    ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE,
                    "logbook",
                    new LogbookClientHandler(logbook)
            )
        }
    }
}
Customizing the Netty server pipeline for Logbook
import io.micronaut.context.annotation.Requires
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import io.micronaut.http.client.netty.NettyClientCustomizer
import io.micronaut.http.client.netty.NettyClientCustomizer.ChannelRole
import io.micronaut.http.netty.channel.ChannelPipelineCustomizer
import io.netty.channel.Channel
import jakarta.inject.Singleton
import org.zalando.logbook.Logbook
import org.zalando.logbook.netty.LogbookClientHandler

@Requires(beans = [Logbook::class])
@Singleton
class LogbookNettyClientCustomizer(private val logbook: Logbook) :
    BeanCreatedEventListener<NettyClientCustomizer.Registry> { // (1)

    override fun onCreated(event: BeanCreatedEvent<NettyClientCustomizer.Registry>): NettyClientCustomizer.Registry {
        val registry = event.bean
        registry.register(Customizer(null)) // (2)
        return registry
    }

    private inner class Customizer constructor(private val channel: Channel?) :
        NettyClientCustomizer { // (3)

        override fun specializeForChannel(channel: Channel, role: ChannelRole) = Customizer(channel) // (4)

        override fun onRequestPipelineBuilt() {
            channel!!.pipeline().addBefore( // (5)
                ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE,
                "logbook",
                LogbookClientHandler(logbook)
            )
        }
    }
}
1 LogbookNettyClientCustomizer listens for a Registry and requires the definition of a Logbook bean
2 The root customizer is initialized without a channel and registered
3 The actual customizer implements NettyClientCustomizer
4 When a new channel is created, a new, specialized customizer is created for that channel
5 When the client signals that the stream pipeline has been fully constructed, the logbook handler is registered
Logbook has a major bug that limits its usefulness with netty.

6.31.3 Configuring the Netty Server Pipeline

You can customize the Netty server pipeline by writing a Bean Event Listener that listens for the creation of Registry.

The ChannelPipelineCustomizer interface defines constants for the names of the various handlers the Micronaut framework registers.

As an example the following code sample demonstrates registering the Logbook library which includes additional Netty handlers to perform request and response logging:

Customizing the Netty server pipeline for Logbook
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import io.micronaut.http.netty.channel.ChannelPipelineCustomizer;
import io.micronaut.http.server.netty.NettyServerCustomizer;
import io.netty.channel.Channel;
import jakarta.inject.Singleton;
import org.zalando.logbook.Logbook;
import org.zalando.logbook.netty.LogbookServerHandler;

@Requires(beans = Logbook.class)
@Singleton
public class LogbookNettyServerCustomizer
    implements BeanCreatedEventListener<NettyServerCustomizer.Registry> { // (1)
    private final Logbook logbook;

    public LogbookNettyServerCustomizer(Logbook logbook) {
        this.logbook = logbook;
    }

    @Override
    public NettyServerCustomizer.Registry onCreated(
        BeanCreatedEvent<NettyServerCustomizer.Registry> event) {

        NettyServerCustomizer.Registry registry = event.getBean();
        registry.register(new Customizer(null)); // (2)
        return registry;
    }

    private class Customizer implements NettyServerCustomizer { // (3)
        private final Channel channel;

        Customizer(Channel channel) {
            this.channel = channel;
        }

        @Override
        public NettyServerCustomizer specializeForChannel(Channel channel, ChannelRole role) {
            return new Customizer(channel); // (4)
        }

        @Override
        public void onStreamPipelineBuilt() {
            channel.pipeline().addBefore( // (5)
                ChannelPipelineCustomizer.HANDLER_MICRONAUT_INBOUND,
                "logbook",
                new LogbookServerHandler(logbook)
            );
        }
    }
}
Customizing the Netty server pipeline for Logbook
import io.micronaut.context.annotation.Requires
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import io.micronaut.http.netty.channel.ChannelPipelineCustomizer
import io.micronaut.http.server.netty.NettyServerCustomizer
import io.netty.channel.Channel
import org.zalando.logbook.Logbook
import org.zalando.logbook.netty.LogbookServerHandler

import jakarta.inject.Singleton

@Requires(beans = Logbook.class)
@Singleton
class LogbookNettyServerCustomizer
        implements BeanCreatedEventListener<NettyServerCustomizer.Registry> { // (1)
    private final Logbook logbook;

    LogbookNettyServerCustomizer(Logbook logbook) {
        this.logbook = logbook
    }

    @Override
    NettyServerCustomizer.Registry onCreated(
            BeanCreatedEvent<NettyServerCustomizer.Registry> event) {

        NettyServerCustomizer.Registry registry = event.getBean()
        registry.register(new Customizer(null)) // (2)
        return registry
    }

    private class Customizer implements NettyServerCustomizer { // (3)
        private final Channel channel

        Customizer(Channel channel) {
            this.channel = channel
        }

        @Override
        NettyServerCustomizer specializeForChannel(Channel channel, ChannelRole role) {
            return new Customizer(channel) // (4)
        }

        @Override
        void onStreamPipelineBuilt() {
            channel.pipeline().addBefore( // (5)
                    ChannelPipelineCustomizer.HANDLER_HTTP_STREAM,
                    "logbook",
                    new LogbookServerHandler(logbook)
            )
        }
    }
}
Customizing the Netty server pipeline for Logbook
import io.micronaut.context.annotation.Requires
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import io.micronaut.http.netty.channel.ChannelPipelineCustomizer
import io.micronaut.http.server.netty.NettyServerCustomizer
import io.micronaut.http.server.netty.NettyServerCustomizer.ChannelRole
import io.netty.channel.Channel
import jakarta.inject.Singleton
import org.zalando.logbook.Logbook
import org.zalando.logbook.netty.LogbookServerHandler

@Requires(beans = [Logbook::class])
@Singleton
class LogbookNettyServerCustomizer(private val logbook: Logbook) :
    BeanCreatedEventListener<NettyServerCustomizer.Registry> { // (1)

    override fun onCreated(event: BeanCreatedEvent<NettyServerCustomizer.Registry>): NettyServerCustomizer.Registry {
        val registry = event.bean
        registry.register(Customizer(null)) // (2)
        return registry
    }

    private inner class Customizer constructor(private val channel: Channel?) :
        NettyServerCustomizer { // (3)

        override fun specializeForChannel(channel: Channel, role: ChannelRole) = Customizer(channel) // (4)

        override fun onStreamPipelineBuilt() {
            channel!!.pipeline().addBefore( // (5)
                ChannelPipelineCustomizer.HANDLER_MICRONAUT_HTTP_RESPONSE,
                "logbook",
                LogbookServerHandler(logbook)
            )
        }
    }
}
1 LogbookNettyServerCustomizer listens for a Registry and requires the definition of a Logbook bean
2 The root customizer is initialized without a channel and registered
3 The actual customizer implements NettyServerCustomizer
4 When a new channel is created, a new, specialized customizer is created for that channel
5 When the server signals that the stream pipeline has been fully constructed, the logbook handler is registered
Logbook has a major bug that limits its usefulness with netty.

6.31.4 Advanced Listener Configuration

Instead of configuring a single port, you can also specify each listener manually.

micronaut.server.netty.listeners.httpListener.host=127.0.0.1
micronaut.server.netty.listeners.httpListener.port=8086
micronaut.server.netty.listeners.httpListener.ssl=false
micronaut.server.netty.listeners.httpsListener.port=8087
micronaut.server.netty.listeners.httpsListener.ssl=true
micronaut:
  server:
    netty:
      listeners:
        httpListener:
          host: 127.0.0.1
          port: 8086
          ssl: false
        httpsListener:
          port: 8087
          ssl: true
[micronaut]
  [micronaut.server]
    [micronaut.server.netty]
      [micronaut.server.netty.listeners]
        [micronaut.server.netty.listeners.httpListener]
          host="127.0.0.1"
          port=8086
          ssl=false
        [micronaut.server.netty.listeners.httpsListener]
          port=8087
          ssl=true
micronaut {
  server {
    netty {
      listeners {
        httpListener {
          host = "127.0.0.1"
          port = 8086
          ssl = false
        }
        httpsListener {
          port = 8087
          ssl = true
        }
      }
    }
  }
}
{
  micronaut {
    server {
      netty {
        listeners {
          httpListener {
            host = "127.0.0.1"
            port = 8086
            ssl = false
          }
          httpsListener {
            port = 8087
            ssl = true
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "netty": {
        "listeners": {
          "httpListener": {
            "host": "127.0.0.1",
            "port": 8086,
            "ssl": false
          },
          "httpsListener": {
            "port": 8087,
            "ssl": true
          }
        }
      }
    }
  }
}
  • httpListener is a listener name, and can be an arbitrary value

  • host is optional, and by default binds to all interfaces

If you specify listeners manually, other configuration such as micronaut.server.port will be ignored.

SSL can be enabled or disabled for each listener individually. When enabled, the SSL will be configured as described above.

The embedded server also supports binding to unix domain sockets using netty. This requires the following dependency:

implementation("io.netty:netty-transport-native-unix-common")
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport-native-unix-common</artifactId>
</dependency>

The server must also be configured to use native transport (epoll or kqueue).

micronaut.server.netty.listeners.unixListener.family=UNIX
micronaut.server.netty.listeners.unixListener.path=/run/micronaut.socket
micronaut.server.netty.listeners.unixListener.ssl=true
micronaut:
  server:
    netty:
      listeners:
        unixListener:
          family: UNIX
          path: /run/micronaut.socket
          ssl: true
[micronaut]
  [micronaut.server]
    [micronaut.server.netty]
      [micronaut.server.netty.listeners]
        [micronaut.server.netty.listeners.unixListener]
          family="UNIX"
          path="/run/micronaut.socket"
          ssl=true
micronaut {
  server {
    netty {
      listeners {
        unixListener {
          family = "UNIX"
          path = "/run/micronaut.socket"
          ssl = true
        }
      }
    }
  }
}
{
  micronaut {
    server {
      netty {
        listeners {
          unixListener {
            family = "UNIX"
            path = "/run/micronaut.socket"
            ssl = true
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "netty": {
        "listeners": {
          "unixListener": {
            "family": "UNIX",
            "path": "/run/micronaut.socket",
            "ssl": true
          }
        }
      }
    }
  }
}
  • unixListener is a listener name, and can be an arbitrary value

To use an abstract domain socket instead of a normal one, prefix the path with a NUL character, like "\0/run/micronaut.socket"

systemd socket activation support

When using the epoll transport, the HTTP server can be configured to use an existing file descriptor. With this feature, you can use socket created by systemd and passed to the micronaut process:

micronaut.netty.event-loops.parent.prefer-native-transport=true
micronaut.netty.event-loops.default.prefer-native-transport=true
micronaut.netty.listeners.systemd.fd=3
micronaut.netty.listeners.systemd.bind=false
micronaut:
  netty:
    event-loops:
      # use epoll
      parent:
        prefer-native-transport: true
      default:
        prefer-native-transport: true
    listeners:
      systemd:
        fd: 3 # systemd passes the server socket as fd 3
        bind: false # do not bind again, systemd already did this
[micronaut]
  [micronaut.netty]
    [micronaut.netty.event-loops]
      [micronaut.netty.event-loops.parent]
        prefer-native-transport=true
      [micronaut.netty.event-loops.default]
        prefer-native-transport=true
    [micronaut.netty.listeners]
      [micronaut.netty.listeners.systemd]
        fd=3
        bind=false
micronaut {
  netty {
    eventLoops {
      parent {
        preferNativeTransport = true
      }
      'default' {
        preferNativeTransport = true
      }
    }
    listeners {
      systemd {
        fd = 3
        bind = false
      }
    }
  }
}
{
  micronaut {
    netty {
      event-loops {
        parent {
          prefer-native-transport = true
        }
        default {
          prefer-native-transport = true
        }
      }
      listeners {
        systemd {
          fd = 3
          bind = false
        }
      }
    }
  }
}
{
  "micronaut": {
    "netty": {
      "event-loops": {
        "parent": {
          "prefer-native-transport": true
        },
        "default": {
          "prefer-native-transport": true
        }
      },
      "listeners": {
        "systemd": {
          "fd": 3,
          "bind": false
        }
      }
    }
  }
}

Example app.service file:

[Unit]
Description=Micronaut HTTP server with socket activation
After=network.target app.socket
Requires=app.socket

[Service]
Type=simple
ExecStart=/usr/bin/java -jar /app.jar

Example app.socket file:

[Socket]
ListenStream=127.0.0.1:8080

[Install]
WantedBy=sockets.target

Now your Micronaut application can be started by a client connecting to http://127.0.0.1:8080.

6.31.5 Configuring CORS

The Micronaut framework supports CORS (Cross Origin Resource Sharing) out of the box. By default, CORS requests are rejected.

See the guide for Configure CORS in a Micronaut Application to learn more.

6.31.5.1 Annotation-based CORS Configuration

Micronaut CORS configuration applies by default to all endpoints in the running application.

As an alternative, CORS configuration can be applied in a more fine-grained manner to specific routes using the @CrossOrigin annotation. This is applied to @Controller to apply the CORS configuration to all endpoints in the controller. Alternatively, the annotation can be applied to specific endpoints on a controller, for even more fine-grained control.

The @CrossOrigin annotation maps with a one-to-one correspondence to application-wide CorsOriginConfiguration configuration properties. To specify just an allowed origin, use @CrossOrigin("https://foo.com"). To specify additional configuration details, use a combination of annotation attributes the same as you would for specifying global CorsOriginConfiguration properties.

@CrossOrigin(
	allowedOrigins = { "http://foo.com" },
	allowedOriginsRegex = "^http(|s):\\/\\/www\\.google\\.com$",
	allowedMethods = { HttpMethod.POST, HttpMethod.PUT },
	allowedHeaders = { HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION },
	exposedHeaders = { HttpHeaders.CONTENT_TYPE, HttpHeaders.AUTHORIZATION },
	allowCredentials = false,
	allowPrivateNetwork = false,
	maxAge = 3600
)

The following example demonstrates how the annotation might be applied to a specific endpoint. To enable CORS for all endpoints in the controller, move the annotation to the class level and configure it appropriately.

@CrossOrigin Example
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.cors.CrossOrigin;

@Controller("/hello")
public class CorsController {
    @CrossOrigin("https://myui.com") // (1)
    @Get(produces = MediaType.TEXT_PLAIN) // (2)
    public String cors() {
        return "Welcome to the worlds of CORS";
    }

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/nocors") // (3)
    public String nocorstoday() {
        return "No more CORS for you";
    }
}
@CrossOrigin Example
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import io.micronaut.http.server.cors.CrossOrigin

@Controller("/hello")
class CorsController {
    @CrossOrigin("https://myui.com") // (1)
    @Get(produces = MediaType.TEXT_PLAIN) // (2)
    String cors() {
        return "Welcome to the worlds of CORS"
    }

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/nocors") // (3)
    String nocorstoday() {
        return "No more CORS for you"
    }
}
@CrossOrigin Example
import io.micronaut.context.annotation.Requires
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import io.micronaut.http.server.cors.CrossOrigin

@Controller("/hello")
class CorsController {
    @CrossOrigin("https://myui.com") // (1)
    @Get(produces = [MediaType.TEXT_PLAIN]) // (2)
    fun cors(): String {
        return "Welcome to the worlds of CORS"
    }

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/nocors") // (3)
    fun nocorstoday(): String {
        return "No more CORS for you"
    }
}
1 The @CrossOrigin annotation is applied to a specific endpoint, making the CORS configuration fine-grained.
2 The GET /hello endpoint has "https://myui.com" as an allowed cross-origin endpoint
3 The GET /hello/nocors endpoint cannot use "https://myui.com" as an origin, since it doesn’t have a CORS configuration that allows it.
The @CrossOrigin annotation uses the same defaults as the application configuration alternative, when corresponding annotation attributes are not set. See CORS configuration for details.

6.31.5.2 CORS via Configuration

To enable processing of CORS requests, modify your configuration in the application configuration file:

CORS Configuration Example
micronaut.server.cors.enabled=true
micronaut:
  server:
    cors:
      enabled: true
[micronaut]
  [micronaut.server]
    [micronaut.server.cors]
      enabled=true
micronaut {
  server {
    cors {
      enabled = true
    }
  }
}
{
  micronaut {
    server {
      cors {
        enabled = true
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "cors": {
        "enabled": true
      }
    }
  }
}

By only enabling CORS processing, a "wide open" strategy is adopted that allows requests from any origin.

To change the settings for all origins or a specific origin, change the configuration to provide one or more "configurations". By providing any configuration, the default "wide open" configuration is not configured.

CORS Configurations
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.all=...
micronaut.server.cors.configurations.web=...
micronaut.server.cors.configurations.mobile=...
micronaut:
  server:
    cors:
      enabled: true
      configurations:
        all:
          ...
        web:
          ...
        mobile:
          ...
[micronaut]
  [micronaut.server]
    [micronaut.server.cors]
      enabled=true
      [micronaut.server.cors.configurations]
        all="..."
        web="..."
        mobile="..."
micronaut {
  server {
    cors {
      enabled = true
      configurations {
        all = "..."
        web = "..."
        mobile = "..."
      }
    }
  }
}
{
  micronaut {
    server {
      cors {
        enabled = true
        configurations {
          all = "..."
          web = "..."
          mobile = "..."
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "cors": {
        "enabled": true,
        "configurations": {
          "all": "...",
          "web": "...",
          "mobile": "..."
        }
      }
    }
  }
}

In the above example, three configurations are provided. Their names (all, web, mobile) are not important and have no significance inside Micronaut. They are there purely to be able to easily recognize the intended user of the configuration.

The same configuration properties can be applied to each configuration. See CorsOriginConfiguration for properties that can be defined. The values of each configuration supplied will default to the default values of the corresponding fields.

When a CORS request is made, configurations are searched for allowed origins that match exactly or match the request origin through a regular expression.

6.31.5.3 Allowed Origins

Don’t define allowed-origins or allowed-origins-regex to allow any origin for a given configuration.

For multiple valid origins, set the allowed-origins key of the configuration to a list of strings.

You can also define via allowed-origins-regex a regular expression (^http(|s)://www\.google\.com$). The Regular expression is passed to Pattern#compile and compared to the request origin with Matcher#matches.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.allowed-origins-regex=^http(|s):\/\/www\.google\.com$
micronaut.server.cors.configurations.web.allowed-origins[0]=http://foo.com
micronaut:
  server:
    cors:
      enabled: true
      configurations:
        web:
          allowed-origins-regex: '^http(|s):\/\/www\.google\.com$'
          allowed-origins:
            - http://foo.com
[micronaut]
  [micronaut.server]
    [micronaut.server.cors]
      enabled=true
      [micronaut.server.cors.configurations]
        [micronaut.server.cors.configurations.web]
          allowed-origins-regex="^http(|s):\\/\\/www\\.google\\.com$"
          allowed-origins=[
            "http://foo.com"
          ]
micronaut {
  server {
    cors {
      enabled = true
      configurations {
        web {
          allowedOriginsRegex = "^http(|s):\\/\\/www\\.google\\.com$"
          allowedOrigins = ["http://foo.com"]
        }
      }
    }
  }
}
{
  micronaut {
    server {
      cors {
        enabled = true
        configurations {
          web {
            allowed-origins-regex = "^http(|s):\\/\\/www\\.google\\.com$"
            allowed-origins = ["http://foo.com"]
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "cors": {
        "enabled": true,
        "configurations": {
          "web": {
            "allowed-origins-regex": "^http(|s):\\/\\/www\\.google\\.com$",
            "allowed-origins": ["http://foo.com"]
          }
        }
      }
    }
  }
}
Use the allowed-origins-regex configuration judiciously. You may accidentally make an insecure configuration which could be targeted by an attacker registering domains targeting the regular expression.
allowed-origins-regex and allowed-origins can be combined. However, if using the former and the latter is not set explicitly then allowed-origins defaults to none rather than any to avoid unexpected allowed origins.

6.31.5.4 Allowed Methods

To allow any request method for a given configuration, don’t include the allowed-methods key in your configuration.

For multiple allowed methods, set the allowed-methods key of the configuration to a list of strings.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.allowed-methods[0]=POST
micronaut.server.cors.configurations.web.allowed-methods[1]=PUT
micronaut:
  server:
    cors:
      enabled: true
      configurations:
        web:
          allowed-methods:
            - POST
            - PUT
[micronaut]
  [micronaut.server]
    [micronaut.server.cors]
      enabled=true
      [micronaut.server.cors.configurations]
        [micronaut.server.cors.configurations.web]
          allowed-methods=[
            "POST",
            "PUT"
          ]
micronaut {
  server {
    cors {
      enabled = true
      configurations {
        web {
          allowedMethods = ["POST", "PUT"]
        }
      }
    }
  }
}
{
  micronaut {
    server {
      cors {
        enabled = true
        configurations {
          web {
            allowed-methods = ["POST", "PUT"]
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "cors": {
        "enabled": true,
        "configurations": {
          "web": {
            "allowed-methods": ["POST", "PUT"]
          }
        }
      }
    }
  }
}

6.31.5.5 Allowed Headers

To allow any request header for a given configuration, don’t include the allowed-headers key in your configuration.

For multiple allowed headers, set the allowed-headers key of the configuration to a list of strings.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.allowed-headers[0]=Content-Type
micronaut.server.cors.configurations.web.allowed-headers[1]=Authorization
micronaut:
  server:
    cors:
      enabled: true
      configurations:
        web:
          allowed-headers:
            - Content-Type
            - Authorization
[micronaut]
  [micronaut.server]
    [micronaut.server.cors]
      enabled=true
      [micronaut.server.cors.configurations]
        [micronaut.server.cors.configurations.web]
          allowed-headers=[
            "Content-Type",
            "Authorization"
          ]
micronaut {
  server {
    cors {
      enabled = true
      configurations {
        web {
          allowedHeaders = ["Content-Type", "Authorization"]
        }
      }
    }
  }
}
{
  micronaut {
    server {
      cors {
        enabled = true
        configurations {
          web {
            allowed-headers = ["Content-Type", "Authorization"]
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "cors": {
        "enabled": true,
        "configurations": {
          "web": {
            "allowed-headers": ["Content-Type", "Authorization"]
          }
        }
      }
    }
  }
}

6.31.5.6 Exposed Headers

To configure the headers that are sent in the response to a CORS request through the Access-Control-Expose-Headers header, include a list of strings for the exposed-headers key in your configuration. None are exposed by default.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.exposed-headers[0]=Content-Type
micronaut.server.cors.configurations.web.exposed-headers[1]=Authorization
micronaut:
  server:
    cors:
      enabled: true
      configurations:
        web:
          exposed-headers:
            - Content-Type
            - Authorization
[micronaut]
  [micronaut.server]
    [micronaut.server.cors]
      enabled=true
      [micronaut.server.cors.configurations]
        [micronaut.server.cors.configurations.web]
          exposed-headers=[
            "Content-Type",
            "Authorization"
          ]
micronaut {
  server {
    cors {
      enabled = true
      configurations {
        web {
          exposedHeaders = ["Content-Type", "Authorization"]
        }
      }
    }
  }
}
{
  micronaut {
    server {
      cors {
        enabled = true
        configurations {
          web {
            exposed-headers = ["Content-Type", "Authorization"]
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "cors": {
        "enabled": true,
        "configurations": {
          "web": {
            "exposed-headers": ["Content-Type", "Authorization"]
          }
        }
      }
    }
  }
}

6.31.5.7 Allow Credentials

Credentials are allowed by default for CORS requests. To disallow credentials, set the allow-credentials option to false.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.allow-credentials=false
micronaut:
  server:
    cors:
      enabled: true
      configurations:
        web:
          allow-credentials: false
[micronaut]
  [micronaut.server]
    [micronaut.server.cors]
      enabled=true
      [micronaut.server.cors.configurations]
        [micronaut.server.cors.configurations.web]
          allow-credentials=false
micronaut {
  server {
    cors {
      enabled = true
      configurations {
        web {
          allowCredentials = false
        }
      }
    }
  }
}
{
  micronaut {
    server {
      cors {
        enabled = true
        configurations {
          web {
            allow-credentials = false
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "cors": {
        "enabled": true,
        "configurations": {
          "web": {
            "allow-credentials": false
          }
        }
      }
    }
  }
}

6.31.5.8 Allow Private Network

Access from private network is allowed by default for CORS requests. To disallow acces from local network, set the allow-private-network option to false.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.allow-private-network=false
micronaut:
  server:
    cors:
      enabled: true
      configurations:
        web:
          allow-private-network: false
[micronaut]
  [micronaut.server]
    [micronaut.server.cors]
      enabled=true
      [micronaut.server.cors.configurations]
        [micronaut.server.cors.configurations.web]
          allow-private-network=false
micronaut {
  server {
    cors {
      enabled = true
      configurations {
        web {
          allowPrivateNetwork = false
        }
      }
    }
  }
}
{
  micronaut {
    server {
      cors {
        enabled = true
        configurations {
          web {
            allow-private-network = false
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "cors": {
        "enabled": true,
        "configurations": {
          "web": {
            "allow-private-network": false
          }
        }
      }
    }
  }
}

6.31.5.9 Max Age

The default maximum age that preflight requests can be cached is 30 minutes. To change that behavior, specify a value in seconds.

Example CORS Configuration
micronaut.server.cors.enabled=true
micronaut.server.cors.configurations.web.max-age=3600
micronaut:
  server:
    cors:
      enabled: true
      configurations:
        web:
          max-age: 3600 # 1 hour
[micronaut]
  [micronaut.server]
    [micronaut.server.cors]
      enabled=true
      [micronaut.server.cors.configurations]
        [micronaut.server.cors.configurations.web]
          max-age=3600
micronaut {
  server {
    cors {
      enabled = true
      configurations {
        web {
          maxAge = 3600
        }
      }
    }
  }
}
{
  micronaut {
    server {
      cors {
        enabled = true
        configurations {
          web {
            max-age = 3600
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "cors": {
        "enabled": true,
        "configurations": {
          "web": {
            "max-age": 3600
          }
        }
      }
    }
  }
}

6.31.5.10 Multiple Header Values

By default, when a header has multiple values, multiple headers are sent, each with a single value. It is possible to change the behavior to send a single header with a comma-separated list of values by setting a configuration option.

micronaut.server.cors.single-header=true
micronaut:
  server:
    cors:
      single-header: true
[micronaut]
  [micronaut.server]
    [micronaut.server.cors]
      single-header=true
micronaut {
  server {
    cors {
      singleHeader = true
    }
  }
}
{
  micronaut {
    server {
      cors {
        single-header = true
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "cors": {
        "single-header": true
      }
    }
  }
}

6.31.6 Securing the Server with HTTPS

The Micronaut framework supports HTTPS out of the box. By default, HTTPS is disabled and all requests are served using HTTP. To enable HTTPS support, modify your configuration. For example:

HTTPS Configuration Example
micronaut.server.ssl.enabled=true
micronaut.server.ssl.buildSelfSigned=true
micronaut:
  server:
    ssl:
      enabled: true
      buildSelfSigned: true
[micronaut]
  [micronaut.server]
    [micronaut.server.ssl]
      enabled=true
      buildSelfSigned=true
micronaut {
  server {
    ssl {
      enabled = true
      buildSelfSigned = true
    }
  }
}
{
  micronaut {
    server {
      ssl {
        enabled = true
        buildSelfSigned = true
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "ssl": {
        "enabled": true,
        "buildSelfSigned": true
      }
    }
  }
}
  • The Micronaut framework will create a self-signed certificate.

By default, a Micronaut application with HTTPS support starts on port 8443 but you can change the port with the property micronaut.server.ssl.port.

For generating self-signed certificates, the Micronaut HTTP server will use netty. Netty uses one of two approaches to generate the certificate.

If you use a pre-generated certificate (as you should, for security), these steps are not necessary.

  • Netty can use the JDK-internal sun.security.x509 package. On newer JDK versions, this package is restricted and may not work. You may need to add --add-exports=java.base/sun.security.x509=ALL-UNNAMED as a VM parameter.

  • Alternatively, netty will use the Bouncy Castle BCPKIX API. This needs an additional dependency:

implementation("org.bouncycastle:bcpkix-jdk15on")
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
</dependency>

This configuration will generate a warning in the browser.
https warning

Using a valid x509 certificate

It is also possible to configure a Micronaut application to use an existing valid x509 certificate, for example one created with Let’s Encrypt. You will need the server.crt and server.key files and to convert them to a PKCS #12 file.

$ openssl pkcs12 -export \
                 -in server.crt \ (1)
                 -inkey server.key \ (2)
                 -out server.p12 \ (3)
                 -name someAlias \ (4)
                 -chain -CAfile ca.crt -caname root
1 The original server.crt file
2 The original server.key file
3 The server.p12 file to create
4 The alias for the certificate

During the creation of the server.p12 file it is necessary to define a password that will be required later when using the certificate in Micronaut.

Now modify your configuration:

HTTPS Configuration Example
micronaut.ssl.enabled=true
micronaut.ssl.key-store.path=classpath:server.p12
micronaut.ssl.key-store.password=mypassword
micronaut.ssl.key-store.type=PKCS12
micronaut:
  ssl:
    enabled: true
    key-store:
      path: classpath:server.p12
      password: mypassword
      type: PKCS12
[micronaut]
  [micronaut.ssl]
    enabled=true
    [micronaut.ssl.key-store]
      path="classpath:server.p12"
      password="mypassword"
      type="PKCS12"
micronaut {
  ssl {
    enabled = true
    keyStore {
      path = "classpath:server.p12"
      password = "mypassword"
      type = "PKCS12"
    }
  }
}
{
  micronaut {
    ssl {
      enabled = true
      key-store {
        path = "classpath:server.p12"
        password = "mypassword"
        type = "PKCS12"
      }
    }
  }
}
{
  "micronaut": {
    "ssl": {
      "enabled": true,
      "key-store": {
        "path": "classpath:server.p12",
        "password": "mypassword",
        "type": "PKCS12"
      }
    }
  }
}
  • Specify the p12 file path. It can also be referenced as file:/path/to/the/file

  • Also provide the password defined during the export

With this configuration, if we start a Micronaut application and connect to https://localhost:8443 we still see the warning in the browser, but if we inspect the certificate we can check that it is the one generated by Let’s Encrypt.

https certificate

Finally, we can test that the certificate is valid for the browser by adding an alias to the domain in /etc/hosts file:

$ cat /etc/hosts
...
127.0.0.1   my-domain.org
...

Now we can connect to https://my-domain.org:8443:

https valid certificate

Using Java Keystore (JKS)

Using this type of certificate is not recommended because the format is proprietary - PKCS12 format is preferred. Regardless, the Micronaut framework also supports it.

Convert the p12 certificate to a JKS one:

$ keytool -importkeystore \
          -deststorepass newPassword -destkeypass newPassword \ (1)
          -destkeystore server.keystore \ (2)
          -srckeystore server.p12 -srcstoretype PKCS12 -srcstorepass mypassword \ (3)
          -alias someAlias (4)
1 It is necessary to define the password for the keystore
2 The file to create
3 The PKCS12 file created previously, and the password defined during the creation
4 The alias used before
If either srcstorepass or alias are not the same as defined in the p12 file, the conversion will fail.

Now modify your configuration:

HTTPS Configuration Example
micronaut.ssl.enabled=true
micronaut.ssl.key-store.path=classpath:server.keystore
micronaut.ssl.key-store.password=newPassword
micronaut.ssl.key-store.type=JKS
micronaut:
  ssl:
    enabled: true
    key-store:
      path: classpath:server.keystore
      password: newPassword
      type: JKS
[micronaut]
  [micronaut.ssl]
    enabled=true
    [micronaut.ssl.key-store]
      path="classpath:server.keystore"
      password="newPassword"
      type="JKS"
micronaut {
  ssl {
    enabled = true
    keyStore {
      path = "classpath:server.keystore"
      password = "newPassword"
      type = "JKS"
    }
  }
}
{
  micronaut {
    ssl {
      enabled = true
      key-store {
        path = "classpath:server.keystore"
        password = "newPassword"
        type = "JKS"
      }
    }
  }
}
{
  "micronaut": {
    "ssl": {
      "enabled": true,
      "key-store": {
        "path": "classpath:server.keystore",
        "password": "newPassword",
        "type": "JKS"
      }
    }
  }
}

Start Micronaut, and the application will run on https://localhost:8443 using the certificate in the keystore.

Refreshing/Reloading HTTPS Certificates

Keeping HTTPS certificates up-to-date after expiry can be a challenge. A great solution to this is Automated Certificate Management Environment (ACME) and the Micronaut ACME Module which provides support for automatically refreshing certificates from a certificate authority.

If the use of a certificate authority is not possible, and you need to manually update certificates from disk then you should fire a RefreshEvent using Micronaut’s support for Application Events containing the keys where your HTTPS configuration is defined and the Micronaut framework will reload the certificates from disk and apply the new configuration to the server.

You can also use the Refresh Management Endpoint, however this will only apply if the physical location of certificate on disk has changed

For example the following will reload the previously listed HTTPS configuration from disk and apply it to new incoming requests (this code could run for a scheduled job that polled certificates for changes for example):

Manually Refreshing HTTPS configuration
import jakarta.inject.Inject;
import io.micronaut.context.event.ApplicationEventPublisher;
import io.micronaut.runtime.context.scope.refresh.RefreshEvent;
import java.util.Collections;
...

@Inject ApplicationEventPublisher<RefreshEvent> eventPublisher;

...

eventPublisher.publishEvent(new RefreshEvent(
    Collections.singletonMap("micronaut.ssl", "*")
));

6.31.7 Enabling HTTP and HTTPS

The Micronaut framework supports binding both HTTP and HTTPS. To enable dual protocol support, modify your configuration. For example:

Dual Protocol Configuration Example
micronaut.server.ssl.enabled=true
micronaut.server.ssl.build-self-signed=true
micronaut.server.dual-protocol=true
micronaut:
  server:
    ssl:
      enabled: true
      build-self-signed: true
    dual-protocol : true
[micronaut]
  [micronaut.server]
    dual-protocol=true
    [micronaut.server.ssl]
      enabled=true
      build-self-signed=true
micronaut {
  server {
    ssl {
      enabled = true
      buildSelfSigned = true
    }
    dualProtocol = true
  }
}
{
  micronaut {
    server {
      ssl {
        enabled = true
        build-self-signed = true
      }
      dual-protocol = true
    }
  }
}
{
  "micronaut": {
    "server": {
      "ssl": {
        "enabled": true,
        "build-self-signed": true
      },
      "dual-protocol": true
    }
  }
}
  • You must configure SSL for HTTPS to work. In this example we are just using a self-signed certificate with build-self-signed, but see Securing the Server with HTTPS for other configurations

  • dual-protocol enables both HTTP and HTTPS is an opt-in feature - setting the dualProtocol flag enables it. By default, the Micronaut framework only enables one.

It is also possible to redirect automatically all HTTP request to HTTPS. Besides the previous configuration, you need to enable this option. For example:

Enable HTTP to HTTPS Redirects
micronaut.server.ssl.enabled=true
micronaut.server.ssl.build-self-signed=true
micronaut.server.dual-protocol=true
micronaut.server.http-to-https-redirect=true
micronaut:
  server:
    ssl:
      enabled: true
      build-self-signed: true
    dual-protocol : true
    http-to-https-redirect: true
[micronaut]
  [micronaut.server]
    dual-protocol=true
    http-to-https-redirect=true
    [micronaut.server.ssl]
      enabled=true
      build-self-signed=true
micronaut {
  server {
    ssl {
      enabled = true
      buildSelfSigned = true
    }
    dualProtocol = true
    httpToHttpsRedirect = true
  }
}
{
  micronaut {
    server {
      ssl {
        enabled = true
        build-self-signed = true
      }
      dual-protocol = true
      http-to-https-redirect = true
    }
  }
}
{
  "micronaut": {
    "server": {
      "ssl": {
        "enabled": true,
        "build-self-signed": true
      },
      "dual-protocol": true,
      "http-to-https-redirect": true
    }
  }
}
  • http-to-https-redirect enables HTTP to HTTPS redirects

6.31.8 Enabling Access Logger

In the spirit of apache mod_log_config and Tomcat Access Log Valve, it is possible to enable an access logger for the HTTP server (this works for both HTTP/1 and HTTP/2).

To enable and configure the access logger, in your configuration file (e.g application.yml) set:

Enabling the access logger
micronaut.server.netty.access-logger.enabled=true
micronaut.server.netty.access-logger.logger-name=my-access-logger
micronaut.server.netty.access-logger.log-format=common
micronaut:
  server:
    netty:
      access-logger:
        enabled: true
        logger-name: my-access-logger
        log-format: common
[micronaut]
  [micronaut.server]
    [micronaut.server.netty]
      [micronaut.server.netty.access-logger]
        enabled=true
        logger-name="my-access-logger"
        log-format="common"
micronaut {
  server {
    netty {
      accessLogger {
        enabled = true
        loggerName = "my-access-logger"
        logFormat = "common"
      }
    }
  }
}
{
  micronaut {
    server {
      netty {
        access-logger {
          enabled = true
          logger-name = "my-access-logger"
          log-format = "common"
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "netty": {
        "access-logger": {
          "enabled": true,
          "logger-name": "my-access-logger",
          "log-format": "common"
        }
      }
    }
  }
}
  • enabled Enables the access logger

  • optionally specify a logger-name, which defaults to HTTP_ACCESS_LOGGER

  • optionally specify a log-format, which defaults to the Common Log Format

Filtering access logs

If you wish to not log access to certain paths, you can specify regular expression filters in the configuration:

Filtering the access logs
micronaut.server.netty.access-logger.enabled=true
micronaut.server.netty.access-logger.logger-name=my-access-logger
micronaut.server.netty.access-logger.log-format=common
micronaut.server.netty.access-logger.exclusions[0]=/health
micronaut.server.netty.access-logger.exclusions[1]=/path/.+
micronaut:
  server:
    netty:
      access-logger:
        enabled: true
        logger-name: my-access-logger
        log-format: common
        exclusions:
          - /health
          - /path/.+
[micronaut]
  [micronaut.server]
    [micronaut.server.netty]
      [micronaut.server.netty.access-logger]
        enabled=true
        logger-name="my-access-logger"
        log-format="common"
        exclusions=[
          "/health",
          "/path/.+"
        ]
micronaut {
  server {
    netty {
      accessLogger {
        enabled = true
        loggerName = "my-access-logger"
        logFormat = "common"
        exclusions = ["/health", "/path/.+"]
      }
    }
  }
}
{
  micronaut {
    server {
      netty {
        access-logger {
          enabled = true
          logger-name = "my-access-logger"
          log-format = "common"
          exclusions = ["/health", "/path/.+"]
        }
      }
    }
  }
}
{
  "micronaut": {
    "server": {
      "netty": {
        "access-logger": {
          "enabled": true,
          "logger-name": "my-access-logger",
          "log-format": "common",
          "exclusions": ["/health", "/path/.+"]
        }
      }
    }
  }
}
  • enabled Enables the access logger

  • optionally specify a logger-name, which defaults to HTTP_ACCESS_LOGGER

  • optionally specify a log-format, which defaults to the Common Log Format

Logback Configuration

In addition to enabling the access logger, you must add a logger for the specified or default logger name. For instance using the default logger name for logback:

Logback configuration
<appender
    name="httpAccessLogAppender"
    class="ch.qos.logback.core.rolling.RollingFileAppender">
    <append>true</append>
    <file>log/http-access.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!-- daily rollover -->
        <fileNamePattern>log/http-access-%d{yyyy-MM-dd}.log
        </fileNamePattern>
        <maxHistory>7</maxHistory>
    </rollingPolicy>
    <encoder>
        <charset>UTF-8</charset>
        <pattern>%msg%n</pattern>
    </encoder>
    <immediateFlush>true</immediateFlush>
</appender>

<logger name="HTTP_ACCESS_LOGGER" additivity="false" level="info">
    <appender-ref ref="httpAccessLogAppender" />
</logger>

The pattern should only have the message marker, as other elements will be processed by the access logger.

Log Format

The syntax is based on Apache httpd log format.

These are the supported markers:

  • %a - Remote IP address

  • %A - Local IP address

  • %b - Bytes sent, excluding HTTP headers, or '-' if no bytes were sent

  • %B - Bytes sent, excluding HTTP headers

  • %h - Remote host name

  • %H - Request protocol

  • %{<header>}i - Request header. If the argument is omitted (%i) all headers are printed

  • %{<header>}o - Response header. If the argument is omitted (%o) all headers are printed

  • %{<cookie>}C - Request cookie (COOKIE). If the argument is omitted (%C) all cookies are printed

  • %{<cookie>}c - Response cookie (SET_COOKIE). If the argument is omitted (%c) all cookies are printed

  • %l - Remote logical username from identd (always returns '-')

  • %m - Request method

  • %p - Local port

  • %q - Query string (excluding the '?' character)

  • %r - First line of the request

  • %s - HTTP status code of the response

  • %{<format>}t - Date and time. If the argument is omitted, Common Log Format is used ("'['dd/MMM/yyyy:HH:mm:ss Z']'").

    • If the format starts with begin: (default) the time is taken at the beginning of the request processing. If it starts with end: it is the time when the log entry gets written, close to the end of the request processing.

    • The format should follow DateTimeFormatter syntax.

  • %{property}u - Remote authenticated user. When micronaut-session is on the classpath, returns the session id if the argument is omitted, or the specified property otherwise prints '-'

  • %U - Requested URI

  • %v - Local server name

  • %D - Time taken to process the request, in milliseconds

  • %T - Time taken to process the request, in seconds

In addition, you can use the following aliases for common patterns:

6.31.9 Starting Secondary Servers

The Micronaut framework supports the programmatic creation of additional Netty servers through the NettyEmbeddedServerFactory interface.

This is useful in cases where you, for example, need to expose distinct servers over different ports with potentially differing configurations (HTTPS, thread resources etc).

The following example demonstrates how to define a Factory Bean that starts an additional server using a programmatically created configuration:

Programmatically creating Secondary servers
import java.util.Collections;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Context;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.core.util.StringUtils;
import io.micronaut.discovery.ServiceInstanceList;
import io.micronaut.discovery.StaticServiceInstanceList;
import io.micronaut.http.server.netty.NettyEmbeddedServer;
import io.micronaut.http.server.netty.NettyEmbeddedServerFactory;
import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration;
import io.micronaut.http.ssl.ServerSslConfiguration;
import jakarta.inject.Named;

@Factory
public class SecondaryNettyServer {
    public static final String SERVER_ID = "another"; // (1)

    @Named(SERVER_ID)
    @Context
    @Bean(preDestroy = "close") // (2)
    @Requires(beans = Environment.class)
    NettyEmbeddedServer nettyEmbeddedServer(NettyEmbeddedServerFactory serverFactory) { // (3)
        // configure server programmatically
        final NettyHttpServerConfiguration configuration =
                new NettyHttpServerConfiguration(); // (4)
        final ServerSslConfiguration sslConfiguration = new ServerSslConfiguration(); // (5)
        sslConfiguration.setBuildSelfSigned(true);
        sslConfiguration.setEnabled(true);
        sslConfiguration.setPort(-1); // random port
        final NettyEmbeddedServer embeddedServer = serverFactory.build(configuration, sslConfiguration); // (6)
        embeddedServer.start(); // (7)
        return embeddedServer; // (8)
    }

    @Bean
    ServiceInstanceList serviceInstanceList( // (9)
            @Named(SERVER_ID) NettyEmbeddedServer nettyEmbeddedServer) {
        return new StaticServiceInstanceList(
                SERVER_ID,
                Collections.singleton(nettyEmbeddedServer.getURI())
        );
    }
}
Programmatically creating Secondary servers
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Context
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.micronaut.core.util.StringUtils
import io.micronaut.discovery.ServiceInstanceList
import io.micronaut.discovery.StaticServiceInstanceList
import io.micronaut.http.server.netty.NettyEmbeddedServer
import io.micronaut.http.server.netty.NettyEmbeddedServerFactory
import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration
import io.micronaut.http.ssl.ServerSslConfiguration
import jakarta.inject.Named

@Factory
class SecondaryNettyServer {
    static final String SERVER_ID = "another" // (1)

    @Named(SERVER_ID)
    @Context
    @Bean(preDestroy = "stop") // (2)
    @Requires(beans = Environment.class)
    NettyEmbeddedServer nettyEmbeddedServer(NettyEmbeddedServerFactory serverFactory) { // (3)
        def configuration =
                new NettyHttpServerConfiguration() // (4)
        def sslConfiguration = new ServerSslConfiguration() // (5)
        sslConfiguration.setBuildSelfSigned(true)
        sslConfiguration.enabled = true
        sslConfiguration.port = -1 // random port
        // configure server programmatically
        final NettyEmbeddedServer embeddedServer = serverFactory.build(configuration, sslConfiguration) // (5)
        embeddedServer.start() // (6)
        return embeddedServer // (7)
    }

    @Bean
    ServiceInstanceList serviceInstanceList( // (8)
                                             @Named(SERVER_ID) NettyEmbeddedServer nettyEmbeddedServer) {
        return new StaticServiceInstanceList(
                SERVER_ID,
                [ nettyEmbeddedServer.URI ]
        )
    }
}
Programmatically creating Secondary servers
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Context
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.micronaut.core.util.StringUtils
import io.micronaut.discovery.ServiceInstanceList
import io.micronaut.discovery.StaticServiceInstanceList
import io.micronaut.http.server.netty.NettyEmbeddedServer
import io.micronaut.http.server.netty.NettyEmbeddedServerFactory
import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration
import io.micronaut.http.ssl.ServerSslConfiguration
import jakarta.inject.Named

@Factory
class SecondaryNettyServer {
    companion object {
        const val SERVER_ID = "another" // (1)
    }

    @Named(SERVER_ID)
    @Context
    @Bean(preDestroy = "close") // (2)
    @Requires(beans = [Environment::class])
    fun nettyEmbeddedServer(
        serverFactory: NettyEmbeddedServerFactory // (3)
    ) : NettyEmbeddedServer {
        val configuration = NettyHttpServerConfiguration() // (4)
        val sslConfiguration = ServerSslConfiguration() // (5)

        sslConfiguration.setBuildSelfSigned(true)
        sslConfiguration.isEnabled = true
        sslConfiguration.port = -1 // random port

        // configure server programmatically
        val embeddedServer = serverFactory.build(configuration, sslConfiguration) // (6)
        embeddedServer.start() // (7)
        return embeddedServer // (8)
    }

    @Bean
    fun serviceInstanceList( // (9)
        @Named(SERVER_ID) nettyEmbeddedServer: NettyEmbeddedServer
    ): ServiceInstanceList {
        return StaticServiceInstanceList(
            SERVER_ID, setOf(nettyEmbeddedServer.uri)
        )
    }
}
1 Define a unique name for the server
2 Define a @Context scoped bean using the server name and including preDestroy="close" to ensure the server is shutdown when the context is closed
3 Inject the NettyEmbeddedServerFactory into a Factory Bean
4 Programmatically create the NettyHttpServerConfiguration
5 Optionally create the ServerSslConfiguration
6 Use the build method to build the server instance
7 Start the server with the start method
8 Return the server instance as a managed bean
9 Optionally define an instance of ServiceInstanceList if you wish to inject HTTP Clients by the server name

With this class in place when the ApplicationContext starts the server will also be started with the appropriate configuration.

Thanks to the definition of the ServiceInstanceList in step 8, you can then inject a client into your tests to test the secondary server:

Injecting the server or client
@Client(path = "/", id = SecondaryNettyServer.SERVER_ID)
@Inject
HttpClient httpClient; // (1)

@Named(SecondaryNettyServer.SERVER_ID)
EmbeddedServer embeddedServer; // (2)
Injecting the server or client
@Client(path = "/", id = SecondaryNettyServer.SERVER_ID)
@Inject
HttpClient httpClient // (1)

@Named(SecondaryNettyServer.SERVER_ID)
EmbeddedServer embeddedServer // (2)
Injecting the server or client
@Inject
@field:Client(path = "/", id = SecondaryNettyServer.SERVER_ID)
lateinit var httpClient : HttpClient // (1)

@Inject
@field:Named(SecondaryNettyServer.SERVER_ID)
lateinit var embeddedServer : EmbeddedServer // (2)
1 Use the server name to inject a client by ID
2 Use the @Named annotation as a qualifier to inject the embedded server instance

6.32 Server Side View Rendering

The Micronaut framework supports Server Side View Rendering.

See the documentation for Micronaut Views for more information.

6.33 OpenAPI / Swagger Support

To configure the Micronaut integration with OpenAPI/Swagger, please follow these instructions

6.34 GraphQL Support

GraphQL is a query language for building APIs that provides an intuitive and flexible syntax and a system for describing data requirements and interactions.

See the documentation for Micronaut GraphQL for more information on how to build GraphQL applications with Micronaut.

7 The HTTP Client

Client communication between Microservices is a critical component of any Microservice architecture. With that in mind, Micronaut framework includes an HTTP client that has both a low-level API and a higher-level AOP-driven API.

Regardless whether you choose to use Micronaut’s HTTP server, you may wish to use the Micronaut HTTP client in your application since it is a feature-rich client implementation.

7.1 HTTP Client Implementations

There are several implementations of the Micronaut HTTP Client.

7.1.1 HTTP Client based on Netty

To use an implementation based on Netty, add the following dependency to your build:

implementation("io.micronaut:micronaut-http-client")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-http-client</artifactId>
</dependency>

7.1.2 HTTP Client based on the Java HTTP Client

To use an implementation based on Java HTTP Client, add the following dependency to your build:

implementation("io.micronaut:micronaut-http-client-jdk")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-http-client-jdk</artifactId>
</dependency>

This implementation of the Micronaut HTTP Client is available since Micronaut Framework 4.0.

The implementation based on Java HTTP Client does not support the following features:

If you require any of these, we recommend you use the implementation of the HTTP Client based on Netty.

7.2 Low-Level and High-Level APIs

Since the higher level API is built on the low-level HTTP client, we first introduce the low-level client.

7.3 Using the Low-Level HTTP Client

The HttpClient interface forms the basis for the low-level API. This interfaces declares methods to help ease executing HTTP requests and receiving responses.

The majority of the methods in the HttpClient interface return Reactive Streams Publisher instances, which is not always the most useful interface to work against.

Micronaut’s Reactor HTTP Client dependency ships with a sub-interface named ReactorHttpClient. It provides a variation of the HttpClient interface that returns Project Reactor Flux types.

See the guide for Micronaut HTTP Client to learn more.

7.3.1 Sending your first HTTP request

Obtaining a HttpClient

There are a few ways to obtain a reference to an HttpClient. The most common is to use the Client annotation. For example:

Injecting an HTTP client
@Client("https://api.twitter.com/1.1") @Inject HttpClient httpClient;

The above example injects a client that targets the Twitter API.

@field:Client("\${myapp.api.twitter.url}") @Inject lateinit var httpClient: HttpClient

The above Kotlin example injects a client that targets the Twitter API using a configuration path. Note the required escaping (backslash) on "\${path.to.config}" which is necessary due to Kotlin string interpolation.

The Client annotation is also a custom scope that manages the creation of HttpClient instances and ensures they are stopped when the application shuts down.

The value you pass to the Client annotation can be one of the following:

  • An absolute URI, e.g. https://api.twitter.com/1.1

  • A relative URI, in which case the targeted server will be the current server (useful for testing)

  • A service identifier. See the section on Service Discovery for more information on this topic.

Another way to create an HttpClient is with the static create method of HttpClient, however this approach is not recommended as you must ensure you manually shutdown the client, and of course no dependency injection will occur for the created client.

Performing an HTTP GET

Generally there are two methods of interest when working with the HttpClient. The first is retrieve, which executes an HTTP request and returns the body in whichever type you request (by default a String) as Publisher.

The retrieve method accepts an HttpRequest or a String URI to the endpoint you wish to request.

The following example shows how to use retrieve to execute an HTTP GET and receive the response body as a String:

Using retrieve
String uri = UriBuilder.of("/hello/{name}")
                       .expand(Collections.singletonMap("name", "John"))
                       .toString();
assertEquals("/hello/John", uri);

String result = client.toBlocking().retrieve(uri);

assertEquals("Hello John", result);
Using retrieve
when:
String uri = UriBuilder.of("/hello/{name}")
                       .expand(name: "John")
then:
"/hello/John" == uri

when:
String result = client.toBlocking().retrieve(uri)

then:
"Hello John" == result
Using retrieve
val uri = UriBuilder.of("/hello/{name}")
                    .expand(Collections.singletonMap("name", "John"))
                    .toString()
uri shouldBe "/hello/John"

val result = client.toBlocking().retrieve(uri)

result shouldBe "Hello John"

Note that in this example, for illustration purposes we call toBlocking() to return a blocking version of the client. However, in production code you should not do this and instead rely on the non-blocking nature of the Micronaut HTTP server.

For example the following @Controller method calls another endpoint in a non-blocking manner:

Using the HTTP client without blocking
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Status;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import io.micronaut.core.async.annotation.SingleResult;
import static io.micronaut.http.HttpRequest.GET;
import static io.micronaut.http.HttpStatus.CREATED;
import static io.micronaut.http.MediaType.TEXT_PLAIN;

@Get("/hello/{name}")
@SingleResult
Publisher<String> hello(String name) { // (1)
    return Mono.from(httpClient.retrieve(GET("/hello/" + name))); // (2)
}
Using the HTTP client without blocking
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.http.annotation.Status
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import org.reactivestreams.Publisher
import io.micronaut.core.async.annotation.SingleResult
import reactor.core.publisher.Mono
import static io.micronaut.http.HttpRequest.GET
import static io.micronaut.http.HttpStatus.CREATED
import static io.micronaut.http.MediaType.TEXT_PLAIN

@Get("/hello/{name}")
@SingleResult
Publisher<String> hello(String name) { // (1)
    Mono.from(httpClient.retrieve( GET("/hello/" + name))) // (2)
}
Using the HTTP client without blocking
import io.micronaut.http.HttpRequest.GET
import io.micronaut.http.HttpStatus.CREATED
import io.micronaut.http.MediaType.TEXT_PLAIN
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.http.annotation.Status
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
import io.micronaut.core.async.annotation.SingleResult

@Get("/hello/{name}")
@SingleResult
internal fun hello(name: String): Publisher<String> { // (1)
    return Flux.from(httpClient.retrieve(GET<Any>("/hello/$name")))
                     .next() // (2)
}
1 The hello method returns a Mono which may or may not emit an item. If an item is not emitted, a 404 is returned.
2 The retrieve method is called which returns a Flux. This has a firstElement method that returns the first emitted item or nothing
Using Reactor (or RxJava if you prefer) you can easily and efficiently compose multiple HTTP client calls without blocking (which limits the throughput and scalability of your application).

Debugging / Tracing the HTTP Client

To debug requests being sent and received from the HTTP client you can enable tracing logging via your logback.xml file:

logback.xml
<logger name="io.micronaut.http.client" level="TRACE"/>

Client Specific Debugging / Tracing

To enable client-specific logging you can configure the default logger for all HTTP clients. You can also configure different loggers for different clients using Client-Specific Configuration. For example, in your configuration file (e.g application.yml):

micronaut.http.client.logger-name=mylogger
micronaut.http.services.otherClient.logger-name=other.client
micronaut:
  http:
    client:
      logger-name: mylogger
    services:
      otherClient:
        logger-name: other.client
[micronaut]
  [micronaut.http]
    [micronaut.http.client]
      logger-name="mylogger"
    [micronaut.http.services]
      [micronaut.http.services.otherClient]
        logger-name="other.client"
micronaut {
  http {
    client {
      loggerName = "mylogger"
    }
    services {
      otherClient {
        loggerName = "other.client"
      }
    }
  }
}
{
  micronaut {
    http {
      client {
        logger-name = "mylogger"
      }
      services {
        otherClient {
          logger-name = "other.client"
        }
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "client": {
        "logger-name": "mylogger"
      },
      "services": {
        "otherClient": {
          "logger-name": "other.client"
        }
      }
    }
  }
}

Then enable logging in logback.xml:

logback.xml
<logger name="mylogger" level="DEBUG"/>
<logger name="other.client" level="TRACE"/>

Customizing the HTTP Request

The previous example demonstrates using the static methods of the HttpRequest interface to construct a MutableHttpRequest instance. Like the name suggests, a MutableHttpRequest can be mutated, including the ability to add headers, customize the request body, etc. For example:

Passing an HttpRequest to retrieve
Flux<String> response = Flux.from(client.retrieve(
        GET("/hello/John")
        .header("X-My-Header", "SomeValue")
));
Passing an HttpRequest to retrieve
Flux<String> response = Flux.from(client.retrieve(
        GET("/hello/John")
        .header("X-My-Header", "SomeValue")
))
Passing an HttpRequest to retrieve
val response = client.retrieve(
        GET<Any>("/hello/John")
                .header("X-My-Header", "SomeValue")
)

The above example adds a header (X-My-Header) to the response before it is sent. The MutableHttpRequest interface has more convenience methods that make it easy to modify the request in common ways.

Reading JSON Responses

Microservices typically use a message encoding format such as JSON. Micronaut’s HTTP client leverages Jackson for JSON parsing, hence any type Jackson can decode can be passed as a second argument to retrieve.

For example consider the following @Controller method that returns a JSON response:

Returning JSON from a controller
@Get("/greet/{name}")
Message greet(String name) {
    return new Message("Hello " + name);
}
Returning JSON from a controller
@Get("/greet/{name}")
Message greet(String name) {
    new Message("Hello $name")
}
Returning JSON from a controller
@Get("/greet/{name}")
internal fun greet(name: String): Message {
    return Message("Hello $name")
}

The method above returns a POJO of type Message which looks like:

Message POJO
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class Message {

    private final String text;

    @JsonCreator
    public Message(@JsonProperty("text") String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }
}
Message POJO
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty

class Message {

    final String text

    @JsonCreator
    Message(@JsonProperty("text") String text) {
        this.text = text
    }
}
Message POJO
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty

class Message @JsonCreator
constructor(@param:JsonProperty("text") val text: String)
Jackson annotations are used to map the constructor

On the client you can call this endpoint and decode the JSON into a map using the retrieve method as follows:

Decoding the response body to a Map
Flux<Map> response = Flux.from(client.retrieve(
        GET("/greet/John"), Map.class
));
Decoding the response body to a Map
Flux<Map> response = Flux.from(client.retrieve(
        GET("/greet/John"), Map
))
Decoding the response body to a Map
var response: Flux<Map<*, *>> = Flux.from(client.retrieve(
        GET<Any>("/greet/John"), Map::class.java
))

The above example decodes the response into a Map representing the JSON. You can use the Argument.of(..) method to customize the type of the key and value:

Decoding the response body to a Map
response = Flux.from(client.retrieve(
        GET("/greet/John"),
        Argument.of(Map.class, String.class, String.class) // (1)
));
Decoding the response body to a Map
response = Flux.from(client.retrieve(
        GET("/greet/John"),
        Argument.of(Map, String, String) // (1)
))
Decoding the response body to a Map
response = Flux.from(client.retrieve(
        GET<Any>("/greet/John"),
        Argument.mapOf(String::class.java, String::class.java) // (1)
))
1 The Argument.of method returns a Map where the key and value types are String

Whilst retrieving JSON as a map can be desirable, typically you want to decode objects into POJOs. To do that, pass the type instead:

Decoding the response body to a POJO
Flux<Message> response = Flux.from(client.retrieve(
        GET("/greet/John"), Message.class
));

assertEquals("Hello John", response.blockFirst().getText());
Decoding the response body to a POJO
when:
Flux<Message> response = Flux.from(client.retrieve(
        GET("/greet/John"), Message
))

then:
"Hello John" == response.blockFirst().getText()
Decoding the response body to a POJO
val response = Flux.from(client.retrieve(
        GET<Any>("/greet/John"), Message::class.java
))

response.blockFirst().text shouldBe "Hello John"

Note how you can use the same Java type on both the client and the server. The implication of this is that typically you define a common API project where you define the interfaces and types that define your API.

Decoding Other Content Types

If the server you communicate with uses a custom content type that is not JSON, by default Micronaut’s HTTP client will not know how to decode this type.

To resolve this, register MediaTypeCodec as a bean, and it will be automatically picked up and used to decode (or encode) messages.

Receiving the Full HTTP Response

Sometimes receiving just the body of the response is not enough, and you need other information from the response such as headers, cookies, etc. In this case, instead of retrieve use the exchange method:

Receiving the Full HTTP Response
Flux<HttpResponse<Message>> call = Flux.from(client.exchange(
        GET("/greet/John"), Message.class // (1)
));

HttpResponse<Message> response = call.blockFirst();
Optional<Message> message = response.getBody(Message.class); // (2)
// check the status
assertEquals(HttpStatus.OK, response.getStatus()); // (3)
// check the body
assertTrue(message.isPresent());
assertEquals("Hello John", message.get().getText());
Receiving the Full HTTP Response
when:
Flux<HttpResponse<Message>> call = Flux.from(client.exchange(
        GET("/greet/John"), Message // (1)
))

HttpResponse<Message> response = call.blockFirst();
Optional<Message> message = response.getBody(Message) // (2)
// check the status
then:
HttpStatus.OK == response.getStatus() // (3)
// check the body
message.isPresent()
"Hello John" == message.get().getText()
Receiving the Full HTTP Response
val call = client.exchange(
        GET<Any>("/greet/John"), Message::class.java // (1)
)

val response = Flux.from(call).blockFirst()
val message = response.getBody(Message::class.java) // (2)
// check the status
response.status shouldBe HttpStatus.OK // (3)
// check the body
message.isPresent shouldBe true
message.get().text shouldBe "Hello John"
1 The exchange method receives the HttpResponse
2 The body is retrieved using the getBody(..) method of the response
3 Other aspects of the response such as the HttpStatus can be checked

The above example receives the full HttpResponse from which you can obtain headers and other useful information.

7.3.2 Posting a Request Body

All the examples so far have used the same HTTP method i.e GET. The HttpRequest interface has factory methods for all the different HTTP methods. The following table summarizes them:

Table 1. HttpRequest Factory Methods
Method Description Allows Body

HttpRequest.GET(java.lang.String)

Constructs an HTTP GET request

false

HttpRequest.OPTIONS(java.lang.String)

Constructs an HTTP OPTIONS request

false

HttpRequest.HEAD(java.lang.String)

Constructs an HTTP HEAD request

false

HttpRequest.POST(java.lang.String,T)

Constructs an HTTP POST request

true

HttpRequest.PUT(java.lang.String,T)

Constructs an HTTP PUT request

true

HttpRequest.PATCH(java.lang.String,T)

Constructs an HTTP PATCH request

true

HttpRequest.DELETE(java.lang.String)

Constructs an HTTP DELETE request

true

A create method also exists to construct a request for any HttpMethod type. Since the POST, PUT and PATCH methods require a body, a second argument which is the body object is required.

The following example demonstrates how to send a simple String body:

Sending a String body
Flux<HttpResponse<String>> call = Flux.from(client.exchange(
        POST("/hello", "Hello John") // (1)
            .contentType(MediaType.TEXT_PLAIN_TYPE)
            .accept(MediaType.TEXT_PLAIN_TYPE), // (2)
        String.class // (3)
));
Sending a String body
Flux<HttpResponse<String>> call = Flux.from(client.exchange(
        POST("/hello", "Hello John") // (1)
            .contentType(MediaType.TEXT_PLAIN_TYPE)
            .accept(MediaType.TEXT_PLAIN_TYPE), // (2)
        String // (3)
))
Sending a String body
val call = client.exchange(
        POST("/hello", "Hello John") // (1)
                .contentType(MediaType.TEXT_PLAIN_TYPE)
                .accept(MediaType.TEXT_PLAIN_TYPE), String::class.java // (3)
)
1 The POST method is used; the first argument is the URI and the second is the body
2 The content type and accepted type are set to text/plain (the default is application/json)
3 The expected response type is a String

Sending JSON

The previous example sends plain text. To send JSON, pass the object to encode to JSON (whether that be a Map or a POJO) as long as Jackson is able to encode it.

For example, you can create a Message from the previous section and pass it to the POST method:

Sending a JSON body
Flux<HttpResponse<Message>> call = Flux.from(client.exchange(
        POST("/greet", new Message("Hello John")), // (1)
        Message.class // (2)
));
Sending a JSON body
Flux<HttpResponse<Message>> call = Flux.from(client.exchange(
        POST("/greet", new Message("Hello John")), // (1)
        Message // (2)
))
Sending a JSON body
val call = client.exchange(
        POST("/greet", Message("Hello John")), Message::class.java // (2)
)
1 An instance of Message is created and passed to the POST method
2 The same class decodes the response

With the above example the following JSON is sent as the body of the request:

Resulting JSON
{"text":"Hello John"}

The JSON can be customized using Jackson Annotations.

Using a URI Template

If include some properties of the object in the URI, you can use a URI template.

For example imagine you have a Book class with a title property. You can include the title in the URI template and then populate it from an instance of Book. For example:

Sending a JSON body with a URI template
Flux<HttpResponse<Book>> call = Flux.from(client.exchange(
        POST("/amazon/book/{title}", new Book("The Stand")),
        Book.class
));
Sending a JSON body with a URI template
Flux<HttpResponse<Book>> call = client.exchange(
        POST("/amazon/book/{title}", new Book("The Stand")),
        Book
);
Sending a JSON body with a URI template
val call = client.exchange(
        POST("/amazon/book/{title}", Book("The Stand")),
        Book::class.java
)

In the above case the title property is included in the URI.

Sending Form Data

You can also encode a POJO or a map as form data instead of JSON. Just set the content type to application/x-www-form-urlencoded on the post request:

Sending a Form Data
Flux<HttpResponse<Book>> call = Flux.from(client.exchange(
        POST("/amazon/book/{title}", new Book("The Stand"))
        .contentType(MediaType.APPLICATION_FORM_URLENCODED),
        Book.class
));
Sending a Form Data
Flux<HttpResponse<Book>> call = client.exchange(
        POST("/amazon/book/{title}", new Book("The Stand"))
        .contentType(MediaType.APPLICATION_FORM_URLENCODED),
        Book
)
Sending a Form Data
val call = client.exchange(
        POST("/amazon/book/{title}", Book("The Stand"))
                .contentType(MediaType.APPLICATION_FORM_URLENCODED),
        Book::class.java
)

Note that Jackson can bind form data too, so to customize the binding process use Jackson annotations.

7.3.3 Multipart Client Uploads

The Micronaut HTTP Client supports multipart requests. To build a multipart request, set the content type to multipart/form-data and set the body to an instance of MultipartBody.

For example:

Creating the body
import io.micronaut.http.client.multipart.MultipartBody;

String toWrite = "test file";
File file = File.createTempFile("data", ".txt");
FileWriter writer = new FileWriter(file);
writer.write(toWrite);
writer.close();

MultipartBody requestBody = MultipartBody.builder()     // (1)
        .addPart(                                       // (2)
            "data",
            file.getName(),
            MediaType.TEXT_PLAIN_TYPE,
            file
        ).build();                                      // (3)
Creating the body
import io.micronaut.http.multipart.CompletedFileUpload
import io.micronaut.http.multipart.StreamingFileUpload
import io.micronaut.http.client.multipart.MultipartBody
import org.reactivestreams.Publisher

File file = new File(uploadDir, "data.txt")
file.text = "test file"
file.createNewFile()


MultipartBody requestBody = MultipartBody.builder()     // (1)
        .addPart(                                       // (2)
            "data",
            file.name,
            MediaType.TEXT_PLAIN_TYPE,
            file
        ).build()                                       // (3)
Creating the body
import io.micronaut.http.client.multipart.MultipartBody

val toWrite = "test file"
val file = File.createTempFile("data", ".txt")
val writer = FileWriter(file)
writer.write(toWrite)
writer.close()

val requestBody = MultipartBody.builder()     // (1)
        .addPart(                             // (2)
                "data",
                file.name,
                MediaType.TEXT_PLAIN_TYPE,
                file
        ).build()                             // (3)
1 Create a MultipartBody builder for adding parts to the body.
2 Add a part to the body, in this case a file. There are different variations of this method in MultipartBody.Builder.
3 The build method assembles all parts from the builder into a MultipartBody. At least one part is required.
Creating a request
HttpRequest.POST("/multipart/upload", requestBody)    // (1)
           .contentType(MediaType.MULTIPART_FORM_DATA_TYPE) // (2)
Creating a request
HttpRequest.POST("/multipart/upload", requestBody)      // (1)
           .contentType(MediaType.MULTIPART_FORM_DATA_TYPE) // (2)
Creating a request
HttpRequest.POST("/multipart/upload", requestBody)    // (1)
           .contentType(MediaType.MULTIPART_FORM_DATA_TYPE) // (2)
1 The multipart request body with different types of data.
2 Set the content-type header of the request to multipart/form-data.

7.3.4 Streaming JSON over HTTP

Micronaut’s HTTP client includes support for streaming data over HTTP via the ReactorStreamingHttpClient interface which includes methods specific to streaming including:

Table 1. HTTP Streaming Methods
Method Description

dataStream(HttpRequest<I> request)

Returns a stream of data as a Flux of ByteBuffer

exchangeStream(HttpRequest<I> request)

Returns the HttpResponse wrapping a Flux of ByteBuffer

jsonStream(HttpRequest<I> request)

Returns a non-blocking stream of JSON objects

To use JSON streaming, declare a controller method on the server that returns a application/x-json-stream of JSON objects. For example:

Streaming JSON on the Server
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;

@Get(value = "/headlines", processes = MediaType.APPLICATION_JSON_STREAM) // (1)
Publisher<Headline> streamHeadlines() {
    return Mono.fromCallable(() -> {  // (2)
        Headline headline = new Headline();
        headline.setText("Latest Headline at " + ZonedDateTime.now());
        return headline;
    }).repeat(100) // (3)
      .delayElements(Duration.of(1, ChronoUnit.SECONDS)); // (4)
}
Streaming JSON on the Server
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

import java.time.Duration
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit

@Get(value = "/headlines", processes = MediaType.APPLICATION_JSON_STREAM) // (1)
Flux<Headline> streamHeadlines() {
    Mono.fromCallable({ // (2)
        new Headline(text: "Latest Headline at ${ZonedDateTime.now()}")
    }).repeat(100) // (3)
            .delayElements(Duration.of(1, ChronoUnit.SECONDS)) // (4)
}
Streaming JSON on the Server
import io.micronaut.http.MediaType.APPLICATION_JSON_STREAM
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.time.Duration
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit.SECONDS

@Get(value = "/headlines", processes = [APPLICATION_JSON_STREAM]) // (1)
internal fun streamHeadlines(): Flux<Headline> {
    return Mono.fromCallable { // (2)
        Headline().apply {
            text = "Latest Headline at ${ZonedDateTime.now()}"
        }
    }.repeat(100) // (3)
     .delayElements(Duration.of(1, ChronoUnit.SECONDS)) // (4)
}
1 The streamHeadlines method produces application/x-json-stream
2 A Flux is created from a Callable function (note no blocking occurs within the function, so this is ok, otherwise you should subscribeOn an I/O thread pool).
3 The Flux repeats 100 times
4 The Flux emits items with a delay of one second between each
The server does not have to be written in Micronaut, any server that supports JSON streaming will do.

Then on the client, subscribe to the stream using jsonStream and every time the server emits a JSON object the client will decode and consume it:

Streaming JSON on the Client
Flux<Headline> headlineStream = Flux.from(client.jsonStream(
        GET("/streaming/headlines"), Headline.class)); // (1)
CompletableFuture<Headline> future = new CompletableFuture<>(); // (2)
headlineStream.subscribe(new Subscriber<>() {
    @Override
    public void onSubscribe(Subscription s) {
        s.request(1); // (3)
    }

    @Override
    public void onNext(Headline headline) {
        System.out.println("Received Headline = " + headline.getText());
        future.complete(headline); // (4)
    }

    @Override
    public void onError(Throwable t) {
        future.completeExceptionally(t); // (5)
    }

    @Override
    public void onComplete() {
        // no-op // (6)
    }
});
Streaming JSON on the Client
Flux<Headline> headlineStream = Flux.from(client.jsonStream(
        GET("/streaming/headlines"), Headline)) // (1)
CompletableFuture<Headline> future = new CompletableFuture<>() // (2)
headlineStream.subscribe(new Subscriber<Headline>() {
    @Override
    void onSubscribe(Subscription s) {
        s.request(1) // (3)
    }

    @Override
    void onNext(Headline headline) {
        println "Received Headline = $headline.text"
        future.complete(headline) // (4)
    }

    @Override
    void onError(Throwable t) {
        future.completeExceptionally(t) // (5)
    }

    @Override
    void onComplete() {
        // no-op // (6)
    }
})
Streaming JSON on the Client
val headlineStream = client.jsonStream(
    GET<Any>("/streaming/headlines"), Headline::class.java
) // (1)
val future = CompletableFuture<Headline>() // (2)
headlineStream.subscribe(object : Subscriber<Headline> {
    override fun onSubscribe(s: Subscription) {
        s.request(1) // (3)
    }

    override fun onNext(headline: Headline) {
        println("Received Headline = ${headline.text!!}")
        future.complete(headline) // (4)
    }

    override fun onError(t: Throwable) {
        future.completeExceptionally(t) // (5)
    }

    override fun onComplete() {
        // no-op // (6)
    }
})
1 The jsonStream method returns a Flux
2 A CompletableFuture is used to receive a value, but what you do with each emitted item is application-specific
3 The Subscription requests a single item. You can use the Subscription to regulate back pressure and demand.
4 The onNext method is called when an item is emitted
5 The onError method is called when an error occurs
6 The onComplete method is called when all Headline instances have been emitted

Note neither the server nor the client in the example above perform any blocking I/O.

7.3.5 Configuring HTTP clients

Global Configuration for All Clients

The default HTTP client configuration is a Configuration Properties named DefaultHttpClientConfiguration that allows configuring the default behaviour for all HTTP clients. For example, in your configuration file (e.g application.yml):

Altering default HTTP client configuration
micronaut.http.client.read-timeout=5s
micronaut:
  http:
    client:
      read-timeout: 5s
[micronaut]
  [micronaut.http]
    [micronaut.http.client]
      read-timeout="5s"
micronaut {
  http {
    client {
      readTimeout = "5s"
    }
  }
}
{
  micronaut {
    http {
      client {
        read-timeout = "5s"
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "client": {
        "read-timeout": "5s"
      }
    }
  }
}

The above example sets the readTimeout property of the HttpClientConfiguration class.

Client Specific Configuration

To have separate configuration per-client, there are a couple of options. You can configure Service Discovery manually in your configuration file (e.g. application.yml) and apply per-client configuration:

Manually configuring HTTP services
micronaut.http.services.foo.urls[0]=http://foo1
micronaut.http.services.foo.urls[1]=http://foo2
micronaut.http.services.foo.read-timeout=5s
micronaut:
  http:
    services:
      foo:
        urls:
          - http://foo1
          - http://foo2
        read-timeout: 5s
[micronaut]
  [micronaut.http]
    [micronaut.http.services]
      [micronaut.http.services.foo]
        urls=[
          "http://foo1",
          "http://foo2"
        ]
        read-timeout="5s"
micronaut {
  http {
    services {
      foo {
        urls = ["http://foo1", "http://foo2"]
        readTimeout = "5s"
      }
    }
  }
}
{
  micronaut {
    http {
      services {
        foo {
          urls = ["http://foo1", "http://foo2"]
          read-timeout = "5s"
        }
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "services": {
        "foo": {
          "urls": ["http://foo1", "http://foo2"],
          "read-timeout": "5s"
        }
      }
    }
  }
}
  • The read-timeout is applied to the foo client.

WARN: This client configuration can be used in conjunction with the @Client annotation, either by injecting an HttpClient directly or use on a client interface. In any case, all other attributes on the annotation will be ignored except the service id.

Then, inject the named client configuration:

Injecting an HTTP client
@Client("foo") @Inject ReactorHttpClient httpClient;

You can also define a bean that extends from HttpClientConfiguration and ensure that the jakarta.inject.Named annotation names it appropriately:

Defining an HTTP client configuration bean
@Named("twitter")
@Singleton
class TwitterHttpClientConfiguration extends HttpClientConfiguration {
   public TwitterHttpClientConfiguration(ApplicationConfiguration configuration) {
        super(configuration);
    }
}

This configuration will be picked up if you inject a service named twitter with @Client using Service Discovery:

Injecting an HTTP client
@Client("twitter") @Inject ReactorHttpClient httpClient;

Alternatively, if you don’t use service discovery you can use the configuration member of @Client to refer to a specific type:

Injecting an HTTP client
@Client(value = "https://api.twitter.com/1.1",
        configuration = TwitterHttpClientConfiguration.class)
@Inject
ReactorHttpClient httpClient;

Connection Pooling and HTTP/2

Connections using normal HTTP (without TLS/SSL) use HTTP/1.1. This can be configured using the plaintext-mode configuration option.

Secure connections (i.e. HTTPS, with TLS/SSL) use a feature called "Application Layer Protocol Negotiation" (ALPN) that is part of TLS to select the HTTP version. If the server supports HTTP/2, the Micronaut HTTP Client will use that capability by default, but if it doesn’t, HTTP/1.1 is still supported. This is configured using the alpn-modes option, which is a list of supported ALPN protocol IDs ("h2" and "http/1.1").

The HTTP/2 standard forbids the use of certain less secure TLS cipher suites for HTTP/2 connections. When the HTTP client supports HTTP/2 (which is the default), it will not support those cipher suites. Removing "h2" from alpn-modes will enable support for all cipher suites.

Each HTTP/1.1 connection can only support one request at a time, but can be reused for subsequent requests using the keep-alive mechanism. HTTP/2 connections can support any number of concurrent requests.

To remove the overhead of opening a new connection for each request, the Micronaut HTTP Client will reuse HTTP connections wherever possible. They are managed in a connection pool. HTTP/1.1 connections are kept around using keep-alive and are used for new requests, and for HTTP/2, a single connection is used for all requests.

Manually configuring HTTP services
micronaut.http.services.foo.urls[0]=http://foo1
micronaut.http.services.foo.urls[1]=http://foo2
micronaut.http.services.foo.pool.max-concurrent-http1-connections=50
micronaut.http.services.foo.pool.enabled=true
micronaut.http.services.foo.pool.max-connections=50
micronaut:
  http:
    services:
      foo:
        urls:
          - http://foo1
          - http://foo2
        pool:
          max-concurrent-http1-connections: 50
          enabled: true
          max-connections: 50
[micronaut]
  [micronaut.http]
    [micronaut.http.services]
      [micronaut.http.services.foo]
        urls=[
          "http://foo1",
          "http://foo2"
        ]
        [micronaut.http.services.foo.pool]
          max-concurrent-http1-connections=50
          enabled=true
          max-connections=50
micronaut {
  http {
    services {
      foo {
        urls = ["http://foo1", "http://foo2"]
        pool {
          maxConcurrentHttp1Connections = 50
          enabled = true
          maxConnections = 50
        }
      }
    }
  }
}
{
  micronaut {
    http {
      services {
        foo {
          urls = ["http://foo1", "http://foo2"]
          pool {
            max-concurrent-http1-connections = 50
            enabled = true
            max-connections = 50
          }
        }
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "services": {
        "foo": {
          "urls": ["http://foo1", "http://foo2"],
          "pool": {
            "max-concurrent-http1-connections": 50,
            "enabled": true,
            "max-connections": 50
          }
        }
      }
    }
  }
}
  • Limit maximum concurrent HTTP/1.1 connections

  • max-concurrent-http1-connections limits the maximum concurrent HTTP/1.1 connections to 50.

  • pool enables the pool and sets the maximum number of connections for it

See the API for ConnectionPoolConfiguration for details on available pool configuration options.

By setting the pool.enabled property to false, you can disable connection reuse. The pool is still used and other configuration options (e.g. concurrent HTTP 1 connections) still apply, but one connection will only serve one request.

Configuring Event Loop Groups

By default, the Micronaut framework shares a common Netty EventLoopGroup for worker threads and all HTTP client threads.

This EventLoopGroup can be configured via the micronaut.netty.event-loops.default property:

Configuring The Default Event Loop
micronaut.netty.event-loops.default.num-threads=10
micronaut.netty.event-loops.default.prefer-native-transport=true
micronaut:
  netty:
    event-loops:
      default:
        num-threads: 10
        prefer-native-transport: true
[micronaut]
  [micronaut.netty]
    [micronaut.netty.event-loops]
      [micronaut.netty.event-loops.default]
        num-threads=10
        prefer-native-transport=true
micronaut {
  netty {
    eventLoops {
      'default' {
        numThreads = 10
        preferNativeTransport = true
      }
    }
  }
}
{
  micronaut {
    netty {
      event-loops {
        default {
          num-threads = 10
          prefer-native-transport = true
        }
      }
    }
  }
}
{
  "micronaut": {
    "netty": {
      "event-loops": {
        "default": {
          "num-threads": 10,
          "prefer-native-transport": true
        }
      }
    }
  }
}

You can also use the micronaut.netty.event-loops setting to configure one or more additional event loops. The following table summarizes the properties:

🔗
Table 1. Configuration Properties for DefaultEventLoopGroupConfiguration
Property Type Description

micronaut.netty.event-loops.*.num-threads

int

The number of threads

micronaut.netty.event-loops.*.io-ratio

java.lang.Integer

The IO ratio (optional)

micronaut.netty.event-loops.*.prefer-native-transport

boolean

Whether native transport is to be preferred

micronaut.netty.event-loops.*.executor

java.lang.String

A named executor service to use (optional)

micronaut.netty.event-loops.*.shutdown-quiet-period

java.time.Duration

The shutdown quiet period

micronaut.netty.event-loops.*.shutdown-timeout

java.time.Duration

The shutdown timeout (must be >= shutdownQuietPeriod)

For example, if your interactions with an HTTP client involve CPU-intensive work, it may be worthwhile configuring a separate EventLoopGroup for one or all clients.

The following example configures an additional event loop group called "other" with 10 threads:

Configuring Additional Event Loops
micronaut.netty.event-loops.other.num-threads=10
micronaut.netty.event-loops.other.prefer-native-transport=true
micronaut:
  netty:
    event-loops:
      other:
        num-threads: 10
        prefer-native-transport: true
[micronaut]
  [micronaut.netty]
    [micronaut.netty.event-loops]
      [micronaut.netty.event-loops.other]
        num-threads=10
        prefer-native-transport=true
micronaut {
  netty {
    eventLoops {
      other {
        numThreads = 10
        preferNativeTransport = true
      }
    }
  }
}
{
  micronaut {
    netty {
      event-loops {
        other {
          num-threads = 10
          prefer-native-transport = true
        }
      }
    }
  }
}
{
  "micronaut": {
    "netty": {
      "event-loops": {
        "other": {
          "num-threads": 10,
          "prefer-native-transport": true
        }
      }
    }
  }
}

Once an additional event loop has been configured you can alter the HTTP client configuration to use it:

Altering the Event Loop Group used by Clients
micronaut.http.client.event-loop-group=other
micronaut:
  http:
    client:
      event-loop-group: other
[micronaut]
  [micronaut.http]
    [micronaut.http.client]
      event-loop-group="other"
micronaut {
  http {
    client {
      eventLoopGroup = "other"
    }
  }
}
{
  micronaut {
    http {
      client {
        event-loop-group = "other"
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "client": {
        "event-loop-group": "other"
      }
    }
  }
}

7.3.6 Error Responses

If an HTTP response is returned with a code of 400 or higher, an HttpClientResponseException is created. The exception contains the original response. How that exception gets thrown depends on the return type of the method.

For blocking clients, the exception is thrown and should be caught and handled by the caller. For reactive clients, the exception is passed through the publisher as an error.

7.3.7 Bind Errors

Often you want to consume an endpoint and bind to a POJO if the request is successful and bind to a different POJO if an error occurs. The following example shows how to invoke exchange with a success and error type.

@Controller("/books")
public class BooksController {

    @Get("/{isbn}")
    public HttpResponse find(String isbn) {
        if (isbn.equals("1680502395")) {
            Map<String, Object> m = new HashMap<>();
            m.put("status", 401);
            m.put("error", "Unauthorized");
            m.put("message", "No message available");
            m.put("path", "/books/" + isbn);
            return HttpResponse.status(HttpStatus.UNAUTHORIZED).body(m);
        }

        return HttpResponse.ok(new Book("1491950358", "Building Microservices"));
    }
}
@Controller("/books")
class BooksController {

    @Get("/{isbn}")
    HttpResponse find(String isbn) {
        if (isbn == "1680502395") {
            Map<String, Object> m = [
                    status : 401,
                    error  : "Unauthorized",
                    message: "No message available",
                    path   : "/books/" + isbn]
            return HttpResponse.status(HttpStatus.UNAUTHORIZED).body(m)
        }

        return HttpResponse.ok(new Book("1491950358", "Building Microservices"))
    }
}
@Controller("/books")
class BooksController {

    @Get("/{isbn}")
    fun find(isbn: String): HttpResponse<*> {
        if (isbn == "1680502395") {
            val m = mapOf(
                "status" to 401,
                "error" to "Unauthorized",
                "message" to "No message available",
                "path" to "/books/$isbn"
            )
            return HttpResponse.status<Any>(HttpStatus.UNAUTHORIZED).body(m)
        }

        return HttpResponse.ok(Book("1491950358", "Building Microservices"))
    }
}

@Test
void afterAnHttpClientExceptionTheResponseBodyCanBeBoundToAPOJO() {
    try {
        client.toBlocking().exchange(HttpRequest.GET("/books/1680502395"),
                Argument.of(Book.class), // (1)
                Argument.of(CustomError.class)); // (2)
    } catch (HttpClientResponseException e) {
        assertEquals(HttpStatus.UNAUTHORIZED, e.getResponse().getStatus());
        Optional<CustomError> jsonError = e.getResponse().getBody(CustomError.class);
        assertTrue(jsonError.isPresent());
        assertEquals(401, jsonError.get().status);
        assertEquals("Unauthorized", jsonError.get().error);
        assertEquals("No message available", jsonError.get().message);
        assertEquals("/books/1680502395", jsonError.get().path);
    }
}
def "after an HttpClientException the response body can be bound to a POJO"() {
    when:
    client.toBlocking().exchange(HttpRequest.GET("/books/1680502395"),
            Argument.of(Book), // (1)
            Argument.of(CustomError)) // (2)

    then:
    def e = thrown(HttpClientResponseException)
    e.response.status == HttpStatus.UNAUTHORIZED

    when:
    Optional<CustomError> jsonError = e.response.getBody(CustomError)

    then:
    jsonError.isPresent()
    jsonError.get().status == 401
    jsonError.get().error == 'Unauthorized'
    jsonError.get().message == 'No message available'
    jsonError.get().path == '/books/1680502395'
}
"after an httpclient exception the response body can be bound to a POJO" {
    try {
        client.toBlocking().exchange(HttpRequest.GET<Any>("/books/1680502395"),
                Argument.of(Book::class.java), // (1)
                Argument.of(CustomError::class.java)) // (2)
    } catch (e: HttpClientResponseException) {
        e.response.status shouldBe HttpStatus.UNAUTHORIZED
    }
}
1 Success Type
2 Error Type

7.4 Proxying Requests with ProxyHttpClient

A common requirement in Microservice environments is to proxy requests in a Gateway Microservice to other backend Microservices.

The regular HttpClient API is designed around simplifying message exchange and is not designed for proxying requests. For this case, use the ProxyHttpClient, which can be used from an HTTP Server Filter to proxy requests to backend Microservices.

The following example demonstrates rewriting requests under the URI /proxy to the URI /real onto the same server (although in a real environment you generally proxy to another server):

Proxy filter that rewrites original requests
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.client.ProxyHttpClient;
import io.micronaut.http.filter.HttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain;
import io.micronaut.runtime.server.EmbeddedServer;
import org.reactivestreams.Publisher;

@Filter("/proxy/**")
public class ProxyFilter implements HttpServerFilter { // (1)

    private final ProxyHttpClient client;
    private final EmbeddedServer embeddedServer;

    public ProxyFilter(ProxyHttpClient client,
                       EmbeddedServer embeddedServer) { // (2)
        this.client = client;
        this.embeddedServer = embeddedServer;
    }

    @Override
    public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
                                                      ServerFilterChain chain) {
        return Publishers.map(client.proxy( // (3)
                request.mutate() // (4)
                        .uri(b -> b // (5)
                                .scheme("http")
                                .host(embeddedServer.getHost())
                                .port(embeddedServer.getPort())
                                .replacePath(StringUtils.prependUri(
                                        "/real",
                                        request.getPath().substring("/proxy".length())
                                ))
                        )
                        .header("X-My-Request-Header", "XXX") // (6)
        ), response -> response.header("X-My-Response-Header", "YYY"));
    }
}
Proxy filter that rewrites original requests
import io.micronaut.core.async.publisher.Publishers
import io.micronaut.core.util.StringUtils
import io.micronaut.http.HttpRequest
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.Filter
import io.micronaut.http.client.ProxyHttpClient
import io.micronaut.http.filter.HttpServerFilter
import io.micronaut.http.filter.ServerFilterChain
import io.micronaut.http.uri.UriBuilder
import io.micronaut.runtime.server.EmbeddedServer
import org.reactivestreams.Publisher

@Filter("/proxy/**")
class ProxyFilter implements HttpServerFilter { // (1)

    private final ProxyHttpClient client
    private final EmbeddedServer embeddedServer

    ProxyFilter(ProxyHttpClient client,
                EmbeddedServer embeddedServer) { // (2)
        this.client = client
        this.embeddedServer = embeddedServer
    }

    @Override
    Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
                                               ServerFilterChain chain) {
        Publishers.map(client.proxy( // (3)
                request.mutate() // (4)
                        .uri { UriBuilder b -> // (5)
                            b.with {
                                scheme("http")
                                host(embeddedServer.host)
                                port(embeddedServer.port)
                                replacePath(StringUtils.prependUri(
                                        "/real",
                                        request.path.substring("/proxy".length())
                                ))
                            }
                        }
                        .header("X-My-Request-Header", "XXX") // (6)
        ), { it.header("X-My-Response-Header", "YYY") })
    }
}
Proxy filter that rewrites original requests
import io.micronaut.core.async.publisher.Publishers
import io.micronaut.core.util.StringUtils
import io.micronaut.http.HttpRequest
import io.micronaut.http.MutableHttpResponse
import io.micronaut.http.annotation.Filter
import io.micronaut.http.client.ProxyHttpClient
import io.micronaut.http.filter.HttpServerFilter
import io.micronaut.http.filter.ServerFilterChain
import io.micronaut.http.uri.UriBuilder
import io.micronaut.runtime.server.EmbeddedServer
import org.reactivestreams.Publisher

@Filter("/proxy/**")
class ProxyFilter(
    private val client: ProxyHttpClient, // (2)
    private val embeddedServer: EmbeddedServer
) : HttpServerFilter { // (1)

    override fun doFilter(request: HttpRequest<*>,
                          chain: ServerFilterChain): Publisher<MutableHttpResponse<*>> {
        return Publishers.map(client.proxy( // (3)
            request.mutate() // (4)
                .uri { b: UriBuilder -> // (5)
                    b.apply {
                        scheme("http")
                        host(embeddedServer.host)
                        port(embeddedServer.port)
                        replacePath(StringUtils.prependUri(
                            "/real",
                            request.path.substring("/proxy".length))
                        )
                    }
                }
                .header("X-My-Request-Header", "XXX") // (6)
        ), { response: MutableHttpResponse<*> -> response.header("X-My-Response-Header", "YYY") })
    }
}
1 The filter extends HttpServerFilter
2 The ProxyHttpClient is injected into the constructor.
3 The proxy method proxies the request
4 The request is mutated to modify the URI and include an additional header
5 The UriBuilder API rewrites the URI
6 Additional request and response headers are included
The ProxyHttpClient API is a low-level API that can be used to build a higher-level abstraction such as an API Gateway.

7.5 Declarative HTTP Clients with @Client

Now that you have an understanding of the workings of the lower-level HTTP client, let’s take a look at Micronaut’s support for declarative clients via the Client annotation.

Essentially, the @Client annotation can be declared on any interface or abstract class, and through the use of Introduction Advice the abstract methods are implemented for you at compile time, greatly simplifying the creation of HTTP clients.

Let’s start with a simple example. Given the following class:

Pet.java
public class Pet {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
Pet.java
class Pet {
    String name
    int age
}
Pet.java
class Pet {
    var name: String? = null
    var age: Int = 0
}

You can define a common interface for saving new Pet instances:

PetOperations.java
import io.micronaut.http.annotation.Post;
import io.micronaut.validation.Validated;
import org.reactivestreams.Publisher;
import io.micronaut.core.async.annotation.SingleResult;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;

@Validated
public interface PetOperations {
    @Post
    @SingleResult
    Publisher<Pet> save(@NotBlank String name, @Min(1L) int age);
}
PetOperations.java
import io.micronaut.http.annotation.Post
import io.micronaut.validation.Validated
import org.reactivestreams.Publisher
import io.micronaut.core.async.annotation.SingleResult
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank

@Validated
interface PetOperations {
    @Post
    @SingleResult
    Publisher<Pet> save(@NotBlank String name, @Min(1L) int age)
}
PetOperations.java
import io.micronaut.http.annotation.Post
import io.micronaut.validation.Validated
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank
import io.micronaut.core.async.annotation.SingleResult
import org.reactivestreams.Publisher

@Validated
interface PetOperations {
    @Post
    @SingleResult
    fun save(@NotBlank name: String, @Min(1L) age: Int): Publisher<Pet>
}

Note how the interface uses Micronaut’s HTTP annotations which are usable on both the server and client side. You can also use jakarta.validation constraints to validate arguments.

Be aware that some annotations, such as Produces and Consumes, have different semantics between server and client side usage. For example, @Produces on a controller method (server side) indicates how the method’s return value is formatted, while @Produces on a client indicates how the method’s parameters are formatted when sent to the server. While this may seem a little confusing, it is logical considering the different semantics between a server producing/consuming vs a client: a server consumes an argument and returns a response to the client, whereas a client consumes an argument and sends output to a server.

Additionally, to use the jakarta.validation features, add the validation module to your build:

implementation("io.micronaut:micronaut-validation")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-validation</artifactId>
</dependency>

On the server-side of the Micronaut framework you can implement the PetOperations interface:

PetController.java
import io.micronaut.http.annotation.Controller;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import io.micronaut.core.async.annotation.SingleResult;

@Controller("/pets")
public class PetController implements PetOperations {

    @Override
    @SingleResult
    public Publisher<Pet> save(String name, int age) {
        Pet pet = new Pet();
        pet.setName(name);
        pet.setAge(age);
        // save to database or something
        return Mono.just(pet);
    }
}
PetController.java
import io.micronaut.http.annotation.Controller
import org.reactivestreams.Publisher
import io.micronaut.core.async.annotation.SingleResult
import reactor.core.publisher.Mono

@Controller("/pets")
class PetController implements PetOperations {

    @Override
    @SingleResult
    Publisher<Pet> save(String name, int age) {
        Pet pet = new Pet(name: name, age: age)
        // save to database or something
        return Mono.just(pet)
    }
}
PetController.java
import io.micronaut.http.annotation.Controller
import reactor.core.publisher.Mono
import io.micronaut.core.async.annotation.SingleResult
import org.reactivestreams.Publisher

@Controller("/pets")
open class PetController : PetOperations {

    @SingleResult
    override fun save(name: String, age: Int): Publisher<Pet> {
        val pet = Pet()
        pet.name = name
        pet.age = age
        // save to database or something
        return Mono.just(pet)
    }
}

You can then define a declarative client in src/test/java that uses @Client to automatically implement a client at compile time:

PetClient.java
import io.micronaut.http.client.annotation.Client;
import org.reactivestreams.Publisher;
import io.micronaut.core.async.annotation.SingleResult;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;

@Client("/pets") // (1)
public interface PetClient extends PetOperations { // (2)

    @Override
    @SingleResult
    Publisher<Pet> save(@NotBlank String name, @Min(1L) int age); // (3)
}
PetClient.java
import io.micronaut.http.client.annotation.Client
import org.reactivestreams.Publisher
import io.micronaut.core.async.annotation.SingleResult

@Client("/pets") // (1)
interface PetClient extends PetOperations { // (2)

    @Override
    @SingleResult
    Publisher<Pet> save(String name, int age) // (3)
}
PetClient.java
import io.micronaut.http.client.annotation.Client
import io.micronaut.core.async.annotation.SingleResult
import org.reactivestreams.Publisher

@Client("/pets") // (1)
interface PetClient : PetOperations { // (2)

    @SingleResult
    override fun save(name: String, age: Int): Publisher<Pet> // (3)
}
1 The Client annotation is used with a value relative to the current server, in this case /pets
2 The interface extends from PetOperations
3 The save method is overridden. See the warning below.
Notice in the above example we override the save method. This is necessary if you compile without the -parameters option since Java does not retain parameters names in bytecode otherwise. Overriding is not necessary if you compile with -parameters. In addition, when overriding methods you should ensure any validation annotations are declared again since these are not Inherited annotations.

Once you have defined a client you can @Inject it wherever you need it.

Recall that the value of @Client can be:

  • An absolute URI, e.g. https://api.twitter.com/1.1

  • A relative URI, in which case the server targeted is the current server (useful for testing)

  • A service identifier. See the section on Service Discovery for more information on this topic.

In production, you typically use a service ID and Service Discovery to discover services automatically.

Another important thing to notice regarding the save method in the example above is that it returns a Single type.

This is a non-blocking reactive type - typically you want your HTTP clients to not block. There are cases where you may want an HTTP client that does block (such as in unit tests), but this is rare.

The following table illustrates common return types usable with @Client:

Table 1. Micronaut Response Types
Type Description Example Signature

Publisher

Any type that implements the Publisher interface

Flux<String> hello()

HttpResponse

An HttpResponse and optional response body type

Mono<HttpResponse<String>> hello()

Publisher

A Publisher implementation that emits a POJO

Mono<Book> hello()

CompletableFuture

A Java CompletableFuture instance

CompletableFuture<String> hello()

CharSequence

A blocking native type. Such as String

String hello()

T

Any simple POJO type.

Book show()

Generally, any reactive type that can be converted to the Publisher interface is supported as a return type, including (but not limited to) the reactive types defined by RxJava 1.x, RxJava 2.x, and Reactor 3.x.

Returning CompletableFuture instances is also supported. Note that returning any other type results in a blocking request and is not recommended other than for testing.

7.5.1 Customizing Parameter Binding

The previous example presented a simple example using method parameters to represent the body of a POST request:

PetOperations.java
@Post
@SingleResult
Publisher<Pet> save(@NotBlank String name, @Min(1L) int age);

The save method performs an HTTP POST with the following JSON by default:

Example Produced JSON
{"name":"Dino", "age":10}

You may however want to customize what is sent as the body, the parameters, URI variables, etc. The @Client annotation is very flexible in this regard and supports the same io.micronaut.http.annotation as Micronaut’s HTTP server.

For example, the following defines a URI template, and the name parameter is used as part of the URI template, whilst @Body declares that the contents to send to the server are represented by the Pet POJO:

PetOperations.java
@Post("/{name}")
Mono<Pet> save(
    @NotBlank String name, (1)
    @Body @Valid Pet pet) (2)
1 The name parameter, included as part of the URI, and declared @NotBlank
2 The pet parameter, used to encode the body and declared @Valid

The following table summarizes the parameter annotations and their purpose, and provides an example:

Table 1. Parameter Binding Annotations
Annotation Description Example

@Body

Specifies the parameter for the body of the request

@Body String body

@CookieValue

Specifies parameters to be sent as cookies

@CookieValue String myCookie

@Header

Specifies parameters to be sent as HTTP headers

@Header String requestId

@QueryValue

Customizes the name of the URI parameter to bind from

@QueryValue("userAge") Integer age

@PathVariable

Binds a parameter exclusively from a Path Variable.

@PathVariable Long id

@RequestAttribute

Specifies parameters to be set as request attributes

@RequestAttribute Integer locationId

Always use @Produces or @Consumes instead of supplying a header for Content-Type or Accept.

Query values formatting

The Format annotation can be used together with @QueryValue annotation to format query values.

The supported values are: "csv", "ssv", "pipes", "multi" and "deep-object", where the meaning is similar to Open API v3 query parameter’s style attribute.

The format can only be applied to java.lang.Iterable, java.util.Map or POJO with Introspected annotation. Examples of how different values will be formatted are given in the table below:

Format Iterable example Map or POJO example

Original value

["Mike", "Adam", "Kate"]

{"name": "Mike", "age": 30"}

"CSV"

"param=Mike,Adam,Kate"

"param=name,Mike,age,30"

"SSV"

"param=Mike Adam Kate"

"param=name Mike age 30"

"PIPES"

"param=Mike|Adam|Kate"

"param=name|Mike|age|30"

"MULTI"

"param=Mike&param=Adam&param=Kate"

"name=Mike&age=30"

"DEEP_OBJECT"

"param[0]=Mike&param[1]=Adam&param[2]=Kate"

"param[name]=Mike&param[age]=30"

Type-Based Binding Parameters

Some parameters are recognized by their type instead of their annotation. The following table summarizes these parameter types and their purpose, and provides an example:

Type Description Example

BasicAuth

Sets the Authorization header

BasicAuth basicAuth

HttpHeaders

Adds multiple headers to the request

HttpHeaders headers

Cookies

Adds multiple cookies to the request

Cookies cookies

Cookie

Adds a cookie to the request

Cookie cookie

Locale

Sets the Accept-Language header. Annotate with @QueryValue or @PathVariable to populate a URI variable.

Locale locale

Custom Binding

The ClientArgumentRequestBinder API binds client arguments to the request. Custom binder classes registered as beans are automatically used during the binding process. Annotation-based binders are searched for first, with type-based binders being searched if a binder was not found.

Binding By Annotation

To control how an argument is bound to the request based on an annotation on the argument, create a bean of type AnnotatedClientArgumentRequestBinder. Any custom annotations must be annotated with @Bindable.

In this example, see the following client:

Client With @Metadata Argument
@Client("/")
public interface MetadataClient {

    @Get("/client/bind")
    String get(@Metadata Map<String, Object> metadata);
}
Client With @Metadata Argument
@Client("/")
interface MetadataClient {

    @Get("/client/bind")
    String get(@Metadata Map metadata)
}
Client With @Metadata Argument
@Client("/")
interface MetadataClient {

    @Get("/client/bind")
    operator fun get(@Metadata metadata: Map<String, Any>): String
}

The argument is annotated with the following annotation:

@Metadata Annotation
import io.micronaut.core.bind.annotation.Bindable;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target(PARAMETER)
@Bindable
public @interface Metadata {
}
@Metadata Annotation
import io.micronaut.core.bind.annotation.Bindable

import java.lang.annotation.Documented
import java.lang.annotation.Retention
import java.lang.annotation.Target

import static java.lang.annotation.ElementType.PARAMETER
import static java.lang.annotation.RetentionPolicy.RUNTIME

@Documented
@Retention(RUNTIME)
@Target(PARAMETER)
@Bindable
@interface Metadata {
}
@Metadata Annotation
import io.micronaut.core.bind.annotation.Bindable
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER

@MustBeDocumented
@Retention(RUNTIME)
@Target(VALUE_PARAMETER)
@Bindable
annotation class Metadata

Without any additional code, the client attempts to convert the metadata to a string and append it as a query parameter. In this case that isn’t the desired effect, so a custom binder is needed.

The following binder handles arguments passed to clients that are annotated with the @Metadata annotation, and mutate the request to contain the desired headers. The implementation could be modified to accept more types of data other than Map.

Annotation Argument Binder
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder;
import io.micronaut.http.client.bind.ClientRequestUriContext;

import jakarta.inject.Singleton;
import java.util.Map;

@Singleton
public class MetadataClientArgumentBinder implements AnnotatedClientArgumentRequestBinder<Metadata> {

    @NonNull
    @Override
    public Class<Metadata> getAnnotationType() {
        return Metadata.class;
    }

    @Override
    public void bind(@NonNull ArgumentConversionContext<Object> context,
                     @NonNull ClientRequestUriContext uriContext,
                     @NonNull Object value,
                     @NonNull MutableHttpRequest<?> request) {
        if (value instanceof Map<?,?> map) {
            for (Map.Entry<?, ?> entry: map.entrySet()) {
                String key = NameUtils.hyphenate(StringUtils.capitalize(entry.getKey().toString()), false);
                request.header("X-Metadata-" + key, entry.getValue().toString());
            }
        }
    }
}
Annotation Argument Binder
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.naming.NameUtils
import io.micronaut.core.util.StringUtils
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder
import io.micronaut.http.client.bind.ClientRequestUriContext

import jakarta.inject.Singleton

@Singleton
class MetadataClientArgumentBinder implements AnnotatedClientArgumentRequestBinder<Metadata> {

    final Class<Metadata> annotationType = Metadata

    @Override
    void bind(@NonNull ArgumentConversionContext<Object> context,
              @NonNull ClientRequestUriContext uriContext,
              @NonNull Object value,
              @NonNull MutableHttpRequest<?> request) {
        if (value instanceof Map) {
            for (entry in value.entrySet()) {
                String key = NameUtils.hyphenate(StringUtils.capitalize(entry.key as String), false)
                request.header("X-Metadata-$key", entry.value as String)
            }
        }
    }
}
Annotation Argument Binder
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.naming.NameUtils
import io.micronaut.core.util.StringUtils
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder
import io.micronaut.http.client.bind.ClientRequestUriContext
import jakarta.inject.Singleton

@Singleton
class MetadataClientArgumentBinder : AnnotatedClientArgumentRequestBinder<Metadata> {

    override fun getAnnotationType(): Class<Metadata> {
        return Metadata::class.java
    }

    override fun bind(context: ArgumentConversionContext<Any>,
                      uriContext: ClientRequestUriContext,
                      value: Any,
                      request: MutableHttpRequest<*>) {
        if (value is Map<*, *>) {
            for ((key1, value1) in value) {
                val key = NameUtils.hyphenate(StringUtils.capitalize(key1.toString()), false)
                request.header("X-Metadata-$key", value1.toString())
            }
        }
    }
}
Binding By Type

To bind to the request based on the type of the argument, create a bean of type TypedClientArgumentRequestBinder.

In this example, see the following client:

Client With Metadata Argument
@Client("/")
public interface MetadataClient {

    @Get("/client/bind")
    String get(Metadata metadata);
}
Client With Metadata Argument
@Client("/")
interface MetadataClient {

    @Get("/client/bind")
    String get(Metadata metadata)
}
Client With Metadata Argument
@Client("/")
interface MetadataClient {

    @Get("/client/bind")
    operator fun get(metadata: Metadata?): String?
}

Without any additional code, the client attempts to convert the metadata to a string and append it as a query parameter. In this case that isn’t the desired effect, so a custom binder is needed.

The following binder handles arguments passed to clients of type Metadata and mutate the request to contain the desired headers.

Typed Argument Binder
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.client.bind.ClientRequestUriContext;
import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder;

import jakarta.inject.Singleton;

@Singleton
public class MetadataClientArgumentBinder implements TypedClientArgumentRequestBinder<Metadata> {

    @Override
    @NonNull
    public Argument<Metadata> argumentType() {
        return Argument.of(Metadata.class);
    }

    @Override
    public void bind(@NonNull ArgumentConversionContext<Metadata> context,
                     @NonNull ClientRequestUriContext uriContext,
                     @NonNull Metadata value,
                     @NonNull MutableHttpRequest<?> request) {
        request.header("X-Metadata-Version", value.getVersion().toString());
        request.header("X-Metadata-Deployment-Id", value.getDeploymentId().toString());
    }
}
Typed Argument Binder
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.ClientRequestUriContext
import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder

import jakarta.inject.Singleton

@Singleton
class MetadataClientArgumentBinder implements TypedClientArgumentRequestBinder<Metadata> {

    @Override
    @NonNull
    Argument<Metadata> argumentType() {
        Argument.of(Metadata)
    }

    @Override
    void bind(@NonNull ArgumentConversionContext<Metadata> context,
              @NonNull ClientRequestUriContext uriContext,
              @NonNull Metadata value,
              @NonNull MutableHttpRequest<?> request) {
        request.header("X-Metadata-Version", value.version.toString())
        request.header("X-Metadata-Deployment-Id", value.deploymentId.toString())
    }
}
Typed Argument Binder
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.ClientRequestUriContext
import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder
import jakarta.inject.Singleton

@Singleton
class MetadataClientArgumentBinder : TypedClientArgumentRequestBinder<Metadata> {

    override fun argumentType(): Argument<Metadata> {
        return Argument.of(Metadata::class.java)
    }

    override fun bind(
        context: ArgumentConversionContext<Metadata>,
        uriContext: ClientRequestUriContext,
        value: Metadata,
        request: MutableHttpRequest<*>
    ) {
        request.header("X-Metadata-Version", value.version.toString())
        request.header("X-Metadata-Deployment-Id", value.deploymentId.toString())
    }
}

Binding On Method

It is also possible to create a binder, that would change the request with an annotation on the method. For example:

Client With Annotated Method
@Client("/")
public interface NameAuthorizedClient {

    @Get("/client/authorized-resource")
    @NameAuthorization(name="Bob") // (1)
    String get();
}
Client With Annotated Method
@Client("/")
public interface NameAuthorizedClient {

    @Get("/client/authorized-resource")
    @NameAuthorization(name="Bob") // (1)
    String get()
}
Client With Annotated Method
@Client("/")
public interface NameAuthorizedClient {

    @Get("/client/authorized-resource")
    @NameAuthorization(name="Bob") // (1)
    fun get(): String
}
1 The @NameAuthorization is annotating a method

The annotation is defined as:

Annotation Definition
@Documented
@Retention(RUNTIME)
@Target(METHOD) // (1)
@Bindable
public @interface NameAuthorization {
    @AliasFor(member = "name")
    String value() default "";

    @AliasFor(member = "value")
    String name() default "";
}
Annotation Definition
@Documented
@Retention(RUNTIME)
@Target(METHOD) // (1)
@Bindable
@interface NameAuthorization {
    @AliasFor(member = "name")
    String value() default ""

    @AliasFor(member = "value")
    String name() default ""
}
Annotation Definition
@MustBeDocumented
@Retention(RUNTIME)
@Target(FUNCTION) // (1)
@Bindable
annotation class NameAuthorization(val name: String = "")
1 It is defined to be used on methods

The following binder specifies the behaviour:

Annotation Definition
@Singleton // (1)
public class NameAuthorizationBinder implements AnnotatedClientRequestBinder<NameAuthorization> { // (2)
    @NonNull
    @Override
    public Class<NameAuthorization> getAnnotationType() {
        return NameAuthorization.class;
    }

    @Override
    public void bind( // (3)
            @NonNull MethodInvocationContext<Object, Object> context,
            @NonNull ClientRequestUriContext uriContext,
            @NonNull MutableHttpRequest<?> request
    ) {
        context.getValue(NameAuthorization.class)
                .ifPresent(name -> uriContext.addQueryParameter("name", String.valueOf(name)));

    }
}
Annotation Definition
@Singleton // (1)
public class NameAuthorizationBinder implements AnnotatedClientRequestBinder<NameAuthorization> { // (2)
    @NonNull
    @Override
    Class<NameAuthorization> getAnnotationType() {
        return NameAuthorization.class
    }

    @Override
    void bind( // (3)
            @NonNull MethodInvocationContext<Object, Object> context,
            @NonNull ClientRequestUriContext uriContext,
            @NonNull MutableHttpRequest<?> request
    ) {
        context.getValue(NameAuthorization.class)
                .ifPresent(name -> uriContext.addQueryParameter("name", String.valueOf(name)))

    }
}
Annotation Definition
import io.micronaut.http.client.bind.AnnotatedClientRequestBinder

@Singleton // (1)
class NameAuthorizationBinder: AnnotatedClientRequestBinder<NameAuthorization> { // (2)
    @NonNull
    override fun getAnnotationType(): Class<NameAuthorization> {
        return NameAuthorization::class.java
    }

    override fun bind( // (3)
            @NonNull context: MethodInvocationContext<Any, Any>,
            @NonNull uriContext: ClientRequestUriContext,
            @NonNull request: MutableHttpRequest<*>
    ) {
        context.getValue(NameAuthorization::class.java, "name")
                .ifPresent { name -> uriContext.addQueryParameter("name", name.toString()) }

    }
}
1 The @Singleton annotation registers it in Micronaut context
2 It implements the AnnotatedClientRequestBinder<NameAuthorization>
3 The custom bind method is used to implement the behaviour of the binder

7.5.2 Streaming with @Client

The @Client annotation can also handle streaming HTTP responses.

Streaming JSON with @Client

For example, to write a client that streams data from the controller defined in the JSON Streaming section of the documentation, you can define a client that returns an unbound Publisher such as Reactor’s Flux or a RxJava’s Flowable:

HeadlineClient.java
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import org.reactivestreams.Publisher;

import static io.micronaut.http.MediaType.APPLICATION_JSON_STREAM;

@Client("/streaming")
public interface HeadlineClient {

    @Get(value = "/headlines", processes = APPLICATION_JSON_STREAM) // (1)
    Publisher<Headline> streamHeadlines(); // (2)

}
HeadlineClient.java
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import org.reactivestreams.Publisher

import static io.micronaut.http.MediaType.APPLICATION_JSON_STREAM

@Client("/streaming")
interface HeadlineClient {

    @Get(value = "/headlines", processes = APPLICATION_JSON_STREAM) // (1)
    Publisher<Headline> streamHeadlines() // (2)

}
HeadlineClient.java
import io.micronaut.http.MediaType.APPLICATION_JSON_STREAM
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import reactor.core.publisher.Flux


@Client("/streaming")
interface HeadlineClient {

    @Get(value = "/headlines", processes = [APPLICATION_JSON_STREAM]) // (1)
    fun streamHeadlines(): Flux<Headline>  // (2)

}
1 The @Get method processes responses of type APPLICATION_JSON_STREAM
2 The return type is Publisher

The following example shows how the previously defined HeadlineClient can be invoked from a test:

Streaming HeadlineClient
@Test
void testClientAnnotationStreaming() {
    try(EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class)) {
        HeadlineClient headlineClient = embeddedServer
                                            .getApplicationContext()
                                            .getBean(HeadlineClient.class); // (1)

        Mono<Headline> firstHeadline = Mono.from(headlineClient.streamHeadlines()); // (2)

        Headline headline = firstHeadline.block(); // (3)

        assertNotNull(headline);
        assertTrue(headline.getText().startsWith("Latest Headline"));
    }
}
Streaming HeadlineClient
void "test client annotation streaming"() throws Exception {
    when:
    def headlineClient = embeddedServer.applicationContext
            .getBean(HeadlineClient) // (1)

    Mono<Headline> firstHeadline = Mono.from(headlineClient.streamHeadlines()) // (2)

    Headline headline = firstHeadline.block() // (3)

    then:
    headline
    headline.text.startsWith("Latest Headline")
}
Streaming HeadlineClient
"test client annotation streaming" {
    val headlineClient = embeddedServer
        .applicationContext
        .getBean(HeadlineClient::class.java) // (1)

    val firstHeadline = headlineClient.streamHeadlines().next() // (2)

    val headline = firstHeadline.block() // (3)

    headline shouldNotBe null
    headline.text shouldStartWith "Latest Headline"
}
1 The client is retrieved from the ApplicationContext
2 The next method emits the first element emmited by the Flux into a Mono.
3 The block() method retrieves the result in the test.

Streaming Clients and Response Types

The example defined in the previous section expects the server to respond with a stream of JSON objects, and the content type to be application/x-json-stream. For example:

A JSON Stream
{"title":"The Stand"}
{"title":"The Shining"}

The reason for this is simple; a sequence of JSON object is not, in fact, valid JSON and hence the response content type cannot be application/json. For the JSON to be valid it would have to return an array:

A JSON Array
[
    {"title":"The Stand"},
    {"title":"The Shining"}
]

Micronaut’s client does however support streaming of both individual JSON objects via application/x-json-stream and also JSON arrays defined with application/json.

If the server returns application/json and a non-single Publisher is returned (such as a Reactor’s Flux or a RxJava’s Flowable), the client streams the array elements as they become available.

Streaming Clients and Read Timeout

When streaming responses from servers, the underlying HTTP client will not apply the default readTimeout setting (which defaults to 10 seconds) of the HttpClientConfiguration since the delay between reads for streaming responses may differ from normal reads.

Instead, the read-idle-timeout setting (which defaults to 5 minutes) dictates when to close a connection after it becomes idle.

If you stream data from a server that defines a longer delay than 5 minutes between items, you should adjust readIdleTimeout. The following configuration in your configuration file (e.g. application.yml) demonstrates how:

Adjusting the readIdleTimeout
micronaut.http.client.read-idle-timeout=10m
micronaut:
  http:
    client:
      read-idle-timeout: 10m
[micronaut]
  [micronaut.http]
    [micronaut.http.client]
      read-idle-timeout="10m"
micronaut {
  http {
    client {
      readIdleTimeout = "10m"
    }
  }
}
{
  micronaut {
    http {
      client {
        read-idle-timeout = "10m"
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "client": {
        "read-idle-timeout": "10m"
      }
    }
  }
}

The above example sets the readIdleTimeout to ten minutes.

Streaming Server Sent Events

The Micronaut framework features a native client for Server Sent Events (SSE) defined by the interface SseClient.

You can use this client to stream SSE events from any server that emits them.

Although SSE streams are typically consumed by a browser EventSource, there are cases where you may wish to consume an SSE stream via SseClient, such as in unit tests or when a Micronaut service acts as a gateway for another service.

The @Client annotation also supports consuming SSE streams. For example, consider the following controller method that produces a stream of SSE events:

SSE Controller
@Get(value = "/headlines", processes = MediaType.TEXT_EVENT_STREAM) // (1)
Publisher<Event<Headline>> streamHeadlines() {
    return Flux.<Event<Headline>>create((emitter) -> {  // (2)
        Headline headline = new Headline();
        headline.setText("Latest Headline at " + ZonedDateTime.now());
        emitter.next(Event.of(headline));
        emitter.complete();
    }, FluxSink.OverflowStrategy.BUFFER)
            .repeat(100) // (3)
            .delayElements(Duration.of(1, ChronoUnit.SECONDS)); // (4)
}
SSE Controller
@Get(value = "/headlines", processes = MediaType.TEXT_EVENT_STREAM) // (1)
Flux<Event<Headline>> streamHeadlines() {
    Flux.<Event<Headline>>create( { emitter -> // (2)
        Headline headline = new Headline(text: "Latest Headline at ${ZonedDateTime.now()}")
        emitter.next(Event.of(headline))
        emitter.complete()
    }, FluxSink.OverflowStrategy.BUFFER)
            .repeat(100) // (3)
            .delayElements(Duration.of(1, ChronoUnit.SECONDS)) // (4)
}
SSE Controller
@Get(value = "/headlines", processes = [TEXT_EVENT_STREAM]) // (1)
internal fun streamHeadlines(): Flux<Event<Headline>> {
    return Flux.create<Event<Headline>>( { emitter -> // (2)
        val headline = Headline()
        headline.text = "Latest Headline at ${ZonedDateTime.now()}"
        emitter.next(Event.of(headline))
        emitter.complete()
    }, FluxSink.OverflowStrategy.BUFFER)
        .repeat(100) // (3)
        .delayElements(Duration.of(1, ChronoUnit.SECONDS)) // (4)
}
1 The controller defines a @Get annotation that produces a MediaType.TEXT_EVENT_STREAM
2 The method uses Reactor to emit a Headline object
3 The repeat method repeats the emission 100 times
4 With a delay of one second between each

Notice that the return type of the controller is also Event and that the Event.of method creates events to stream to the client.

To define a client that consumes the events, define a method that processes MediaType.TEXT_EVENT_STREAM:

SSE Client
@Client("/streaming/sse")
public interface HeadlineClient {

    @Get(value = "/headlines", processes = TEXT_EVENT_STREAM)
    Publisher<Event<Headline>> streamHeadlines();
}
SSE Client
@Client("/streaming/sse")
interface HeadlineClient {

    @Get(value = "/headlines", processes = TEXT_EVENT_STREAM)
    Publisher<Event<Headline>> streamHeadlines()
}
SSE Client
@Client("/streaming/sse")
interface HeadlineClient {

    @Get(value = "/headlines", processes = [TEXT_EVENT_STREAM])
    fun streamHeadlines(): Flux<Event<Headline>>
}

The generic type of the Flux can be either an Event, in which case you will receive the full event object, or a POJO, in which case you will receive only the data contained within the event converted from JSON.

7.5.3 Error Responses

If an HTTP response is returned with a code of 400 or higher, an HttpClientResponseException is created. The exception contains the original response. How that exception is thrown depends on the method return type.

  • For reactive response types, the exception is passed through the publisher as an error.

  • For blocking response types, the exception is thrown and should be caught and handled by the caller.

The one exception to this rule is HTTP Not Found (404) responses. This exception only applies to the declarative client.

HTTP Not Found (404) responses for blocking return types is not considered an error condition and the client exception will not be thrown. That behavior includes methods that return void.

If the method returns an HttpResponse, the original response is returned. If the return type is Optional, an empty optional is returned. For all other types, null is returned.

7.5.4 Customizing Request Headers

Customizing the request headers deserves special mention as there are several ways that can be accomplished.

Populating Headers Using Configuration

The @Header annotation can be declared at the type level and is repeatable such that it is possible to drive the request headers sent via configuration using annotation metadata.

The following example serves to illustrate this:

Defining Headers via Configuration
@Client("/pets")
@Header(name="X-Pet-Client", value="${pet.client.id}")
public interface PetClient extends PetOperations {

    @Override
    @SingleResult
    Publisher<Pet> save(String name, int age);

    @Get("/{name}")
    @SingleResult
    Publisher<Pet> get(String name);
}
Defining Headers via Configuration
@Client("/pets")
@Header(name="X-Pet-Client", value='${pet.client.id}')
interface PetClient extends PetOperations {

    @Override
    @SingleResult
    Publisher<Pet> save(@NotBlank String name, @Min(1L) int age)

    @Get("/{name}")
    @SingleResult
    Publisher<Pet> get(String name)
}
Defining Headers via Configuration
@Client("/pets")
@Header(name = "X-Pet-Client", value = "\${pet.client.id}")
interface PetClient : PetOperations {

    @SingleResult
    override fun save(name: String, age: Int): Publisher<Pet>

    @Get("/{name}")
    @SingleResult
    operator fun get(name: String): Publisher<Pet>
}

The above example defines a @Header annotation on the PetClient interface that reads the pet.client.id property using property placeholder configuration.

Then set the following in your configuration file (e.g. application.yml) to populate the value:

Configuring Headers
pet.client.id=foo
pet:
  client:
    id: foo
[pet]
  [pet.client]
    id="foo"
pet {
  client {
    id = "foo"
  }
}
{
  pet {
    client {
      id = "foo"
    }
  }
}
{
  "pet": {
    "client": {
      "id": "foo"
    }
  }
}

Alternatively you can supply a PET_CLIENT_ID environment variable and the value will be populated.

Populating Headers using a Client Filter

Alternatively, to dynamically populate headers, another option is to use a Client Filter.

For more information on writing client filters see the Client Filters section of the guide.

7.5.5 Customizing Jackson Settings

As mentioned previously, Jackson is used for message encoding to JSON. A default Jackson ObjectMapper is configured and used by Micronaut HTTP clients.

You can override the settings used to construct the ObjectMapper with properties defined by the JacksonConfiguration class in your configuration file (e.g application.yml).

For example, the following configuration enables indented output for Jackson:

Example Jackson Configuration
jackson.serialization.indentOutput=true
jackson:
  serialization:
    indentOutput: true
[jackson]
  [jackson.serialization]
    indentOutput=true
jackson {
  serialization {
    indentOutput = true
  }
}
{
  jackson {
    serialization {
      indentOutput = true
    }
  }
}
{
  "jackson": {
    "serialization": {
      "indentOutput": true
    }
  }
}

However, these settings apply globally and impact both how the HTTP server renders JSON and how JSON is sent from the HTTP client. Given that, sometimes it is useful to provide client-specific Jackson settings. You can do this with the @JacksonFeatures annotation on a client:

As an example, the following snippet is taken from Micronaut’s native Eureka client (which of course uses Micronaut’s HTTP client):

Example of JacksonFeatures
@Client(id = EurekaClient.SERVICE_ID,
        path = "/eureka",
        configuration = EurekaConfiguration.class)
@JacksonFeatures(
    enabledSerializationFeatures = WRAP_ROOT_VALUE,
    disabledSerializationFeatures = WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED,
    enabledDeserializationFeatures = {UNWRAP_ROOT_VALUE, ACCEPT_SINGLE_VALUE_AS_ARRAY}
)
public interface EurekaClient {
    ...
}

The Eureka serialization format for JSON uses the WRAP_ROOT_VALUE serialization feature of Jackson, hence it is enabled just for that client.

If the customization offered by JacksonFeatures is not enough, you can also write a BeanCreatedEventListener for the ObjectMapper and add whatever customizations you need.

7.5.6 Retry and Circuit Breaker

Recovering from failure is critical for HTTP clients, and that is where Micronaut’s integrated Retry Advice comes in handy.

Since Micronaut Framework 4.0, declarative clients annotated with @Client no longer invoke fallbacks by default. To restore the previous behaviour add the following dependency and annotate any declarative clients with @Recoverable.

implementation("io.micronaut:micronaut-retry")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-retry</artifactId>
</dependency>

You can declare the @Retryable or @CircuitBreaker annotations on any @Client interface and the retry policy will be applied, for example:

Declaring @Retryable
@Client("/pets")
@Retryable
public interface PetClient extends PetOperations {

    @Override
    @SingleResult
    Publisher<Pet> save(String name, int age);
}
Declaring @Retryable
@Client("/pets")
@Retryable
interface PetClient extends PetOperations {

    @Override
    Mono<Pet> save(String name, int age)
}
Declaring @Retryable
@Client("/pets")
@Retryable
interface PetClient : PetOperations {

    override fun save(name: String, age: Int): Mono<Pet>
}

For more information on customizing retry, see the section on Retry Advice.

7.5.7 Client Fallbacks

In distributed systems, failure happens, and it is best to be prepared for it and handle it gracefully.

In addition, when developing Microservices it is quite common to work on a single Microservice without other Microservices the project requires being available.

With that in mind, the Micronaut framework features a native fallback mechanism that is integrated into Retry Advice that allows falling back to another implementation in the case of failure.

Using the @Fallback annotation, you can declare a fallback implementation of a client to be used when all possible retries have been exhausted.

In fact the mechanism is not strictly linked to Retry; you can declare any class as @Recoverable, and if a method call fails (or, in the case of reactive types, an error is emitted) a class annotated with @Fallback will be searched for.

To illustrate this, consider again the PetOperations interface declared earlier. You can define a PetFallback class that will be called in the case of failure:

Defining a Fallback
@Fallback
public class PetFallback implements PetOperations {
    @Override
    @SingleResult
    public Publisher<Pet> save(String name, int age) {
        Pet pet = new Pet();
        pet.setAge(age);
        pet.setName(name);
        return Mono.just(pet);
    }
}
Defining a Fallback
@Fallback
class PetFallback implements PetOperations {
    @Override
    Mono<Pet> save(String name, int age) {
        Pet pet = new Pet(age: age, name: name)
        return Mono.just(pet)
    }
}
Defining a Fallback
@Fallback
open class PetFallback : PetOperations {
    override fun save(name: String, age: Int): Mono<Pet> {
        val pet = Pet()
        pet.age = age
        pet.name = name
        return Mono.just(pet)
    }
}
If you only need fallbacks to help with testing against external Microservices, you can define fallbacks in the src/test/java directory, so they are not included in production code. You will have to specify @Recoverable(api = PetOperations.class) on the declarative client if you are using fallbacks without hystrix.

As you can see the fallback does not perform any network operations and is quite simple, hence will provide a successful result in the case of an external system being down.

Of course, the actual behaviour of the fallback is up to you. You can for example implement a fallback that pulls data from a local cache when real data is not available, and sends alert emails or other notifications to operations about downtime.

7.5.8 Netflix Hystrix Support

Using the CLI

If you create your project using the Micronaut CLI, supply the netflix-hystrix feature to configure Hystrix in your project:

$ mn create-app my-app --features netflix-hystrix

Netflix Hystrix is a fault tolerance library developed by the Netflix team and is designed to improve resilience of interprocess communication.

The Micronaut framework integrates with Hystrix through the netflix-hystrix module, which you can add to your build:

implementation("io.micronaut.netflix:micronaut-netflix-hystrix")
<dependency>
    <groupId>io.micronaut.netflix</groupId>
    <artifactId>micronaut-netflix-hystrix</artifactId>
</dependency>

Using the @HystrixCommand Annotation

With the above dependency declared you can annotate any method (including methods defined on @Client interfaces) with the HystrixCommand annotation, and method’s execution will be wrapped in a Hystrix command. For example:

Using @HystrixCommand
@HystrixCommand
String hello(String name) {
    return "Hello $name"
}
This works for reactive return types such as Flux, and the reactive type will be wrapped in a HystrixObservableCommand.

The HystrixCommand annotation also integrates with Micronaut’s support for Retry Advice and Fallbacks

For information on how to customize the Hystrix thread pool, group, and properties, see the Javadoc for HystrixCommand.

Enabling Hystrix Stream and Dashboard

You can enable a Server Sent Event stream to feed into the Hystrix Dashboard by setting the hystrix.stream.enabled setting to true in your configuration file (e.g application.yml):

Enabling Hystrix Stream
hystrix.stream.enabled=true
hystrix:
  stream:
    enabled: true
[hystrix]
  [hystrix.stream]
    enabled=true
hystrix {
  stream {
    enabled = true
  }
}
{
  hystrix {
    stream {
      enabled = true
    }
  }
}
{
  "hystrix": {
    "stream": {
      "enabled": true
    }
  }
}

This exposes a /hystrix.stream endpoint with the format the Hystrix Dashboard expects.

7.6 HTTP Client Filters

Often, you need to include the same HTTP headers or URL parameters in a set of requests against a third-party API or when calling another Microservice.

To simplify this, you can define ClientFilter classes that are applied to all matching HTTP client requests. The details of how filters worked are described in the server filter documentation. Here we will only show some client filters.

As an example, say you want to build a client to communicate with the Bintray REST API. It would be tedious to specify authentication for every single HTTP call.

To resolve this you can define a filter. The following is an example BintrayService:

class BintrayApi {
    public static final String URL = 'https://api.bintray.com'
}

@Singleton
class BintrayService {
    final HttpClient client;
    final String org;

    BintrayService(
            @Client(BintrayApi.URL) HttpClient client,           // (1)
            @Value("${bintray.organization}") String org ) {
        this.client = client;
        this.org = org;
    }

    Flux<HttpResponse<String>> fetchRepositories() {
        return Flux.from(client.exchange(HttpRequest.GET(
                "/repos/" + org), String.class)); // (2)
    }

    Flux<HttpResponse<String>> fetchPackages(String repo) {
        return Flux.from(client.exchange(HttpRequest.GET(
                "/repos/" + org + "/" + repo + "/packages"), String.class)); // (2)
    }
}
class BintrayApi {
    public static final String URL = 'https://api.bintray.com'
}

@Singleton
class BintrayService {
    final HttpClient client
    final String org

    BintrayService(
            @Client(BintrayApi.URL) HttpClient client, // (1)
            @Value('${bintray.organization}') String org ) {
        this.client = client
        this.org = org
    }

    Flux<HttpResponse<String>> fetchRepositories() {
        client.exchange(HttpRequest.GET("/repos/$org"), String) // (2)
    }

    Flux<HttpResponse<String>> fetchPackages(String repo) {
        client.exchange(HttpRequest.GET("/repos/${org}/${repo}/packages"), String) // (2)
    }
}
class BintrayApi {
    public static final String URL = 'https://api.bintray.com'
}

@Singleton
internal class BintrayService(
    @param:Client(BintrayApi.URL) val client: HttpClient, // (1)
    @param:Value("\${bintray.organization}") val org: String) {

    fun fetchRepositories(): Flux<HttpResponse<String>> {
        return Flux.from(client.exchange(HttpRequest.GET<Any>("/repos/$org"), String::class.java)) // (2)
    }

    fun fetchPackages(repo: String): Flux<HttpResponse<String>> {
        return Flux.from(client.exchange(HttpRequest.GET<Any>("/repos/$org/$repo/packages"), String::class.java)) // (2)
    }
}
1 An ReactorHttpClient is injected for the Bintray API
2 The organization is configurable via configuration

The Bintray API is secured. To authenticate you add an Authorization header for every request. You can modify fetchRepositories and fetchPackages methods to include the necessary HTTP Header for each request, but using a filter is much simpler:

@ClientFilter("/repos/**") // (1)
class BintrayFilter {

    final String username;
    final String token;

    BintrayFilter(
            @Value("${bintray.username}") String username, // (2)
            @Value("${bintray.token}") String token ) { // (2)
        this.username = username;
        this.token = token;
    }

    @RequestFilter
    public void filter(MutableHttpRequest<?> request) {
        request.basicAuth(username, token); // (3)
    }
}
@ClientFilter('/repos/**') // (1)
class BintrayFilter {

    final String username
    final String token

    BintrayFilter(
            @Value('${bintray.username}') String username, // (2)
            @Value('${bintray.token}') String token ) { // (2)
        this.username = username
        this.token = token
    }

    @RequestFilter
    void filter(MutableHttpRequest<?> request) {
        request.basicAuth(username, token) // (3)
    }
}
@ClientFilter("/repos/**") // (1)
internal class BintrayFilter(
        @param:Value("\${bintray.username}") val username: String, // (2)
        @param:Value("\${bintray.token}") val token: String // (2)
) {

    @RequestFilter
    fun filter(request: MutableHttpRequest<*>) {
        request.basicAuth(username, token) // (3)
    }
}
1 You can match only a subset of paths with a Client filter.
2 The username and token are injected via configuration
3 The basicAuth method includes HTTP Basic credentials

Now when you invoke the bintrayService.fetchRepositories() method, the Authorization HTTP header is included in the request.

Injecting Another Client into a ClientFilter

To create an ReactorHttpClient, the Micronaut framework needs to resolve all ClientFilter beans, which creates a circular dependency when injecting another ReactorHttpClient or a @Client bean into an instance of a ClientFilter.

To resolve this issue, use the BeanProvider interface to inject another ReactorHttpClient or a @Client bean into an instance of ClientFilter.

The following example which implements a filter allowing authentication between services on Google Cloud Run demonstrates how to use BeanProvider to inject another client:

import io.micronaut.context.BeanProvider;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.annotation.ClientFilter;
import io.micronaut.http.annotation.RequestFilter;
import io.micronaut.http.client.HttpClient;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;

@Requires(env = Environment.GOOGLE_COMPUTE)
@ClientFilter(patterns = "/google-auth/api/**")
public class GoogleAuthFilter {

    private final BeanProvider<HttpClient> authClientProvider;

    public GoogleAuthFilter(BeanProvider<HttpClient> httpClientProvider) { // (1)
        this.authClientProvider = httpClientProvider;
    }

    @RequestFilter
    @ExecuteOn(TaskExecutors.BLOCKING)
    public void filter(MutableHttpRequest<?> request) throws Exception {
        String uri = encodeURI(request);
        String t = authClientProvider.get().toBlocking().retrieve(HttpRequest.GET(uri) // (2)
            .header("Metadata-Flavor", "Google"));
        request.bearerAuth(t);
    }

    private String encodeURI(MutableHttpRequest<?> request) throws UnsupportedEncodingException {
        URI fullURI = request.getUri();
        String receivingURI = fullURI.getScheme() + "://" + fullURI.getHost();
        return "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" +
                URLEncoder.encode(receivingURI, "UTF-8");
    }
}
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.annotation.ClientFilter
import io.micronaut.http.annotation.RequestFilter
import io.micronaut.http.client.HttpClient
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn

@Requires(env = Environment.GOOGLE_COMPUTE)
@ClientFilter(patterns = "/google-auth/api/**")
class GoogleAuthFilter {

    private final BeanProvider<HttpClient> authClientProvider

    GoogleAuthFilter(BeanProvider<HttpClient> httpClientProvider) { // (1)
        this.authClientProvider = httpClientProvider
    }

    @RequestFilter
    @ExecuteOn(TaskExecutors.BLOCKING)
    void filter(MutableHttpRequest<?> request) {
        String authURI = encodeURI(request)
        String token = authClientProvider.get().toBlocking().retrieve(HttpRequest.GET(authURI).header( // (2)
                "Metadata-Flavor", "Google"
        ))

        request.bearerAuth(token)
    }

    private static String encodeURI(MutableHttpRequest<?> request) {
        String receivingURI = "$request.uri.scheme://$request.uri.host"
        "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" +
                URLEncoder.encode(receivingURI, "UTF-8")
    }
}
import io.micronaut.context.BeanProvider
import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.micronaut.http.HttpRequest
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.annotation.ClientFilter
import io.micronaut.http.annotation.RequestFilter
import io.micronaut.http.client.HttpClient
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn
import java.net.URLEncoder

@Requires(env = [Environment.GOOGLE_COMPUTE])
@ClientFilter(patterns = ["/google-auth/api/**"])
class GoogleAuthFilter (
    private val authClientProvider: BeanProvider<HttpClient>) { // (1)

    @RequestFilter
    @ExecuteOn(TaskExecutors.BLOCKING)
    fun filter(request: MutableHttpRequest<*>) {
        val authURI = encodeURI(request)
        val t = authClientProvider.get().toBlocking().retrieve(HttpRequest.GET<Any>(authURI)
            .header("Metadata-Flavor", "Google") // (2)
        )
        request.bearerAuth(t.toString())
    }

    private fun encodeURI(request: MutableHttpRequest<*>): String {
        val receivingURI = "${request.uri.scheme}://${request.uri.host}"
        return "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" +
                URLEncoder.encode(receivingURI, "UTF-8")
    }

}
1 The BeanProvider interface is used to inject another client, avoiding a circular reference
2 The get() method of the Provider interface is used to obtain the client instance.

Filter Matching By Annotation

For cases where a filter should be applied to a client regardless of the URL, filters can be matched by the presence of an annotation applied to both the filter and the client. Given the following client:

import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;

@BasicAuth // (1)
@Client("/message")
public interface BasicAuthClient {

    @Get
    String getMessage();
}
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client

@BasicAuth // (1)
@Client("/message")
interface BasicAuthClient {

    @Get
    String getMessage()
}
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client

@BasicAuth // (1)
@Client("/message")
interface BasicAuthClient {

    @Get
    fun getMessage(): String
}
1 The @BasicAuth annotation is applied to the client

The following filter will filter the client requests:

import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.annotation.ClientFilter;
import io.micronaut.http.annotation.RequestFilter;
import jakarta.inject.Singleton;

@BasicAuth // (1)
@Singleton // (2)
@ClientFilter
public class BasicAuthClientFilter {

    @RequestFilter
    public void filter(MutableHttpRequest<?> request) {
        request.basicAuth("user", "pass");
    }
}
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.annotation.ClientFilter
import io.micronaut.http.annotation.RequestFilter
import jakarta.inject.Singleton

@BasicAuth // (1)
@Singleton // (2)
@ClientFilter
class BasicAuthClientFilter {

    @RequestFilter
    void filter(MutableHttpRequest<?> request) {
        request.basicAuth("user", "pass")
    }
}
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.annotation.ClientFilter
import io.micronaut.http.annotation.RequestFilter
import jakarta.inject.Singleton

@BasicAuth // (1)
@Singleton // (2)
@ClientFilter
class BasicAuthClientFilter {

    @RequestFilter
    fun doFilter(request: MutableHttpRequest<*>) {
        request.basicAuth("user", "pass")
    }
}
1 The same annotation, @BasicAuth, is applied to the filter
2 Normally the @Filter annotation makes filters singletons by default. Because the @Filter annotation is not used, the desired scope must be applied

The @BasicAuth annotation is just an example and can be replaced with your own.

import io.micronaut.http.annotation.FilterMatcher;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@FilterMatcher // (1)
@Documented
@Retention(RUNTIME)
@Target({TYPE, PARAMETER})
public @interface BasicAuth {
}
import io.micronaut.http.annotation.FilterMatcher

import java.lang.annotation.Documented
import java.lang.annotation.Retention
import java.lang.annotation.Target

import static java.lang.annotation.ElementType.PARAMETER
import static java.lang.annotation.ElementType.TYPE
import static java.lang.annotation.RetentionPolicy.RUNTIME

@FilterMatcher // (1)
@Documented
@Retention(RUNTIME)
@Target([TYPE, PARAMETER])
@interface BasicAuth {
}
import io.micronaut.http.annotation.FilterMatcher
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER

@FilterMatcher // (1)
@MustBeDocumented
@Retention(RUNTIME)
@Target(CLASS, VALUE_PARAMETER)
annotation class BasicAuth
1 The only requirement for custom annotations is that the @FilterMatcher annotation must be present

7.7 HTTP/2 Support

By default, Micronaut’s HTTP client is configured to support HTTP 1.1. To enable support for HTTP/2, set the supported HTTP version in configuration:

Enabling HTTP/2 in Clients
micronaut.http.client.http-version=2.0
micronaut:
  http:
    client:
      http-version: 2.0
[micronaut]
  [micronaut.http]
    [micronaut.http.client]
      http-version=2.0
micronaut {
  http {
    client {
      httpVersion = 2.0
    }
  }
}
{
  micronaut {
    http {
      client {
        http-version = 2.0
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "client": {
        "http-version": 2.0
      }
    }
  }
}

Or by specifying the HTTP version to use when injecting the client:

Injecting an HTTP/2 Client
@Inject
@Client(httpVersion=HttpVersion.HTTP_2_0)
ReactorHttpClient client;

7.8 HTTP/3 Support

Since the Micronaut framework 4.x, Micronaut’s Netty-based HTTP client can be configured to support HTTP/3. This support is experimental and may change without notice.

Instead of the TCP used for HTTP/1.1 and HTTP/2, HTTP/3 runs on UDP. If the client is configured with the special h3 value for the alpn-modes property, the client will automatically use HTTP/3 over UDP instead of HTTP/1.1 or HTTP/2 over TCP. At this time, the client cannot fall back to TCP if the server does not support HTTP/3.

Enabling HTTP/3 in Clients
micronaut:
  http:
    client:
      alpn-modes: [h3]

Additionally, the netty HTTP/3 codec needs to be present on the classpath:

implementation("io.netty.incubator:netty-incubator-codec-http3")
<dependency>
    <groupId>io.netty.incubator</groupId>
    <artifactId>netty-incubator-codec-http3</artifactId>
</dependency>

7.9 HTTP Client Sample

Read the HTTP Client Guide (Java, Groovy, Kotlin), a step-by-step tutorial, to learn more.

8 Context propagation

The new Propagation Context API aims to simplify reactor instrumentation, avoid thread-local usage, and integrate idiomatically with Kotlin Coroutines.

The PropagatedContext object represents the context propagation. We designed the context propagation API for immutability. It consists of multiple elements of type PropagatedContextElement.

Each element represents a particular state that needs to be propagated. There is a special element that can be used to update and restore the thread-local value ThreadPropagatedContextElement:

Example of MDC propagated element implementing ThreadPropagatedContextElement
public record MdcPropagationContext(Map<String, String> state) implements ThreadPropagatedContextElement<Map<String, String>> { (1)

    public MdcPropagationContext() {
        this(MDC.getCopyOfContextMap());
    }

    @Override
    public Map<String, String> updateThreadContext() {
        Map<String, String> oldState = MDC.getCopyOfContextMap();
        setCurrent(state); (2)
        return oldState; (3)
    }

    @Override
    public void restoreThreadContext(Map<String, String> oldState) {
        setCurrent(oldState); (4)
    }

    private void setCurrent(Map<String, String> contextMap) {
        if (contextMap == null) {
            MDC.clear();
        } else {
            MDC.setContextMap(contextMap);
        }
    }
}
1 The class has the MDC state passed in the contractor
2 The context update sets the MDC state as the current one
3 The previous state is captured to be restored
4 The previous state is restored

In this example, the propagated context element implements the setting of the MDC context, and it restores it after.

The ThreadPropagatedContextElement is inspired by Kotlin Coroutines propagation API element kotlinx.coroutines.ThreadContextElement
MDC propagation example
public String createUser(String name) {
    try {
        UUID newUserId = UUID.randomUUID();
        MDC.put("userId", newUserId.toString());
        try (PropagatedContext.Scope ignore = PropagatedContext.getOrEmpty().plus(new MdcPropagationContext()).propagate()) {
            return createUserInternal(newUserId, name);
        }
    } finally {
        MDC.remove("userId");
    }
}
In the previous versions of Micronaut Framework, we would capture the context to propagate, this is not the case since Micronaut Framework 4. We always require for the context to be modified manually and repropagated.

8.1 Reactor context propagation

Since Micronaut Framework version 4, Project Reactor integration no longer captures the state automatically. Micronaut Framework users need to extend the propagation context manually.

Before version 4, Micronaut Framework required the instrumentation of every reactive operator to capture the current state to propagate it. It added an unwanted overhead and forced us to maintain complicated Reactor operators' instrumentation.

Since 3.5.0, Reactor-Core embeds support for the io.micrometer:context-propagation SPI. This allows to achieve the same thread-local propagation by including the Micrometer Context Propagation dependency.

The framework automatically adds the PropagatedContext to Project Reactor’s context for interceptors and the HTTP filters. You can access it via the utility class ReactorPropagation.

ReactorPropagation is an experimental class and might change in the future.

It is possible to use Micrometer Context Propagation, which Reactor supports for propagation and restoring the thread-local context.

To enable it, include the dependency:

implementation("io.micrometer:context-propagation")
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>context-propagation</artifactId>
</dependency>

After that, all the thread-local propagated elements can restore their thread-local value.

The thread-local values are read-only. To modify them, the PropagatedContext instance needs to be changed and put into the Reactor’s context.

To add the context in the middle of the reactive chain, do something like the following:

import io.micronaut.core.async.propagation.ReactorPropagation;
import io.micronaut.core.propagation.PropagatedContext;
import io.micronaut.core.propagation.PropagatedContextElement;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.QueryValue;
import reactor.core.publisher.Mono;

@Controller
class HelloController {

    @Get("/hello")
    Mono<String> hello(@QueryValue("name") String name) {
        PropagatedContext propagatedContext = PropagatedContext.get().plus(new MyContextElement(name)); // (1)
        return Mono.just("Hello, " + name)
            .contextWrite(ctx -> ReactorPropagation.addPropagatedContext(ctx, propagatedContext)); // (2)
    }
}

record MyContextElement(String value) implements PropagatedContextElement { }
import io.micronaut.core.async.propagation.ReactorPropagation
import io.micronaut.core.propagation.PropagatedContext
import io.micronaut.core.propagation.PropagatedContextElement
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.QueryValue
import reactor.core.publisher.Mono

@Controller
class HelloController {

    @Get('/hello')
    Mono<String> hello(@QueryValue('name') String name) {
        PropagatedContext propagatedContext = PropagatedContext.get() + new MyContextElement(name) // (1)
        return Mono.just("Hello, $name")
                .contextWrite(ctx -> ReactorPropagation.addPropagatedContext(ctx, propagatedContext)) // (2)
    }
}

record MyContextElement(String value) implements PropagatedContextElement { }
1 Obtain the current context that has been modified, e.g. with PropagatedContext.get().plus(…​), etc.
2 Add the context into the reactive chain.

If you have Micrometer Context Propagation on the classpath but don’t want to use it, apply the following configuration:

Disable Micrometer Context Propagation in Reactor
reactor.enable-automatic-context-propagation=false
reactor:
    enable-automatic-context-propagation: false
[reactor]
  enable-automatic-context-propagation=false
reactor {
  enableAutomaticContextPropagation = false
}
{
  reactor {
    enable-automatic-context-propagation = false
  }
}
{
  "reactor": {
    "enable-automatic-context-propagation": false
  }
}

8.2 HTTP filters context propagation

Modifying the propagated context is a common scenario. Usually, you want to extend the context to include the request-related values.

To use a non-reactive HTTP filter API, you need to add a method parameter MutablePropagatedContext and modify the propagated context elements by adding or removing the existing ones:

Example of adding a new MDC propagated context element
@ServerFilter(MATCH_ALL_PATTERN)
public class MdcFilter {

    @RequestFilter
    public void myRequestFilter(HttpRequest<?> request, MutablePropagatedContext mutablePropagatedContext) {
        try {
            String trackingId = request.getHeaders().get("X-TrackingId");
            MDC.put("trackingId", trackingId);
            mutablePropagatedContext.add(new MdcPropagationContext());
        } finally {
            MDC.remove("trackingId");
        }
    }

}

The next filter in the chain will have the new propagated context available. Any of the thread-local context elements will be set for the next filter or the controller method invocation.

To use the legacy reactive HTTP filters, simply modify and propagate the context bound to the following chain invocation:

Example of adding a new MDC propagated context element for the reactive filter:
@Filter(MATCH_ALL_PATTERN)
public class MdcLegacyFilter implements HttpServerFilter {

    @Override
    public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
                                                      ServerFilterChain chain) {
        try {
            String trackingId = request.getHeaders().get("X-TrackingId");
            MDC.put("trackingId", trackingId);
            try (PropagatedContext.Scope ignore = PropagatedContext.get().plus(new MdcPropagationContext()).propagate()) {
                return chain.proceed(request);
            }
        } finally {
            MDC.remove("trackingId");
        }
    }

}

9 Cloud Native Features

The majority of JVM frameworks in use today were designed before the rise of cloud deployments and microservice architectures. Applications built with these frameworks were intended to be deployed to traditional Java containers. As a result, cloud support in these frameworks typically comes as an add-on rather than as core design features.

Micronaut framework was designed from the ground up for building microservices for the cloud. As a result, many key features that typically require external libraries or services are available within your application itself. To override one of the industry’s current favorite buzzwords, Micronaut applications are "natively cloud-native".

The following are some cloud-specific features that are integrated directly into the Micronaut runtime:

  • Distributed Configuration

  • Service Discovery

  • Client-Side Load-Balancing

  • Distributed Tracing

  • Serverless Functions

Many features in the Micronaut framework are heavily inspired by features from Spring and Grails. This is by design and helps developers who are already familiar with systems such as Spring Cloud.

The following sections cover these features and how to use them.

9.1 Cloud Configuration

Applications built for the Cloud often need to adapt to running in a Cloud environment, read and share configuration in a distributed manner, and externalize configuration to the environment where necessary.

Micronaut’s Environment concept can be configured to be Cloud platform-aware and makes the best effort to detect the underlying active Cloud environment.

To enable this feature you can:

  1. call deduceCloudEnvironment(true) on the ApplicationContextBuilder interface when starting Micronaut. For example:

    Enabling Cloud Environment Detection
    public static void main(String...args) {
        Micronaut.build(args)
                 .deduceCloudEnvironment(true)
                 .start();
    }
  2. Set the micronaut.env.cloud-deduction property to true in your configuration.

  3. Provide an environment variable MICRONAUT_ENV_CLOUD_DEDUCTION set to true.

You can then use the Requires annotation to conditionally load bean definitions.

The following table summarizes the constants in the Environment interface and provides an example:

Table 1. Micronaut Environment Detection
Constant Description Requires Example Environment name

ANDROID

The application is running as an Android application

@Requires(env = Environment.ANDROID)

android

TEST

The application is running within a JUnit or Spock test

@Requires(env = Environment.TEST)

test

CLOUD

The application is running in a Cloud environment (present for all other cloud platform types)

@Requires(env = Environment.CLOUD)

cloud

AMAZON_EC2

Running on Amazon EC2

@Requires(env = Environment.AMAZON_EC2)

ec2

GOOGLE_COMPUTE

Running on Google Compute

@Requires(env = Environment.GOOGLE_COMPUTE)

gcp

KUBERNETES

Running on Kubernetes

@Requires(env = Environment.KUBERNETES)

k8s

HEROKU

Running on Heroku

@Requires(env = Environment.HEROKU)

heroku

CLOUD_FOUNDRY

Running on Cloud Foundry

@Requires(env = Environment.CLOUD_FOUNDRY)

pcf

AZURE

Running on Microsoft Azure

@Requires(env = Environment.AZURE)

azure

IBM

Running on IBM Cloud

@Requires(env = Environment.IBM)

ibm

DIGITAL_OCEAN

Running on Digital Ocean

@Requires(env = Environment.DIGITAL_OCEAN)

digitalocean

ORACLE_CLOUD

Running on Oracle Cloud

@Requires(env = Environment.ORACLE_CLOUD)

oraclecloud

Note that you can have multiple environments active, for example when running in Kubernetes on AWS.

In addition, using the value of the constants defined in the table above you can create environment-specific configuration files. For example if you create a src/main/resources/application-gcp.yml file, it is only loaded when running on Google Compute.

Any configuration property in the Environment can also be set via an environment variable. For example, setting the CONSUL_CLIENT_HOST environment variable overrides the host property in ConsulConfiguration.

Using Cloud Instance Metadata

When the Micronaut framework detects it is running on a supported cloud platform, on startup it populates the interface ComputeInstanceMetadata.

As of Micronaut framework 2.1.x this logic depends on the presence of the appropriate core Cloud module for Oracle Cloud, AWS, or GCP.

All this data is merged together into the metadata property for the running ServiceInstance.

To access the metadata for your application instance you can use the interface EmbeddedServerInstance, and call getMetadata() which returns a Map of the metadata.

If you connect remotely via a client, the instance metadata can be referenced once you have retrieved a ServiceInstance from either the LoadBalancer or DiscoveryClient APIs.

The Netflix Ribbon client-side load balancer can be configured to use the metadata to do zone-aware client-side load balancing. See Client-Side Load Balancing

To obtain metadata for a service via Service Discovery use the LoadBalancerResolver interface to resolve a LoadBalancer and obtain a reference to a service by identifier:

Obtaining Metadata for a Service instance
LoadBalancer loadBalancer = loadBalancerResolver.resolve("some-service");
Flux.from(
    loadBalancer.select()
).subscribe((instance) ->
    ConvertibleValues<String> metaData = instance.getMetadata();
    ...
);

The EmbeddedServerInstance is available through event listeners that listen for the ServiceReadyEvent. The @EventListener annotation makes it easy to listen for the event in your beans.

To obtain metadata for the locally running server, use an EventListener for the ServiceReadyEvent:

Obtaining Metadata for a Local Server
@EventListener
void onServiceStarted(ServiceReadyEvent event) {
    ServiceInstance serviceInstance = event.getSource();
    ConvertibleValues<String> metadata = serviceInstance.getMetadata();
}

9.1.1 Distributed Configuration

As you can see, the Micronaut framework features a robust system for externalizing and adapting configuration to the environment inspired by similar approaches in Grails and Spring Boot.

However, what if you want multiple microservices to share configuration? the Micronaut framework includes APIs for distributed configuration.

The ConfigurationClient interface has a getPropertySources method that can be implemented to read and resolve configuration from distributed sources.

The getPropertySources returns a Publisher that emits zero or many PropertySource instances.

The default implementation is DefaultCompositeConfigurationClient which merges all registered ConfigurationClient beans into a single bean.

You can either implement your own ConfigurationClient or use the implementations provided by Micronaut. The following sections cover those.

9.1.2 HashiCorp Consul Support

Consul is a popular Service Discovery and Distributed Configuration server provided by HashiCorp. The Micronaut framework features a native ConsulClient that uses Micronaut’s support for Declarative HTTP Clients.

Starting Consul

The quickest way to start using Consul is via Docker:

  1. Starting Consul with Docker

docker run -p 8500:8500 consul

Enabling Distributed Configuration with Consul

Using the CLI

If you create your project using the Micronaut CLI, supply the config-consul feature to enable Consul’s distributed configuration in your project:

$ mn create-app my-app --features config-consul

To enable distributed configuration make sure [bootstrap] is enabled and create a src/main/resources/bootstrap.[yml/toml/properties] file with the following configuration:

micronaut.application.name=hello-world
micronaut.config-client.enabled=true
consul.client.defaultZone=${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}
micronaut:
  application:
    name: hello-world
  config-client:
    enabled: true
consul:
  client:
    defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
[micronaut]
  [micronaut.application]
    name="hello-world"
  [micronaut.config-client]
    enabled=true
[consul]
  [consul.client]
    defaultZone="${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
micronaut {
  application {
    name = "hello-world"
  }
  configClient {
    enabled = true
  }
}
consul {
  client {
    defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
  }
}
{
  micronaut {
    application {
      name = "hello-world"
    }
    config-client {
      enabled = true
    }
  }
  consul {
    client {
      defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "hello-world"
    },
    "config-client": {
      "enabled": true
    }
  },
  "consul": {
    "client": {
      "defaultZone": "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    }
  }
}

After enabling distributed configuration, store the configuration to share in Consul’s key/value store. There are a number of ways to do that.

Storing Configuration as Key/Value Pairs

One way is to store the keys and values directly in Consul. In this case by default the Micronaut framework looks for configuration in the Consul /config directory.

You can alter the path searched for by setting consul.client.config.path

Within the /config directory Micronaut searches values within the following directories in order of precedence:

Table 1. Configuration Resolution Precedence
Directory Description

/config/application

Configuration shared by all applications

/config/application,prod

Configuration shared by all applications for the prod Environment

/config/[APPLICATION_NAME]

Application-specific configuration, example /config/hello-world

/config/[APPLICATION_NAME],prod

Application-specific configuration for an active Environment

The value of APPLICATION_NAME is whatever your have configured micronaut.application.name to be in your bootstrap configuration file.

To see this in action, use the following cURL command to store a property called foo.bar with a value of myvalue in the directory /config/application.

Using cURL to Write a Value
curl -X PUT -d @- localhost:8500/v1/kv/config/application/foo.bar <<< myvalue

If you now define a @Value("${foo.bar}") or call environment.getProperty(..) the value myvalue will be resolved from Consul.

Storing Configuration in YAML, JSON etc.

Some Consul users prefer storing configuration in blobs of a certain format, such as YAML. The Micronaut framework supports this mode and supports storing configuration in either YAML, JSON, or Java properties format.

The ConfigDiscoveryConfiguration has a number of configuration options for configuring how distributed configuration is discovered.

You can set the consul.client.config.format option to configure the format with which properties are read.

For example, to configure JSON:

consul.client.config.format=JSON
consul:
  client:
    config:
      format: JSON
[consul]
  [consul.client]
    [consul.client.config]
      format="JSON"
consul {
  client {
    config {
      format = "JSON"
    }
  }
}
{
  consul {
    client {
      config {
        format = "JSON"
      }
    }
  }
}
{
  "consul": {
    "client": {
      "config": {
        "format": "JSON"
      }
    }
  }
}

Now write your configuration in JSON format to Consul:

Using cURL to write JSON
curl -X PUT  localhost:8500/v1/kv/config/application \
-d @- << EOF
{ "foo": {  "bar": "myvalue" } }
EOF

Storing Configuration as File References

Another popular option is git2consul which mirrors the contents of a Git repository to Consul’s key/value store.

You can set up a Git repository that contains files like application.yml, hello-world-test.json, etc., and the contents of these files will be cloned to Consul.

In this case, each key in Consul represents a file with an extension, for example /config/application.yml, and you must configure the FILE format:

consul.client.config.format=FILE
consul:
  client:
    config:
      format: FILE
[consul]
  [consul.client]
    [consul.client.config]
      format="FILE"
consul {
  client {
    config {
      format = "FILE"
    }
  }
}
{
  consul {
    client {
      config {
        format = "FILE"
      }
    }
  }
}
{
  "consul": {
    "client": {
      "config": {
        "format": "FILE"
      }
    }
  }
}

9.1.3 HashiCorp Vault Support

The Micronaut framework integrates with HashiCorp Vault as a distributed configuration source.

To enable distributed configuration make sure [bootstrap] is enabled and create a src/main/resources/bootstrap.[yml/toml/properties] file with the following configuration:

Integrating with HashiCorp Vault
micronaut.application.name=hello-world
micronaut.config-client.enabled=true
vault.client.config.enabled=true
micronaut:
  application:
    name: hello-world
  config-client:
    enabled: true

vault:
  client:
    config:
      enabled: true
[micronaut]
  [micronaut.application]
    name="hello-world"
  [micronaut.config-client]
    enabled=true
[vault]
  [vault.client]
    [vault.client.config]
      enabled=true
micronaut {
  application {
    name = "hello-world"
  }
  configClient {
    enabled = true
  }
}
vault {
  client {
    config {
      enabled = true
    }
  }
}
{
  micronaut {
    application {
      name = "hello-world"
    }
    config-client {
      enabled = true
    }
  }
  vault {
    client {
      config {
        enabled = true
      }
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "hello-world"
    },
    "config-client": {
      "enabled": true
    }
  },
  "vault": {
    "client": {
      "config": {
        "enabled": true
      }
    }
  }
}

See the configuration reference for all configuration options.

The Micronaut framework uses the configured micronaut.application.name to lookup property sources for the application from Vault.

Table 1. Configuration Resolution Precedence
Directory Description

/application

Configuration shared by all applications

/[APPLICATION_NAME]

Application-specific configuration

/application/[ENV_NAME]

Configuration shared by all applications for an active environment name

/[APPLICATION_NAME]/[ENV_NAME]

Application-specific configuration for an active environment name

See the Documentation for HashiCorp Vault for more information on how to set up the server.

9.1.4 Spring Cloud Config Support

Since 1.1, the Micronaut framework features a native Spring Cloud Configuration for those who have not switched to a dedicated more complete solution like Consul.

To enable distributed configuration make sure [bootstrap] is enabled and create a src/main/resources/bootstrap.[yml/toml/properties] file with the following configuration:

Integrating with Spring Cloud Configuration
micronaut.application.name=hello-world
micronaut.config-client.enabled=true
spring.cloud.config.enabled=true
spring.cloud.config.uri=http://localhost:8888/
spring.cloud.config.retry-attempts=4
spring.cloud.config.retry-delay=2s
micronaut:
  application:
    name: hello-world
  config-client:
    enabled: true
spring:
  cloud:
    config:
      enabled: true
      uri: http://localhost:8888/
      retry-attempts: 4
      retry-delay: 2s
[micronaut]
  [micronaut.application]
    name="hello-world"
  [micronaut.config-client]
    enabled=true
[spring]
  [spring.cloud]
    [spring.cloud.config]
      enabled=true
      uri="http://localhost:8888/"
      retry-attempts=4
      retry-delay="2s"
micronaut {
  application {
    name = "hello-world"
  }
  configClient {
    enabled = true
  }
}
spring {
  cloud {
    config {
      enabled = true
      uri = "http://localhost:8888/"
      retryAttempts = 4
      retryDelay = "2s"
    }
  }
}
{
  micronaut {
    application {
      name = "hello-world"
    }
    config-client {
      enabled = true
    }
  }
  spring {
    cloud {
      config {
        enabled = true
        uri = "http://localhost:8888/"
        retry-attempts = 4
        retry-delay = "2s"
      }
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "hello-world"
    },
    "config-client": {
      "enabled": true
    }
  },
  "spring": {
    "cloud": {
      "config": {
        "enabled": true,
        "uri": "http://localhost:8888/",
        "retry-attempts": 4,
        "retry-delay": "2s"
      }
    }
  }
}
  • retry-attempts is optional, and specifies the number of times to retry

  • retry-delay is optional, and specifies the delay between retries

The Micronaut framework uses the configured micronaut.application.name to look up property sources for the application from Spring Cloud config server configured via spring.cloud.config.uri.

See the Documentation for Spring Cloud Config Server for more information on how to set up the server.

9.1.5 AWS Parameter Store Support

The Micronaut framework supports configuration sharing via AWS System Manager Parameter Store. You need the following dependencies configured:

implementation("io.micronaut.aws:micronaut-aws-parameter-store")
<dependency>
    <groupId>io.micronaut.aws</groupId>
    <artifactId>micronaut-aws-parameter-store</artifactId>
</dependency>

To enable distributed configuration, make sure [bootstrap] is enabled and create a src/main/resources/bootstrap.yml file with the following configuration:

micronaut.application.name=hello-world
micronaut.config-client.enabled=true
aws.client.system-manager.parameterstore.enabled=true
micronaut:
  application:
    name: hello-world
  config-client:
    enabled: true
aws:
  client:
    system-manager:
      parameterstore:
        enabled: true
[micronaut]
  [micronaut.application]
    name="hello-world"
  [micronaut.config-client]
    enabled=true
[aws]
  [aws.client]
    [aws.client.system-manager]
      [aws.client.system-manager.parameterstore]
        enabled=true
micronaut {
  application {
    name = "hello-world"
  }
  configClient {
    enabled = true
  }
}
aws {
  client {
    systemManager {
      parameterstore {
        enabled = true
      }
    }
  }
}
{
  micronaut {
    application {
      name = "hello-world"
    }
    config-client {
      enabled = true
    }
  }
  aws {
    client {
      system-manager {
        parameterstore {
          enabled = true
        }
      }
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "hello-world"
    },
    "config-client": {
      "enabled": true
    }
  },
  "aws": {
    "client": {
      "system-manager": {
        "parameterstore": {
          "enabled": true
        }
      }
    }
  }
}

See the configuration reference for all configuration options.

You can configure shared properties from the AWS Console → System Manager → Parameter Store.

The Micronaut framework uses a hierarchy to read configuration values, and supports String, StringList, and SecureString types.

You can create environment-specific configurations as well by including the environment name after an underscore _. For example if micronaut.application.name is set to helloworld, specifying configuration values under helloworld_test will be applied only to the test environment.

Table 1. Configuration Resolution Precedence
Directory Description

/config/application

Configuration shared by all applications

/config/[APPLICATION_NAME]

Application-specific configuration, example /config/hello-world

/config/application_prod

Configuration shared by all applications for the prod Environment

/config/[APPLICATION_NAME]_prod

Application-specific configuration for an active Environment

For example, if the configuration name /config/application_test/server.url is configured in AWS Parameter Store, any application connecting to that parameter store can retrieve the value using server.url. If the application has micronaut.application.name configured to be myapp, a value with the name /config/myapp_test/server.url overrides the value just for that application.

Each level of the tree can be composed of key=value pairs. For multiple key/value pairs, set the type to StringList.

For special secure information, such as keys or passwords, use the type SecureString. KMS will be automatically invoked when you add and retrieve values, and will decrypt them with the default key store for your account. If you set the configuration to not use secure strings, they will be returned to you encrypted, and you must manually decrypt them.

9.1.6 Oracle Cloud Vault Support

9.1.7 Google Cloud Pub/Sub Support

See the Micronaut GCP Pub/Sub documentation.

9.1.8 Kubernetes Support

See the Kubernetes Configuration Client documentation.

9.2 Service Discovery

Service Discovery enables Microservices to find each other without knowing the physical location or IP address of associated services.

The Micronaut framework integrates with multiple tools and libraries. See Micronaut Service Discovery documentation for more details.

9.2.1 Consul Support

9.2.2 Eureka Support

9.2.3 Kubernetes Support

Kubernetes is a container runtime with many features including integrated Service Discovery and Distributed Configuration.

The Micronaut framework includes first-class integration with Kubernetes. See the Micronaut Kubernetes documentation for more details.

9.2.4 AWS Route 53 Support

To use Route 53 Service Discovery, you must meet the following criteria:

  • Run EC2 instances of some type

  • Have a domain name hosted in Route 53

  • Have a newer version of AWS-CLI (such as 14+)

Assuming you have those things, you are ready. It is not as fancy as Consul or Eureka, but other than some initial setup with the AWS-CLI, there is no other software running to go wrong. You can even support health checks if you add a custom health check to your service. To test if your account can create and use Service Discovery, see the Integration Test section. More information is available at https://docs.aws.amazon.com/Route53/latest/APIReference/overview-service-discovery.html.

Here are the steps:

  1. Use AWS-CLI to create a namespace. You can make either a public or private one depending on the IPs or subnets you use

  2. Create a service with DNS Records with AWS-CLI command

  3. Add health checks or custom health checks (optional)

  4. Add Service ID to your application configuration file like so:

Sample application configuration
aws.route53.registration.enabled=true
aws.route53.registration.aws-service-id=srv-978fs98fsdf
aws.route53.registration.namespace=micronaut.io
micronaut.application.name=something
aws:
  route53:
    registration:
        enabled: true
        aws-service-id: srv-978fs98fsdf
        namespace: micronaut.io
micronaut:
  application:
    name: something
[aws]
  [aws.route53]
    [aws.route53.registration]
      enabled=true
      aws-service-id="srv-978fs98fsdf"
      namespace="micronaut.io"
[micronaut]
  [micronaut.application]
    name="something"
aws {
  route53 {
    registration {
      enabled = true
      awsServiceId = "srv-978fs98fsdf"
      namespace = "micronaut.io"
    }
  }
}
micronaut {
  application {
    name = "something"
  }
}
{
  aws {
    route53 {
      registration {
        enabled = true
        aws-service-id = "srv-978fs98fsdf"
        namespace = "micronaut.io"
      }
    }
  }
  micronaut {
    application {
      name = "something"
    }
  }
}
{
  "aws": {
    "route53": {
      "registration": {
        "enabled": true,
        "aws-service-id": "srv-978fs98fsdf",
        "namespace": "micronaut.io"
      }
    }
  },
  "micronaut": {
    "application": {
      "name": "something"
    }
  }
}
  1. Make sure you have the following dependencies in your build file:

implementation("io.micronaut.aws:micronaut-aws-route53")
<dependency>
    <groupId>io.micronaut.aws</groupId>
    <artifactId>micronaut-aws-route53</artifactId>
</dependency>

  1. On the client side, you need the same dependencies and fewer configuration options:

aws.route53.discovery.client.enabled=true
aws.route53.discovery.client.aws-service-id=srv-978fs98fsdf
aws.route53.discovery.client.namespace-id=micronaut.io
aws:
  route53:
    discovery:
      client:
        enabled: true
        aws-service-id: srv-978fs98fsdf
        namespace-id: micronaut.io
[aws]
  [aws.route53]
    [aws.route53.discovery]
      [aws.route53.discovery.client]
        enabled=true
        aws-service-id="srv-978fs98fsdf"
        namespace-id="micronaut.io"
aws {
  route53 {
    discovery {
      client {
        enabled = true
        awsServiceId = "srv-978fs98fsdf"
        namespaceId = "micronaut.io"
      }
    }
  }
}
{
  aws {
    route53 {
      discovery {
        client {
          enabled = true
          aws-service-id = "srv-978fs98fsdf"
          namespace-id = "micronaut.io"
        }
      }
    }
  }
}
{
  "aws": {
    "route53": {
      "discovery": {
        "client": {
          "enabled": true,
          "aws-service-id": "srv-978fs98fsdf",
          "namespace-id": "micronaut.io"
        }
      }
    }
  }
}

You can then use the DiscoveryClient API to find other services registered via Route 53. For example:

Sample code for client
DiscoveryClient discoveryClient = embeddedServer.getApplicationContext().getBean(DiscoveryClient.class);
List<String> serviceIds = Flux.from(discoveryClient.getServiceIds()).blockFirst();
List<ServiceInstance> instances = Flux.from(discoveryClient.getInstances(serviceIds.get(0))).blockFirst();

Creating the Namespace

Namespaces are similar to a regular Route53 hosted zone, and they appear in the Route53 console, but the console doesn’t support modifying them. You must use the AWS-CLI at this time for any Service Discovery functionality.

First decide if you are creating a public-facing namespace or a private one, as the commands are different:

Creating Namespace
$ aws servicediscovery create-public-dns-namespace --name micronaut.io --create-request-id create-1522767790 --description adescriptionhere

or

$ aws servicediscovery create-private-dns-namespace --name micronaut.internal.io --create-request-id create-1522767790 --description adescriptionhere --vpc yourvpcID

When you run this you will get an operation ID. You can check the status with the get-operation CLI command:

Get Operation Results
$ aws servicediscovery get-operation --operation-id asdffasdfsda

You can use this command to get the status of any call you make that returns an operation ID.

The result of the command will tell you the ID of the namespace. Write that down, you’ll need it for the next steps. If you get an error it will say what the error was.

Creating the Service and DNS Records

The next step is creating the Service and DNS records.

Create Service
$ aws create-service --name yourservicename --create-request-id somenumber --description someservicedescription --dns-config NamespaceId=yournamespaceid,RoutingPolicy=WEIGHTED,DnsRecords=[{Type=A,TTL=1000},{Type=A,TTL=1000}]

The DnsRecord type can be A(ipv4),AAAA(ipv6),SRV, or CNAME. RoutingPolicy can be WEIGHTED or MULTIVALUE. Keep in mind CNAME must use weighted routing type, SRV must have a valid port configured.

To add a health check, use the following syntax on the CLI:

Specifying a Health Check
Type=string,ResourcePath=string,FailureThreshold=integer

Type can be 'HTTP','HTTPS', or 'TCP'. You can only use a standard health check on a public namespace. See Custom Health Checks for private namespaces. Resource path should be a URL that returns 200 OK if it is healthy.

For a custom health check, you only need to specify --health-check-custom-config FailureThreshold=integer which works on private namespaces as well.

This is also good because the Micronaut framework sends out pulsation commands to let AWS know the instance is still healthy.

For more help run 'aws discoveryservice create-service help'.

You will get a service ID and an ARN back from this command if successful. Write that down, it is going to go into the Micronaut configuration.

Setting up the configuration in Micronaut

Auto Naming Registration

Add the configuration to make your applications register with Route 53 Auto-discovery:

Registration Properties
aws.route53.registration.enabled=true
aws.route53.registration.aws-service-id=<enter the service id you got after creation on aws cli>
aws.route53.discovery.namespace-id=<enter the namespace id you got after creating the namespace>
aws:
  route53:
    registration:
      enabled: true
      aws-service-id: <enter the service id you got after creation on aws cli>
    discovery:
      namespace-id: <enter the namespace id you got after creating the namespace>
[aws]
  [aws.route53]
    [aws.route53.registration]
      enabled=true
      aws-service-id="<enter the service id you got after creation on aws cli>"
    [aws.route53.discovery]
      namespace-id="<enter the namespace id you got after creating the namespace>"
aws {
  route53 {
    registration {
      enabled = true
      awsServiceId = "<enter the service id you got after creation on aws cli>"
    }
    discovery {
      namespaceId = "<enter the namespace id you got after creating the namespace>"
    }
  }
}
{
  aws {
    route53 {
      registration {
        enabled = true
        aws-service-id = "<enter the service id you got after creation on aws cli>"
      }
      discovery {
        namespace-id = "<enter the namespace id you got after creating the namespace>"
      }
    }
  }
}
{
  "aws": {
    "route53": {
      "registration": {
        "enabled": true,
        "aws-service-id": "<enter the service id you got after creation on aws cli>"
      },
      "discovery": {
        "namespace-id": "<enter the namespace id you got after creating the namespace>"
      }
    }
  }
}

Discovery Client Configuration

Discovery Properties
aws.route53.discovery.client.enabled=true
aws.route53.discovery.client.aws-service-id=<enter the service id you got after creation on aws cli>
aws:
  route53:
    discovery:
      client:
        enabled: true
        aws-service-id: <enter the service id you got after creation on aws cli>
[aws]
  [aws.route53]
    [aws.route53.discovery]
      [aws.route53.discovery.client]
        enabled=true
        aws-service-id="<enter the service id you got after creation on aws cli>"
aws {
  route53 {
    discovery {
      client {
        enabled = true
        awsServiceId = "<enter the service id you got after creation on aws cli>"
      }
    }
  }
}
{
  aws {
    route53 {
      discovery {
        client {
          enabled = true
          aws-service-id = "<enter the service id you got after creation on aws cli>"
        }
      }
    }
  }
}
{
  "aws": {
    "route53": {
      "discovery": {
        "client": {
          "enabled": true,
          "aws-service-id": "<enter the service id you got after creation on aws cli>"
        }
      }
    }
  }
}

You can also call the following methods by getting the bean "Route53AutoNamingClient":

Discovery Methods
// if serviceId is null it will use property "aws.route53.discovery.client.awsServiceId"
Publisher<List<ServiceInstance>> getInstances(String serviceId)
// reads property "aws.route53.discovery.namespaceId"
Publisher<List<String>> getServiceIds()

Integration Tests

If you set the environment variable AWS_SUBNET_ID and have credentials configured in your home directory that are valid (in ~/.aws/credentials) you can run the integration tests. You need a domain hosted on Route53 as well. This test will create a t2.nano instance, a namespace, service, and register that instance to service discovery. When the test completes it will remove/terminate all resources it spun up.

9.2.5 Manual Service Discovery Configuration

If you do not wish to involve a service discovery server like Consul or you interact with a third-party service that cannot register with Consul you can instead manually configure services that are available via Service discovery.

To do this, use the micronaut.http.services setting. For example:

Manually configuring services
micronaut.http.services.foo.urls[0]=http://foo1
micronaut.http.services.foo.urls[1]=http://foo2
micronaut:
  http:
    services:
      foo:
        urls:
          - http://foo1
          - http://foo2
[micronaut]
  [micronaut.http]
    [micronaut.http.services]
      [micronaut.http.services.foo]
        urls=[
          "http://foo1",
          "http://foo2"
        ]
micronaut {
  http {
    services {
      foo {
        urls = ["http://foo1", "http://foo2"]
      }
    }
  }
}
{
  micronaut {
    http {
      services {
        foo {
          urls = ["http://foo1", "http://foo2"]
        }
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "services": {
        "foo": {
          "urls": ["http://foo1", "http://foo2"]
        }
      }
    }
  }
}

You can then inject a client with @Client("foo"), and it will use the above configuration to load balance between the two configured servers.

When using @Client with service discovery, the service id must be specified in the annotation in kebab-case. The configuration in the example above however can be in camel case.
You can override this configuration in production by specifying an environment variable such as MICRONAUT_HTTP_SERVICES_FOO_URLS=http://prod1,http://prod2

Note that by default no health checking will happen to assert that the referenced services are operational. You can alter that by enabling health checking and optionally specifying a health check path (the default is /health):

Enabling Health Checking
micronaut.http.services.foo.health-check=true
micronaut.http.services.foo.health-check-interval=15s
micronaut.http.services.foo.health-check-uri=/health
micronaut:
  http:
    services:
      foo:
        health-check: true
        health-check-interval: 15s
        health-check-uri: /health
[micronaut]
  [micronaut.http]
    [micronaut.http.services]
      [micronaut.http.services.foo]
        health-check=true
        health-check-interval="15s"
        health-check-uri="/health"
micronaut {
  http {
    services {
      foo {
        healthCheck = true
        healthCheckInterval = "15s"
        healthCheckUri = "/health"
      }
    }
  }
}
{
  micronaut {
    http {
      services {
        foo {
          health-check = true
          health-check-interval = "15s"
          health-check-uri = "/health"
        }
      }
    }
  }
}
{
  "micronaut": {
    "http": {
      "services": {
        "foo": {
          "health-check": true,
          "health-check-interval": "15s",
          "health-check-uri": "/health"
        }
      }
    }
  }
}
  • health-check indicates whether to health check the service

  • health-check-interval is the interval between checks

  • health-check-uri specifies the endpoint URI of the health check request

The Micronaut framework starts a background thread to check the health status of the service and if any of the configured services respond with an error code, they are removed from the list of available services.

9.3 Client Side Load Balancing

When discovering services from Consul, Eureka, or other Service Discovery servers, the DiscoveryClient emits a list of available ServiceInstance.

The Micronaut framework by default automatically performs Round Robin client-side load balancing using the servers in this list. This combined with Retry Advice adds extra resiliency to your Microservice infrastructure.

The load balancing is handled by the LoadBalancer interface, which has a LoadBalancer.select() method that returns a Publisher which emits a ServiceInstance.

The Publisher is returned because the process for selecting a ServiceInstance may result in a network operation depending on the Service Discovery strategy employed.

The default implementation of the LoadBalancer interface is DiscoveryClientRoundRobinLoadBalancer. You can replace this strategy with another implementation to customize how client side load balancing is handled in Micronaut, since there are many different ways to optimize load balancing.

For example, you may wish to load balance between services in a particular zone, or to load balance between servers that have the best overall response time.

To replace the LoadBalancer, define a bean that replaces the DiscoveryClientLoadBalancerFactory.

In fact that is exactly what the Netflix Ribbon support does, described in the next section.

9.3.1 Netflix Ribbon Support

Using the CLI

If you create your project using the Micronaut CLI, supply the netflix-ribbon feature to configure Netflix Ribbon in your project:

$ mn create-app my-app --features netflix-ribbon

Netflix Ribbon is an inter-process communication library used at Netflix with support for customizable load balancing strategies.

If you need more flexibility in how your application performs client-side load balancing, you can use Micronaut’s Netflix Ribbon support.

To add Ribbon support to your application, add the netflix-ribbon configuration to your build:

implementation("io.micronaut.netflix:micronaut-netflix-ribbon")
<dependency>
    <groupId>io.micronaut.netflix</groupId>
    <artifactId>micronaut-netflix-ribbon</artifactId>
</dependency>

The LoadBalancer implementations will now be RibbonLoadBalancer instances.

Ribbon’s Configuration options can be set using the ribbon namespace in configuration. For example in your configuration file (e.g application.yml):

Configuring Ribbon
ribbon.VipAddress=test
ribbon.ServerListRefreshInterval=2000
ribbon:
  VipAddress: test
  ServerListRefreshInterval: 2000
[ribbon]
  VipAddress="test"
  ServerListRefreshInterval=2000
ribbon {
  VipAddress = "test"
  ServerListRefreshInterval = 2000
}
{
  ribbon {
    VipAddress = "test"
    ServerListRefreshInterval = 2000
  }
}
{
  "ribbon": {
    "VipAddress": "test",
    "ServerListRefreshInterval": 2000
  }
}

Each discovered client can also be configured under ribbon.clients. For example given a @Client(id = "hello-world") you can configure Ribbon settings with:

Per Client Ribbon Settings
ribbon.clients.hello-world.VipAddress=test
ribbon.clients.hello-world.ServerListRefreshInterval=2000
ribbon:
  clients:
    hello-world:
      VipAddress: test
      ServerListRefreshInterval: 2000
[ribbon]
  [ribbon.clients]
    [ribbon.clients.hello-world]
      VipAddress="test"
      ServerListRefreshInterval=2000
ribbon {
  clients {
    helloWorld {
      VipAddress = "test"
      ServerListRefreshInterval = 2000
    }
  }
}
{
  ribbon {
    clients {
      hello-world {
        VipAddress = "test"
        ServerListRefreshInterval = 2000
      }
    }
  }
}
{
  "ribbon": {
    "clients": {
      "hello-world": {
        "VipAddress": "test",
        "ServerListRefreshInterval": 2000
      }
    }
  }
}

By default, the Micronaut framework registers a DiscoveryClientServerList for each client that integrates Ribbon with the Micronaut framework’s DiscoveryClient.

9.4 Distributed Tracing

See the documentation for Micronaut Tracing for more information adding distributed tracing to your applications.

10 Serverless Functions

Serverless architectures, where you deploy functions that are fully managed by a Cloud environment and are executed in ephemeral processes, require a unique approach.

Traditional frameworks like Grails and Spring are not really suitable since low memory consumption and fast startup time are critical, since the Function as a Service (FaaS) server typically spins up your function for a period using a cold start and then keeps it warm.

Micronaut’s compile-time approach, fast startup time, and low memory footprint make it a great candidate for developing functions, and the Micronaut framework includes dedicated support for developing and deploying functions to AWS Lambda, Google Cloud Function, Azure Function, and any FaaS system that supports running functions as containers (such as OpenFaaS, Rift or Fn).

There are generally two approaches to writing functions with Micronaut:

  1. Low-level functions written using the native API of the function platform

  2. Higher-level functions where you simply define controllers as you normally do in a typical Micronaut application and deploy to the function platform.

The first has marginally less startup time overhead and is typically used for non-HTTP functions such as functions that listen to an event or background functions.

The second is only for HTTP functions and is useful for users who want to take a slice of an existing application and deploy it as a serverless function. If cold start performance is a concern it is recommended that you consider building a native image with GraalVM for this option.

10.1 AWS Lambda

Support for AWS Lambda is implemented in the Micronaut AWS subproject.

Simple Functions with AWS Lambda

You can implement AWS Request Handlers with the Micronaut framework that directly implement the AWS Lambda SDK API. See the documentation on Micronaut Request Handlers for more information.

Using the CLI

To create an AWS Lambda Function:

$ mn create-function-app my-app --features aws-lambda

Or with Micronaut Launch

$ curl https://launch.micronaut.io/create/function/example\?features\=aws-lambda -o example.zip
$ unzip example.zip -d example

HTTP Functions with AWS Lambda

You can deploy regular Micronaut applications that use @Controller, etc. using Micronaut’s support for AWS API Gateway. See the documentation on AWS Application Types, Lambda Runtimes, Dependencies for more information.

Using the CLI

To create an AWS API Gateway Proxy application:

$ mn create-app my-app --features aws-lambda

Or with Micronaut Launch

$ curl https://launch.micronaut.io/example.zip\?features\=aws-lambda -o example.zip
$ unzip example.zip -d example

10.2 Google Cloud Function

Support for Google Cloud Function is implemented in the Micronaut GCP subproject.

Simple Functions with Cloud Function

You can implement Cloud Functions with the Micronaut framework that directly implement the Cloud Function Framework API. See the documentation on Simple Functions for more information.

Using the CLI

To create a Google Cloud Function:

$ mn create-function-app my-app --features google-cloud-function

Or with Micronaut Launch

$ curl https://launch.micronaut.io/create/function/example\?features\=google-cloud-function -o example.zip
$ unzip example.zip -d example

HTTP Functions with Cloud Function

You can deploy regular Micronaut applications that use @Controller etc. using Micronaut’s support for HTTP Functions. See the documentation on Google Cloud HTTP Functions for more information.

Using the CLI

To create a Google Cloud HTTP Function:

$ mn create-app my-app --features google-cloud-function

Or with Micronaut Launch

$ curl https://launch.micronaut.io/example.zip\?features\=google-cloud-function -o example.zip
$ unzip example.zip -d example

10.3 Google Cloud Run

To deploy to Google Cloud Run we recommend using JIB to containerize your application.

Using the CLI

Creating an application with JIB:

$ mn create-app my-app --features jib

Or with Micronaut Launch

$ curl https://launch.micronaut.io/example.zip\?features\=jib -o example.zip
$ unzip example.zip -d example

With JIB setup to deploy your application to Google Container Registry, run:

$ ./gradlew jib

You are now ready to deploy your application:

$ gcloud run deploy --image gcr.io/[PROJECT ID]/example --platform=managed --allow-unauthenticated

Where [PROJECT ID] is your project ID. You will be asked to specify a region and will see output like the following:

Service name: (example):
Deploying container to Cloud Run service [example] in project [PROJECT_ID] region [us-central1]

✓ Deploying... Done.
  ✓ Creating Revision...
  ✓ Routing traffic...
  ✓ Setting IAM Policy...
Done.
Service [example] revision [example-00004] has been deployed and is serving 100 percent of traffic at https://example-9487r97234-uc.a.run.app

The URL is the URL of your Cloud Run application.

10.4 Azure Function

Support for Azure Function is implemented in the Micronaut Azure subproject.

Simple Functions with Azure Function

You can implement Azure Functions with the Micronaut framework that directly implement the Azure Function Java SDK. See the documentation on Azure Functions for more information.

Using the CLI

To create an Azure Function:

$ mn create-function-app my-app --features azure-function

Or with Micronaut Launch

$ curl https://launch.micronaut.io/create/function/example\?features\=azure-function -o example.zip
$ unzip example.zip -d example

HTTP Functions with Azure Function

You can deploy regular Micronaut applications that use @Controller etc. using Micronaut’s support for Azure HTTP Functions. See the documentation on Azure HTTP Functions for more information.

Using the CLI

To create an Azure HTTP Function:

$ mn create-app my-app --features azure-function

Or with Micronaut Launch

$ curl https://launch.micronaut.io/example.zip\?features\=azure-function -o example.zip
$ unzip example.zip -d example

11 Message-Driven Microservices

In the past, with monolithic applications, message listeners that listened to messages from messaging systems would frequently be embedded in the same application unit.

In Microservice architectures it is common to have individual Microservice applications that are driven by a message system such as RabbitMQ or Kafka.

In fact a Message-driven Microservice may not even feature an HTTP endpoint or HTTP server (although this can be valuable from a health check and visibility perspective).

11.1 Kafka Support

Apache Kafka is a distributed stream processing platform that can be used for a range of messaging requirements in addition to stream processing and real-time data handling.

The Micronaut framework features dedicated support for defining both Kafka Producer and Consumer instances. Micronaut applications built with Kafka can be deployed with or without the presence of an HTTP server.

With Micronaut’s efficient compile-time AOP and cloud native features, writing efficient Kafka consumer applications that use very little resources is a breeze.

See the documentation for Micronaut Kafka for more information on how to build Kafka applications with Micronaut.

11.2 RabbitMQ Support

RabbitMQ is the most widely deployed open source message broker.

The Micronaut framework features dedicated support for defining both RabbitMQ publishers and consumers. Micronaut applications built with RabbitMQ can be deployed with or without an HTTP server.

With Micronaut framework’s efficient compile-time AOP, using RabbitMQ has never been easier. Support has been added for publisher confirms and RPC through reactive streams.

See the documentation for Micronaut RabbitMQ for more information on how to build RabbitMQ applications with Micronaut.

11.3 Nats.io Support

Nats.io is a simple, secure, and high-performance open source messaging system for cloud native applications, IoT messaging, and microservices architectures.

The Micronaut framework features dedicated support for defining both Nats.io publishers and consumers. Micronaut applications built with Nats.io can be deployed with or without an HTTP server.

With Micronaut’s efficient compile-time AOP, using Nats.io has never been easier. Support has been added for publisher confirms and RPC through reactive streams.

See the documentation for Micronaut Nats for more information on how to build Nats.io applications with Micronaut.

12 Standalone Command Line Applications

In certain cases you may wish to create standalone command-line (CLI) applications that interact with your Microservice infrastructure.

Examples of applications like this include scheduled tasks, batch applications and general command line applications.

In this case having a robust way to parse command line options and positional parameters is important.

12.1 Picocli Support

Picocli is a command line parser that supports usage help with ANSI colors, autocomplete, and nested subcommands. It has an annotations API to create command line applications with almost no code, and a programmatic API for dynamic uses like creating Domain Specific Languages.

See the documentation for the Picocli integration for more information.

13 Configurations

Micronaut framework features several built-in configurations that enable integration with common databases and other servers.

13.1 Configurations for Reactive Programming

Project Reactor is used internally by Micronaut. However, to use Reactor or other reactive libraries (e.g. RxJava) types in your controller and/or HTTP Client methods you need to include dependencies.

13.1.1 Reactor Support

To add support for Reactor, add the following module:

implementation("io.micronaut.reactor:micronaut-reactor")
<dependency>
    <groupId>io.micronaut.reactor</groupId>
    <artifactId>micronaut-reactor</artifactId>
</dependency>

To use the Reactor HTTP client, add the following dependency:

implementation("io.micronaut.reactor:micronaut-reactor-http-client")
<dependency>
    <groupId>io.micronaut.reactor</groupId>
    <artifactId>micronaut-reactor-http-client</artifactId>
</dependency>

For more information see the documentation for Micronaut Reactor.

13.1.2 RxJava 3 Support

To add support for RxJava 3, add the following module:

implementation("io.micronaut.rxjava3:micronaut-rxjava3")
<dependency>
    <groupId>io.micronaut.rxjava3</groupId>
    <artifactId>micronaut-rxjava3</artifactId>
</dependency>

To use the RxJava 3 HTTP client, add the following dependency:

implementation("io.micronaut.rxjava3:micronaut-rxjava3-http-client")
<dependency>
    <groupId>io.micronaut.rxjava3</groupId>
    <artifactId>micronaut-rxjava3-http-client</artifactId>
</dependency>

For more information see the documentation for Micronaut RxJava 3.

13.1.3 RxJava 2 Support

To add support for RxJava 2, add the following module:

implementation("io.micronaut.rxjava2:micronaut-rxjava2")
<dependency>
    <groupId>io.micronaut.rxjava2</groupId>
    <artifactId>micronaut-rxjava2</artifactId>
</dependency>

To use the RxJava 2 HTTP client, add the following dependency:

implementation("io.micronaut.rxjava2:micronaut-rxjava2-http-client")
<dependency>
    <groupId>io.micronaut.rxjava2</groupId>
    <artifactId>micronaut-rxjava2-http-client</artifactId>
</dependency>

For more information see the documentation for Micronaut RxJava 2.

13.1.4 RxJava 1 Support

Legacy support for RxJava 1 can be added with the following module:

implementation("io.micronaut.rxjava1:micronaut-rxjava1")
<dependency>
    <groupId>io.micronaut.rxjava1</groupId>
    <artifactId>micronaut-rxjava1</artifactId>
</dependency>

For more information see the Micronaut RxJava1 documentation.

13.2 Configurations for Data Access

This table summarizes the configuration modules and dependencies to add to your build to enable them:

Table 1. Data Access Configuration Modules
Dependency Description

io.micronaut.sql:micronaut-jdbc-dbcp

Configures SQL DataSources using Commons DBCP

io.micronaut.sql:micronaut-jdbc-hikari

Configures SQL DataSources using Hikari Connection Pool

io.micronaut.sql:micronaut-jdbc-tomcat

Configures SQL DataSources using Tomcat Connection Pool

io.micronaut.sql:micronaut-hibernate-jpa

Configures Hibernate/JPA EntityManagerFactory beans

io.micronaut.groovy:micronaut-hibernate-gorm

Configures GORM for Hibernate for Groovy applications

io.micronaut.mongodb:micronaut-mongo-reactive

Configures the MongoDB Reactive Driver

io.micronaut.groovy:micronaut-mongo-gorm

Configures GORM for MongoDB for Groovy applications

io.micronaut.neo4j:micronaut-neo4j-bolt

Configures the Bolt Java Driver for Neo4j

io.micronaut.groovy:micronaut-neo4j-gorm

Configures GORM for Neo4j for Groovy applications

io.micronaut.sql:micronaut-vertx-mysql-client

Configures the Reactive MySQL Client

io.micronaut.sql:micronaut-vertx-pg-client

Configures the Reactive Postgres Client

io.micronaut.redis:micronaut-redis-lettuce

Configures the Lettuce driver for Redis

io.micronaut.cassandra:micronaut-cassandra

Configures the Datastax Java Driver for Cassandra

For example, to add support for MongoDB, add the following dependency:

build.gradle
compile "io.micronaut.mongodb:micronaut-mongo-reactive"

For Groovy users, the Micronaut framework provides special support for GORM.

With GORM for Hibernate you cannot have both the hibernate-jpa and hibernate-gorm dependencies.

The following sections go into more detail about configuration options and the exposed beans for each implementation.

13.2.1 Configuring a SQL Data Source

JDBC DataSources can be configured for one of three currently provided implementations - Apache DBCP2, Hikari, and Tomcat are supported by default.

Configuring a JDBC DataSource

Using the CLI

If you create your project using the Micronaut CLI, supply one of the jdbc-tomcat, jdbc-hikari, or jdbc-dbcp features to preconfigure a simple JDBC connection pool in your project, along with a default H2 database driver:

$ mn create-app my-app --features jdbc-tomcat

To get started, add a dependency for one of the JDBC configurations that corresponds to the implementation you will use. Choose one of the following:

runtimeOnly("io.micronaut.sql:micronaut-jdbc-tomcat")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-tomcat</artifactId>
    <scope>runtime</scope>
</dependency>

runtimeOnly("io.micronaut.sql:micronaut-jdbc-hikari")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-hikari</artifactId>
    <scope>runtime</scope>
</dependency>

runtimeOnly("io.micronaut.sql:micronaut-jdbc-dbcp")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-dbcp</artifactId>
    <scope>runtime</scope>
</dependency>

runtimeOnly("io.micronaut.sql:micronaut-jdbc-ucp")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-jdbc-ucp</artifactId>
    <scope>runtime</scope>
</dependency>

Also, add a JDBC driver dependency to your build. For example to add the H2 In-Memory Database:

runtimeOnly("com.h2database:h2")
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

For more information see the Configuring JDBC section of the Micronaut SQL libraries project.

13.2.2 Configuring Hibernate

Setting up a Hibernate/JPA EntityManager

Using the CLI

If you create your project using the Micronaut CLI, supply the hibernate-jpa feature to include a Hibernate JPA configuration in your project:

$ mn create-app my-app --features hibernate-jpa

The Micronaut framework includes support for configuring a Hibernate / JPA EntityManager that builds on the SQL DataSource support.

Once you have configured one or more DataSources to use Hibernate, add the hibernate-jpa dependency to your build:

implementation("io.micronaut.sql:micronaut-hibernate-jpa")
<dependency>
    <groupId>io.micronaut.sql</groupId>
    <artifactId>micronaut-hibernate-jpa</artifactId>
</dependency>

For more information see the Configuring Hibernate section of the Micronaut SQL libraries project.

Using GORM for Hibernate

For Groovy users and users familiar with the Grails framework, special support for GORM for Hibernate is available. To use GORM for Hibernate do not include Micronaut’s built-in SQL Support or the hibernate-jpa dependency since GORM itself takes responsibility for creating the DataSource, SessionFactory, etc.

Using the CLI

If you create your project using the Micronaut CLI, supply the hibernate-gorm feature to include GORM, a basic connection pool configuration, and a default H2 database driver in your project:

$ mn create-app my-app --features hibernate-gorm

See the GORM Modules section of the Micronaut Groovy user guide.

13.2.3 Configuring MongoDB

Setting up the Native MongoDB Driver

Using the CLI

If you create your project using the Micronaut CLI, supply the mongo-reactive feature to configure the native MongoDB driver in your project:

$ mn create-app my-app --features mongo-reactive

The Micronaut framework can automatically configure the native MongoDB Java driver. To use this, add the following dependency to your build:

implementation("io.micronaut.mongodb:micronaut-mongo-reactive")
<dependency>
    <groupId>io.micronaut.mongodb</groupId>
    <artifactId>micronaut-mongo-reactive</artifactId>
</dependency>

Then configure the URI of the MongoDB server in your configuration file (e.g application.yml):

Configuring a MongoDB server
mongodb.uri=mongodb://username:password@localhost:27017/databaseName
mongodb:
  uri: mongodb://username:password@localhost:27017/databaseName
[mongodb]
  uri="mongodb://username:password@localhost:27017/databaseName"
mongodb {
  uri = "mongodb://username:password@localhost:27017/databaseName"
}
{
  mongodb {
    uri = "mongodb://username:password@localhost:27017/databaseName"
  }
}
{
  "mongodb": {
    "uri": "mongodb://username:password@localhost:27017/databaseName"
  }
}
The mongodb.uri follows the MongoDB Connection String format.

A non-blocking Reactive Streams MongoClient is then available for dependency injection.

To use the blocking driver, add a dependency to your build on the mongo-java-driver:

runtimeOnly "org.mongodb:mongo-java-driver"

Then the blocking MongoClient will be available for injection.

See the Micronaut MongoDB documentation for further information on configuring and using MongoDB within Micronaut.

13.2.4 Configuring Neo4j

The Micronaut Framework features dedicated support for automatically configuring the Neo4j Bolt Driver for the popular Neo4j Graph Database.

Using the CLI

If you create your project using the Micronaut CLI, supply the neo4j-bolt feature to configure the Neo4j Bolt driver in your project:

$ mn create-app my-app --features neo4j-bolt

To configure the Neo4j Bolt driver, first add the neo4j-bolt module to your build:

implementation("io.micronaut:micronaut-neo4j-bolt")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-neo4j-bolt</artifactId>
</dependency>

Then configure the URI of the Neo4j server in your configuration file (e.g application.yml):

Configuring neo4j.uri
neo4j.uri=bolt://localhost
neo4j:
  uri: bolt://localhost
[neo4j]
  uri="bolt://localhost"
neo4j {
  uri = "bolt://localhost"
}
{
  neo4j {
    uri = "bolt://localhost"
  }
}
{
  "neo4j": {
    "uri": "bolt://localhost"
  }
}
The neo4j.uri setting must be in the format as described in the Connection URIs section of the Neo4j documentation

Once you have the above configuration in place you can inject an instance of the org.neo4j.driver.v1.Driver bean, which features both a synchronous blocking API and a non-blocking API based on CompletableFuture.

See the Micronaut Neo4j documentation for further information on configuring and using Neo4j within Micronaut.

13.2.5 Configuring Postgres

The Micronaut framework supports a reactive and non-blocking client to connect to Postgres using vertx-pg-client, which can handle many database connections with a single thread.

Configuring the Reactive Postgres Client

Using the CLI

If you create your project using the Micronaut CLI, supply the vertx-pg-client feature to configure the Reactive Postgres client in your project:

$ mn create-app my-app --features vertx-pg-client

To configure the Reactive Postgres client, first add the vertx-pg-client module to your build:

build.gradle
compile "io.micronaut.sql:micronaut-vertx-pg-client"

For more information see the Configuring Reactive Postgres section of the Micronaut SQL libraries project.

13.2.6 Configuring Redis

The Micronaut framework features automatic configuration of the Lettuce driver for Redis via the redis-lettuce module.

Configuring Lettuce

Using the CLI

If you create your project using the Micronaut CLI, supply the redis-lettuce feature to configure the Lettuce driver in your project:

$ mn create-app my-app --features redis-lettuce

To configure the Lettuce driver, first add the redis-lettuce module to your build:

build.gradle
compile "io.micronaut.redis:micronaut-redis-lettuce"

Then configure the URI of the Redis server in your configuration file (e.g application.yml):

Configuring redis.uri
redis.uri=redis://localhost
redis:
  uri: redis://localhost
[redis]
  uri="redis://localhost"
redis {
  uri = "redis://localhost"
}
{
  redis {
    uri = "redis://localhost"
  }
}
{
  "redis": {
    "uri": "redis://localhost"
  }
}
The redis.uri setting must be in the format as described in the Connection URIs section of the Lettuce wiki

You can also specify multiple Redis URIs using redis.uris, in which case a RedisClusterClient is created instead.

For more information and further documentation see the Micronaut Redis documentation.

13.2.7 Configuring Cassandra

Using the CLI

If you create your project using the Micronaut CLI, supply the cassandra feature to include Cassandra configuration in your project:

$ mn create-app my-app --features cassandra

For more information see the Micronaut Cassandra Module documentation.

13.2.8 Configuring Liquibase

To configure the Micronaut integration with Liquibase, please follow these instructions.

13.2.9 Configuring Flyway

To configure the Micronaut integration with Flyway, please follow these instructions

14 Logging

The Micronaut framework uses Slf4j to log messages. The default implementation for applications created via Micronaut Launch is Logback. Any other Slf4j implementation is supported, however.

14.1 Logging Messages

To log messages, use the Slf4j LoggerFactory to get a logger for your class.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggerExample {

    private static Logger logger = LoggerFactory.getLogger(LoggerExample.class);

    public static void main(String[] args) {
        logger.debug("Debug message");
        logger.info("Info message");
        logger.error("Error message");
    }
}

14.2 Configuration

Log levels can be configured via properties defined in your configuration file (e.g. application.yml) (and environment variables) with the logger.levels prefix:

logger.levels.foo.bar=ERROR
logger:
  levels:
    foo.bar: ERROR
[logger]
  [logger.levels]
    "foo.bar"="ERROR"
logger {
  levels {
    foo.bar = "ERROR"
  }
}
{
  logger {
    levels {
      "foo.bar" = "ERROR"
    }
  }
}
{
  "logger": {
    "levels": {
      "foo.bar": "ERROR"
    }
  }
}

The same configuration can be achieved by setting the environment variable LOGGER_LEVELS_FOO_BAR. Note that there is currently no way to set log levels for unconventional prefixes such as foo.barBaz.

Custom Logback XML Configuration

logger.config=/foo/custom-logback.xml
logger:
  config: /foo/custom-logback.xml
[logger]
  config="/foo/custom-logback.xml"
logger {
  config = "/foo/custom-logback.xml"
}
{
  logger {
    config = "/foo/custom-logback.xml"
  }
}
{
  "logger": {
    "config": "/foo/custom-logback.xml"
  }
}

You can also set a custom Logback XML configuration file to be used via logger.config. The file is first checked on the classpath and then on the file system.

Disabling a Logger with Properties

To disable a logger, you need to set the logger level to OFF:

logger.levels.io.verbose.logger.who.CriedWolf=false
logger:
  levels:
    io.verbose.logger.who.CriedWolf: OFF
[logger]
  [logger.levels]
    "io.verbose.logger.who.CriedWolf"=false
logger {
  levels {
    io.verbose.logger.who.CriedWolf = false
  }
}
{
  logger {
    levels {
      "io.verbose.logger.who.CriedWolf" = false
    }
  }
}
{
  "logger": {
    "levels": {
      "io.verbose.logger.who.CriedWolf": false
    }
  }
}
  • This will disable ALL logging for the class io.verbose.logger.who.CriedWolf

Note that the ability to control log levels via config is controlled via the LoggingSystem interface. Currently, the Micronaut framework includes a single implementation that allows setting log levels for the Logback library. If you use another library, you should provide a bean that implements this interface.

14.3 Logback

To use the logback library, add the following dependency to your build.

implementation("ch.qos.logback:logback-classic")
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
</dependency>

Logback 1.3.x+ included a breaking binary change that may prevent it working with 3.8.x of the Micronaut framework. If you are using Logback 1.3.x+ and are experiencing issues, please downgrade to Logback 1.2.x.

If it does not exist yet, place a logback.xml file in the resources folder and modify the content for your needs. For example:

src/main/resources/logback.xml
<configuration>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n
            </pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>

</configuration>

To change the log level for specific classes or package names, you can add such a logger entry to the configuration section:

<configuration>
    ...
    <logger name="io.micronaut.context" level="TRACE"/>
    ...
</configuration>

14.4 Logging System

The Micronaut framework has a notion of a logging system. In short, it is a simple API to be able to set log levels in the logging implementation at runtime. Default implementations are provided for Logback and Log4j2. The behavior of the logging system can be overridden by creating your own implementation of LoggingSystem and replace the implementation being used with the @Replaces annotation.

15 Language Support

Micronaut framework supports any JVM language that implements the Java Annotation Processor API.

Although Groovy does not support this API, special support has been built using AST transformations. The current list of supported languages is: Java, Groovy, and Kotlin (via the kapt tool).

Theoretically any language that supports a way to analyze the AST at compile time could be supported. The io.micronaut.inject.writer package includes language-neutral classes that build BeanDefinition classes at compile time using the ASM tool.

The following sections cover language-specific features and considerations for using Micronaut.

15.1 Micronaut for Java

For Java, Micronaut framework uses a Java BeanDefinitionInjectProcessor annotation processor to process classes at compile time and produce BeanDefinition classes.

The major advantage here is that you pay a slight cost at compile time, but at runtime Micronaut framework is largely reflection-free, fast, and consumes very little memory.

15.1.1 Using Micronaut with Java 9+

The Micronaut framework is built with Java 8 but works fine with Java 9 and above. The classes that Micronaut generates sit alongside existing classes in the same package, hence do not violate anything regarding the Java module system.

There are some considerations when using Java 9+ with Micronaut.

The javax.annotation package

Using the CLI

If you create your project using the Micronaut CLI, the javax.annotation dependency is added to your project automatically if you use Java 9+.

The javax.annotation, which includes @PostConstruct, @PreDestroy, etc. has been moved from the core JDK to a module. In general annotations in this package should be avoided and instead the jakarta.annotation equivalents used.

15.1.2 Incremental Annotation Processing with Gradle

The Micronaut framework supports Gradle incremental annotation processing which speeds up builds by compiling only classes that have changed, avoiding a full recompilation.

However, the support is disabled by default since the Micronaut framework allows the definition of custom meta-annotations (to for example define custom AOP advice) that need to be configured for processing.

The following example demonstrates how to enable and configure incremental annotation processing for annotations you have defined under the com.example package:

Enabling Incremental Annotation Processing
tasks.withType(JavaCompile) {
    options.compilerArgs = [
        '-Amicronaut.processing.incremental=true',
        '-Amicronaut.processing.annotations=com.example.*',
    ]
}
If you do not enable processing for your custom annotations, they will be ignored by Micronaut, which may break your application.

15.1.3 Using Project Lombok

Project Lombok is a popular java library that adds a number of useful AST transformations to the Java language via annotation processors.

Since both the Micronaut framework and Lombok use annotation processors, special care must be taken when configuring Lombok to ensure that the Lombok processor runs before Micronaut’s processor.

If you use Gradle, add the following dependencies:

Configuring Lombok in Gradle
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor "org.projectlombok:lombok:1.18.24"
...
// Micronaut processor defined after Lombok
annotationProcessor "io.micronaut:micronaut-inject-java"

Or if using Maven:

Configuring Lombok in Maven
<dependencies>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
    <scope>provided</scope>
  </dependency>
</dependencies>
...
<annotationProcessorPaths combine.self="override">
  <path>
    <!-- must precede micronaut-inject-java -->
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
  </path>
  <path>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-inject-java</artifactId>
    <version>${micronaut.version}</version>
  </path>
  <path>
    <groupId>io.micronaut.validation</groupId>
    <artifactId>micronaut-validation-processor</artifactId>
    <version>${micronaut.version}</version>
  </path>
</annotationProcessorPaths>
In both cases (Gradle and Maven) the Micronaut processor must be configured after the Lombok processor. Reversing the order of the declared dependencies will not work.

15.1.4 Configuring an IDE

You can use any IDE to develop Micronaut applications, if you depend on your configured build tool (Gradle or Maven) to build the application.

However, running tests within the IDE is currently possible with IntelliJ IDEA or Eclipse 4.9 or higher.

See the section on IDE Setup in the Quick start for more information on how to configure IntelliJ and Eclipse.

15.1.5 Retaining Parameter Names

By default, with Java, the parameter name data for method parameters is not retained at compile time. This can be a problem for the Micronaut framework if you do not define parameter names explicitly and depend on an external JAR that is already compiled.

Consider this interface:

Client Interface
interface HelloOperations {
    @Get("/hello/{name}")
    String hello(String name);
}

At compile time the parameter name name is lost and becomes arg0 when compiled against or read via reflection later. To avoid this problem you have two options. You can either declare the parameter name explicitly:

Client Interface
interface HelloOperations {
    @Get("/hello/{name}")
    String hello(@QueryValue("name") String name);
}

Or alternatively it is recommended that you compile all bytecode with -parameters flag to javac. See Obtaining Names of Method Parameters. For example in build.gradle:

build.gradle
compileJava.options.compilerArgs += '-parameters'

15.2 Micronaut for Groovy

Groovy has first-class support in Micronaut.

Groovy-Specific Modules

Additional modules exist specific to Groovy that improve the overall experience. These are detailed in the table below:

Table 1. Groovy-Specific Modules
Dependency Description

io.micronaut:micronaut-inject-groovy

Includes AST transformations to generate bean definitions. Should be compileOnly on your classpath.

io.micronaut:micronaut-runtime-groovy

Adds the ability to specify configuration under src/main/resources in Groovy format (i.e. application.groovy)

io.micronaut:micronaut-function-groovy

Includes AST transforms that make it easier to write Functions for AWS Lambda

The most common module you need is micronaut-inject-groovy, which enables DI and AOP for Groovy classes.

Groovy Support in the CLI

The Micronaut Command Line Interface includes special support for Groovy. To create a Groovy application, use the groovy lang option. For example:

Create a Micronaut Groovy application
$ mn create-app hello-world --lang groovy

The above generates a Groovy project, built with Gradle. Use the -build maven flag to generate a project built with Maven instead.

Once you have created an application with the groovy feature, commands like create-controller, create-client etc. generate Groovy files instead of Java. The following example demonstrates this when using interactive mode of the CLI:

Create a bean
$ mn
| Starting interactive mode...
| Enter a command name to run. Use TAB for completion:
mn>

create-bean          create-client        create-controller
create-job           help

mn> create-bean helloBean
| Rendered template Bean.groovy to destination src/main/groovy/hello/world/HelloBean.groovy

The above example demonstrates creating a Groovy bean that looks like the following:

Micronaut Bean
package hello.world

import jakarta.inject.Singleton

@Singleton
class HelloBean {

}
Groovy automatically imports groovy.lang.Singleton which can be confusing as it conflicts with jakarta.inject.Singleton. Make sure you use jakarta.inject.Singleton when declaring a Micronaut singleton bean to avoid surprising behavior.

We can also create a client - don’t forget Micronaut framework can act as a client or a server!

Create a client
mn> create-client helloClient
| Rendered template Client.groovy to destination src/main/groovy/hello/world/HelloClient.groovy
Micronaut Client
package hello.world

import io.micronaut.http.client.annotation.Client
import io.micronaut.http.annotation.Get
import io.micronaut.http.HttpStatus

@Client("hello")
interface HelloClient {

    @Get
    HttpStatus index()
}

Now let’s create a controller:

Create a controller
mn> create-controller helloController
| Rendered template Controller.groovy to destination src/main/groovy/hello/world/HelloController.groovy
| Rendered template ControllerSpec.groovy to destination src/test/groovy/hello/world/HelloControllerSpec.groovy
mn>
Micronaut Controller
package hello.world

import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.HttpStatus

@Controller("/hello")
class HelloController {

    @Get
    HttpStatus index() {
        return HttpStatus.OK
    }
}

As you can see from the output from the CLI, a Spock test was also generated for you which demonstrates how to test the controller:

HelloControllerSpec.groovy
...
    void "test index"() {
        given:
        HttpResponse response = client.toBlocking().exchange("/hello")

        expect:
        response.status == HttpStatus.OK
    }
...

Notice how you use the Micronaut framework both as client and as a server to test itself.

Programmatic Routes with GroovyRouterBuilder

If you prefer to build your routes programmatically (similar to Grails UrlMappings), a special io.micronaut.web.router.GroovyRouteBuilder exists that has some enhancements to make the DSL better.

The following example shows GroovyRouteBuilder in action:

Using GroovyRouteBuilder
@Singleton
static class MyRoutes extends GroovyRouteBuilder {

    MyRoutes(ApplicationContext beanContext) {
        super(beanContext)
    }

    @Inject
    void bookResources(BookController bookController, AuthorController authorController) {
        GET(bookController) {
            POST("/hello{/message}", bookController.&hello) (1)
        }
        GET(bookController, ID) { (2)
            GET(authorController)
        }
    }
}
1 You can use injected controllers to create routes by convention and Groovy method references to create routes to methods
2 The ID property can be used to reference to include an {id} URI variable

The above example results in the following routes:

  • /book - Maps to BookController.index()

  • /book/hello/{message} - Maps to BookController.hello(String)

  • /book/{id} - Maps to BookController.show(String id)

  • /book/{id}/author - Maps to AuthorController.index

Using GORM in a Groovy application

GORM is a data access toolkit originally created as part of Grails. It supports multiple database types. The following table summarizes the modules needed to use GORM, and links to documentation.

Table 2. GORM Modules
Dependency Description

io.micronaut.groovy:micronaut-hibernate-gorm

Configures GORM for Hibernate for Groovy applications. See the Hibernate Support docs

io.micronaut.groovy:micronaut-mongo-gorm

Configures GORM for MongoDB for Groovy applications. See the Mongo Support docs.

io.micronaut.groovy:micronaut-neo4j-gorm

Configures GORM for Neo4j for Groovy applications. See the Neo4j Support docs.

Once you have configured a GORM implementation per the instructions linked in the table above you can use all features of GORM.

GORM Data Services can also participate in dependency injection and life cycle methods:

GORM Data Service VehicleService.groovy
@Service(Vehicle)
abstract class VehicleService {
    @PostConstruct
    void init() {
       // do something on initialization
    }

    abstract Vehicle findVehicle(@NotBlank String name)

    abstract Vehicle saveVehicle(@NotBlank String name)
}

You can also define the service as an interface instead of an abstract class to have GORM implement the methods for you.

Serverless Functions with Groovy

A microservice application is just one way to use Micronaut. You can also use it for serverless functions like on AWS Lambda.

With the function-groovy module, the Micronaut framework features enhanced support for functions written in Groovy.

See the section on Serverless Functions for more information.

15.3 Micronaut for Kotlin

The Command Line Interface for Micronaut framework includes special support for Kotlin. To create a Kotlin application use the kotlin lang option. For example:
Create a Micronaut Kotlin application
$ mn create-app hello-world --lang kotlin

Since the 4.0 release, Micronaut framework offers support for Kotlin via Kapt or Kotlin Symbol Processing API.

15.3.1 Kotlin support via KAPT or KSP

Micronaut framework has offered support for Kotlin via Kapt.

With version 4.0, Micronaut framework supports Kotlin also via Kotlin Symbol Processing (KSP) API.

Please note that KAPT is in maintenance mode. Micronaut framework 4 includes experimental support for KSP which Kotlin users should consider migrating in the future.

kapt is in maintenance mode. We are keeping it up-to-date with recent Kotlin and Java releases but have no plans to implement new features.

KAPT supports existing Java annotation processors by generating Java stubs and feeding them into the Java annotation processors.

By skipping the generation of stubs, KSP offers several advantages:

  • Faster compilation.

  • Better support Kotlin native syntax.

If you use other annotation processors besides the Micronaut annotation processors, they will not work with KSP.

15.3.2 Kotlin Annotation Processing (KAPT)

The Kapt compiler plugin includes support for Java annotation processors. To use Kotlin in your Micronaut application, add the proper dependencies to configure and run kapt on your kt source files. Kapt creates Java "stub" classes for your Kotlin classes, which can then be processed by Micronaut’s Java annotation processor. The stubs are not included in the final compiled application.

Learn more about kapt and its features from the official documentation.

The Micronaut annotation processors are declared in the kapt scope when using Gradle. For example:

Example build.gradle
dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" (1)
    compile "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
    kapt "io.micronaut:micronaut-inject-java" (2)
    kaptTest "io.micronaut:micronaut-inject-java" (3)
    ...
}
1 Add the Kotlin standard libraries
2 Add the micronaut-inject-java dependency under the kapt scope, so classes in src/main are processed
3 Add the micronaut-inject-java dependency under the kaptTest scope, so classes in src/test are processed.

With a build.gradle file similar to the above, you can now run your Micronaut application using the run task (provided by the Application plugin):

$ ./gradlew run

15.3.3 Kotlin Symbol Processing (KSP)

You can build Micronaut applications with Kotlin and KSP:

Kotlin Symbol Processing (KSP) is an API that you can use to develop lightweight compiler plugins. KSP provides a simplified compiler plugin API that leverages the power of Kotlin while keeping the learning curve at a minimum. Compared to kapt, annotation processors that use KSP can run up to 2 times faster.

If you use the Micronaut Gradle Plugin, you can build Micronaut applications with Kotlin and KSP. You need to apply the com.google.devtools.ksp Gradle plugin.

build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.jvm") version "1.9.20"
    id("com.google.devtools.ksp") version "1.9.20-1.0.13"
    id("org.jetbrains.kotlin.plugin.allopen") version "1.9.20"
    id("io.micronaut.application") version "4.0.0"
}
version = "0.1"
group = "example.micronaut"
repositories {
    mavenCentral()
}
dependencies {
    runtimeOnly("ch.qos.logback:logback-classic")
    runtimeOnly("org.yaml:snakeyaml")
    implementation("io.micronaut:micronaut-jackson-databind")
    testImplementation("io.micronaut:micronaut-http-client")
}
application {
    mainClass.set("example.micronaut.Application")
}
graalvmNative.toolchainDetection.set(false)
micronaut {
    runtime("netty")
    testRuntime("junit5")
    processing {
        incremental(true)
        annotations("example.micronaut.*")
    }
}

If you don’t use the Micronaut Gradle Plugin, in addition to applying the com.google.devtools.ksp Gradle plugin, you have to add micronaut-inject-kotlin with the ksp configuration.

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.9.20"
    id("com.google.devtools.ksp") version "1.9.20-1.0.13"
    id("org.jetbrains.kotlin.plugin.allopen") version "1.9.20"
    application
}
version = "0.1"
group = "dockerisms"
repositories {
    mavenCentral()

    maven {
        url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
    }
}
val micronautVersion by properties
dependencies {
    runtimeOnly("ch.qos.logback:logback-classic")
    runtimeOnly("org.yaml:snakeyaml")
    implementation("io.micronaut:micronaut-jackson-databind")


    implementation(platform("io.micronaut.platform:micronaut-platform:$micronautVersion"))
    implementation("io.micronaut:micronaut-http-server-netty")

    ksp(platform("io.micronaut.platform:micronaut-platform:$micronautVersion"))
    ksp("io.micronaut:micronaut-inject-kotlin")
    kspTest(platform("io.micronaut.platform:micronaut-platform:$micronautVersion"))
    kspTest("io.micronaut:micronaut-inject-kotlin")

    testImplementation(platform("io.micronaut.platform:micronaut-platform:$micronautVersion"))
    testImplementation("io.micronaut:micronaut-http-client")
    testImplementation("io.micronaut.test:micronaut-test-junit5")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

application {
    mainClass.set("dockerisms.Application")
}

tasks {
    withType<Test> {
        useJUnitPlatform()
    }
}

Unfortunately, KSP doesn’t see the changes in the classes made by other compiler plugins, which breaks integration with the allopen plugin. To make the integration work, we have introduced an experimental system property kotlin.allopen.annotations for the annotation processor. The property expects a list of annotations that are open, separated by ,.

15.3.4 Controller in Kotlin

An example controller written in Kotlin can be seen below:

src/main/kotlin/example/HelloController.kt
package example

import io.micronaut.http.annotation.*

@Controller("/")
class HelloController {

    @Get("/hello/{name}")
    fun hello(name: String): String {
        return "Hello $name"
    }
}

15.3.5 Kotlin, Kapt and IntelliJ

As of this writing, IntelliJ’s built-in compiler does not directly support Kapt and annotation processing. You must instead configure Intellij to run Gradle (or Maven) compilation as a build step before running your tests or application class.

First, edit the run configuration for tests or for the application and select "Run Gradle task" as a build step:

Intellij Settings

Then add the classes task as task to execute for the application or for tests the testClasses task:

Intellij Settings

Now when you run tests or start the application, the Micronaut framework will generate classes at compile time.

Alternatively, you can delegate IntelliJ build/run actions to Gradle completely:

delegatetogradle

15.3.6 Incremental Annotation Processing with Gradle and Kapt

To enable Gradle incremental annotation processing with Kapt, the arguments as specified in Incremental Annotation Processing with Gradle must be sent to Kapt.

The following example demonstrates how to enable and configure incremental annotation processing for annotations you have defined under the com.example and io.example packages:

Enabling Incremental Annotation Processing in Kapt
kapt {
    arguments {
        arg("micronaut.processing.incremental", true)
        arg("micronaut.processing.annotations", "com.example.*,io.example.*")
    }
}
If you do not enable processing for your custom annotations, they will be ignored by Micronaut, which may break your application.

15.3.7 Kotlin and AOP Advice

The Micronaut framework provides a compile-time AOP API that does not use reflection. When you use any Micronaut AOP Advice, it creates a subclass at compile-time to provide the AOP behaviour. This can be a problem because Kotlin classes are final by default. If the application was created with the Micronaut CLI, the Kotlin all-open plugin is configured for you to automatically change your classes to open when an AOP annotation is used. To configure it yourself, add the Around class to the list of supported annotations.

If you prefer not to or cannot use the all-open plugin, you must declare the classes that are annotated with an AOP annotation to be open:

import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.HttpStatus
import io.micronaut.validation.Validated
import jakarta.validation.constraints.NotBlank

@Validated
@Controller("/email")
open class EmailController { (1)

    @Get("/send")
    fun index(@NotBlank recipient: String, (1)
              @NotBlank subject: String): HttpStatus {
        return HttpStatus.OK
    }
}
1 if you use @Validated AOP Advice, you need to use open at class and method level.
The all-open plugin does not handle methods. If you declare an AOP annotation on a method, you must manually declare it as open.

15.3.8 Kotlin and Retaining Parameter Names

Like with Java, the parameter name data for method parameters is not retained at compile time when using Kotlin. This can be a problem for the Micronaut framework if you do not define parameter names explicitly and depend on an external JAR that is already compiled.

To enable retention of parameter name data with Kotlin, set the javaParameters option to true in your build.gradle:

configuration in Gradle
compileTestKotlin {
    kotlinOptions {
        javaParameters = true
    }
}
If you use interfaces with default methods add freeCompilerArgs = ["-Xjvm-default=all"] for the Micronaut framework to recognize them.

Or if using Maven configure the Micronaut Maven Plugin accordingly:

configuration in Maven
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <!-- ... -->
  <build>
    <plugins>
      <!-- ... -->
      <plugin>
        <artifactId>kotlin-maven-plugin</artifactId>
        <groupId>org.jetbrains.kotlin</groupId>
        <configuration>
            <javaParameters>true</javaParameters>
            <!-- ... -->
        </configuration>
        <!-- ... -->
      </plugin>
      <!-- ... -->
    </plugins>
  </build>
</project>

15.3.9 Coroutines Support

Kotlin coroutines allow you to create asynchronous applications with imperative style code. A Micronaut controller action can be a suspend function:

Controller suspend function example
@Get("/simple", produces = [MediaType.TEXT_PLAIN])
suspend fun simple(): String { (1)
    return "Hello"
}
1 The function is marked as suspend, though in reality it won’t be suspended.
1 The function is marked as suspend.
2 The delay is called to make sure that a function is suspended and the response is returned from a different thread.
Controller suspend function example
@Status(HttpStatus.CREATED) (1)
@Get("/status")
suspend fun status() {
}
1 suspend function also works when all we want is to return a status.
Controller suspend function example
@Status(HttpStatus.CREATED)
@Get("/statusDelayed")
suspend fun statusDelayed() {
    delay(1)
}

You can also use Flow type for streaming server and client. A streaming controller can return Flow, for example:

Streaming JSON on the Server with Flow
@Get(value = "/headlinesWithFlow", processes = [MediaType.APPLICATION_JSON_STREAM])
internal fun streamHeadlinesWithFlow(): Flow<Headline> = (1)
    flow { (2)
        repeat(100) { (3)
            with (Headline()) {
                text = "Latest Headline at ${ZonedDateTime.now()}"
                emit(this) (4)
                delay(1_000) (5)
            }
        }
    }
1 A method streamHeadlinesWithFlow is defined that produces application/x-json-stream
2 A Flow is created using flow
3 This Flow emits 100 messages
4 Emitting happens with emit suspend function
5 There is a one second delay between messages

A streaming client can simply return a Flow, for example:

Streaming client with Flow
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
import kotlinx.coroutines.flow.Flow
@Client("/streaming")
interface HeadlineFlowClient {
1 The @Get method is defined as processing responses of type APPLICATION_JSON_STREAM
2 The return type is Flow

15.3.10 Coroutine Tracing Context Propagation

The Micronaut framework supports tracing context propagation. If you use suspend functions all the way from your controller actions down to all your services, you don’t have to do anything special. However, when you create coroutines within a regular function, tracing propagation won’t happen automatically. You have to use a HttpCoroutineContextFactory<CoroutineTracingDispatcher> to create a new CoroutineTracingDispatcher and use it as a CoroutineContext.

Following example shows how this might look like:

@Controller
class SimpleController(
    private val coroutineTracingDispatcherFactory: HttpCoroutineContextFactory<CoroutineTracingDispatcher>
) {
    @Get("/runParallelly")
    fun runParallelly(): String = runBlocking {
        val a = async(Dispatchers.Default + coroutineTracingDispatcherFactory.create()) {
            val traceId = MDC.get("traceId")
            println("$traceId: Calculating sth...")
            calculateSth()
        }
        val b = async(Dispatchers.Default + coroutineTracingDispatcherFactory.create()) {
            val traceId = MDC.get("traceId")
            println("$traceId: Calculating sth else...")
            calculateSthElse()
        }

        a.await() + b.await()
    }
}

15.3.11 Reactive Context Propagation

The Micronaut framework supports context propagation from Reactor’s context to coroutine context. To enable this propagation you need to include following dependency:

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-reactor</artifactId>
</dependency>

For more detailed information on how to use the library you can find at the official documentation.

Since Micronaut framework 4, we recommend using the latest Context Propagation API. The ThreadPropagatedContextElement is inspired by Kotlin Coroutines propagation API element kotlinx.coroutines.ThreadContextElement and acts similarly by restoring thread locals.

Following example shows how to propagate Reactor context from the HTTP filter to the controller’s coroutine:

Simple filter which writes into Reactor’s context
@Filter(Filter.MATCH_ALL_PATTERN)
class ReactorHttpServerFilter : HttpServerFilter {

    override fun doFilter(request: HttpRequest<*>, chain: ServerFilterChain): Publisher<MutableHttpResponse<*>> {
        val trackingId = request.headers["X-TrackingId"] as String
        return Mono.from(chain.proceed(request)).contextWrite {
            it.put("reactorTrackingId", trackingId)
        }
    }

    override fun getOrder(): Int = 1
}

Access Reactor context by retrieving ReactorContext from the coroutine context:

Reading Reactor context in the coroutine
@Get("/data")
suspend fun getTracingId(request: HttpRequest<*>): String {
    val reactorContextView = currentCoroutineContext()[ReactorContext.Key]!!.context
    return reactorContextView.get("reactorTrackingId") as String
}

It’s possible to use coroutines Reactor integration to create a filter using a suspended function:

Suspended function filter which writes into Reactor’s context
@Filter(Filter.MATCH_ALL_PATTERN)
class SuspendHttpServerFilter : CoroutineHttpServerFilter {

    override suspend fun filter(request: HttpRequest<*>, chain: ServerFilterChain): MutableHttpResponse<*> {
        val trackingId = request.headers["X-TrackingId"] as String
        //withContext does not merge the current context so data may be lost
        return withContext(Context.of("suspendTrackingId", trackingId).asCoroutineContext()) {
            chain.next(request)
        }
    }

    override fun getOrder(): Int = 0
}

interface CoroutineHttpServerFilter : HttpServerFilter {

    suspend fun filter(request: HttpRequest<*>, chain: ServerFilterChain): MutableHttpResponse<*>

    override fun doFilter(request: HttpRequest<*>, chain: ServerFilterChain): Publisher<MutableHttpResponse<*>> {
        return mono {
            filter(request, chain)
        }
    }

}

suspend fun ServerFilterChain.next(request: HttpRequest<*>): MutableHttpResponse<*> {
    return this.proceed(request).asFlow().single()
}

15.4 Micronaut for GraalVM

GraalVM is a new universal virtual machine from Oracle that supports a polyglot runtime environment and the ability to compile Java applications to native machine code.

Any Micronaut application can be run using the GraalVM JVM, however special support has been added to Micronaut to support running Micronaut applications using GraalVM’s native-image tool.

Micronaut framework currently supports GraalVM version 22.0.0.2 and the team is improving the support in every new release. Don’t hesitate to report issues however if you find any problem.

Many of Micronaut’s modules and third-party libraries have been verified to work with GraalVM: HTTP server, HTTP client, Function support, Micronaut Data JDBC and JPA, Service Discovery, RabbitMQ, Views, Security, Zipkin, etc. Support for other modules is evolving and will improve over time.

Getting Started

Use of GraalVM’s native-image tool is only supported in Java or Kotlin projects. Groovy relies heavily on reflection which is only partially supported by GraalVM.

To start using GraalVM, first install the GraalVM SDK via the Getting Started instructions or using Sdkman!.

15.4.1 Microservices as GraalVM native images

Getting Started with the Micronaut framework and GraalVM

Since Micronaut framework 2.2, any Micronaut application is ready to be built into a native image using the Micronaut Gradle or Maven plugins. To get started, create a new application:

Creating a GraalVM Native Microservice
$ mn create-app hello-world

You can use --build maven for a Maven build.

Building a Native Image Using Docker

To build your native image using Gradle and Docker, run:

Building a Native Image with Docker and Gradle
$ ./gradlew dockerBuildNative

To build your native image using Maven and Docker, run:

Building a Native Image with Docker and Maven
$ ./mvnw package -Dpackaging=docker-native

Building a Native Image Without Using Docker

To build your native image without using Docker, install the GraalVM SDK via the Getting Started instructions or using Sdkman!:

Installing GraalVM 22.0.0.2 with SDKman
$ sdk install java 22.0.0.2.r11-grl
$ sdk use java 22.0.0.2.r11-grl

The native-image tool was extracted from the base GraalVM distribution and is available as a plugin. To install it, run:

Installing native-image tool
$ gu install native-image

Now you can build a native image with Gradle by running the nativeCompile task:

Creating native image with Gradle
$ ./gradlew nativeCompile

The native image will be built in the build/native/nativeCompile directory.

To create a native image with Maven and the Micronaut Maven plugin, use the native-image packaging format:

Creating native image with Maven
$ ./mvnw package -Dpackaging=native-image

which builds the native image in the target directory.

You can then run the native image from the directory where you built it.

Run native image
$ ./hello-world

Understanding Micronaut framework and GraalVM

The Micronaut framework itself does not rely on reflection or dynamic classloading, so it works automatically with GraalVM native, however certain third-party libraries used by Micronaut may require additional input about uses of reflection.

The Micronaut framework includes an annotation processor that helps to generate reflection configuration that is automatically picked up by the native-image tool:

annotationProcessor("io.micronaut:micronaut-graal")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-graal</artifactId>
    </path>
</annotationProcessorPaths>

This processor generates additional classes that implement the GraalReflectionConfigurer inteface and programmatically register reflection configuration.

For example the following class:

package example;

import io.micronaut.core.annotation.ReflectiveAccess;

@ReflectiveAccess
class Test {
    ...
}

The above example results in the public methods, declared fields and declared constructors of example.Test being registered for reflective access.

If you have more advanced requirements and only wish to include certain fields or methods, use the annotation on any constructor, field or method to include only the specific field, constructor or method.

Adding Additional Classes for Reflective Access

To inform the Micronaut framework of additional classes to be included in the generated reflection configuration a number of annotations are available including:

The @ReflectiveAccess annotation is typically used on a particular type, constructor, method or field whilst the latter two are typically used on a module or Application class to include classes that are needed reflectively. For example, the following is from Micronaut’s Jackson module with @TypeHint:

Using the @TypeHint annotation
@TypeHint(
    value = { (1)
        PropertyNamingStrategy.UpperCamelCaseStrategy.class,
        ArrayList.class,
        LinkedHashMap.class,
        HashSet.class
    },
    accessType = TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS (2)
)
1 The value member specifies which classes require reflection.
2 The accessType member specifies if only classloading access is needed or whether full reflection on all public members is needed.

Or alternatively with the @ReflectionConfig annotation which is repeatable and allows distinct configuration per type:

Using the @ReflectionConfig annotation
@ReflectionConfig(
    type = PropertyNamingStrategy.UpperCamelCaseStrategy.class,
    accessType = TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS
)
@ReflectionConfig(
    type = ArrayList.class,
    accessType = TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS
)
@ReflectionConfig(
    type = LinkedHashMap.class,
    accessType = TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS
)
@ReflectionConfig(
    type = HashSet.class,
    accessType = TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS
)

Generating Native Images

GraalVM’s native-image command generates native images. You can use this command manually to generate your native image. For example:

The native-image command
native-image --class-path build/libs/hello-world-0.1-all.jar (1)
1 The class-path argument refers to the Micronaut shaded JAR

Once the image is built, run the application using the native image name:

Running the Native Application
$ ./hello-world
15:15:15.153 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 14ms. Server Running: http://localhost:8080

As you can see, the native image startup completes in milliseconds, and memory consumption does not include the overhead of the JVM (a native Micronaut application runs with just 20mb of memory).

Resource file generation

Starting in Micronaut framework 3.0 the automatic generation of the resource-config.json file is now part of the Gradle and Maven plugins.

15.4.2 GraalVM and Micronaut FAQ

How does Micronaut framework manage to run on GraalVM?

The Micronaut framework features a Dependency Injection and Aspect-Oriented Programming runtime that uses no reflection. This makes it easier for Micronaut applications to run on GraalVM since there are compatibility concerns particularly around reflection in Native Images.

How can I make a Micronaut application that uses picocli run on GraalVM?

Picocli provides a picocli-codegen module with a tool for generating a GraalVM reflection configuration file. The tool can be run manually or automatically as part of the build. The module’s README has usage instructions with code snippets for configuring Gradle and Maven to generate a cli-reflect.json file automatically as part of the build. Add the generated file to the -H:ReflectionConfigurationFiles option when running the native-image tool.

What about other Third-Party Libraries?

The Micronaut framework cannot guarantee that third-party libraries work on GraalVM SubstrateVM, that is down to each individual library to implement support.

I Get a "Class XXX is instantiated reflectively…​" Exception. What do I do?

If you get an error such as:

Class myclass.Foo[] is instantiated reflectively but was never registered. Register the class by using org.graalvm.nativeimage.RuntimeReflection

You may need to manually tweak the generated reflect.json file. For regular classes you need to add an entry into the array:

[
    {
        "name" : "myclass.Foo",
        "allDeclaredConstructors" : true
    },
    ...
]

For arrays this must use the Java JVM internal array representation. For example:

[
    {
        "name" : "[Lmyclass.Foo;",
        "allDeclaredConstructors" : true
    },
    ...
]

What if I want to set the heap’s maximum size with -Xmx, but I get an OutOfMemoryError?

If you set the maximum heap size in the Dockerfile that you use to build your native image, you will probably get a runtime error like this:

java.lang.OutOfMemoryError: Direct buffer memory

The problem is that Netty tries to allocate 16MB of memory per chunk with its default settings for io.netty.allocator.pageSize and io.netty.allocator.maxOrder:

int defaultChunkSize = DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER; // 8192 << 11 = 16MB

The simplest solution is to specify io.netty.allocator.maxOrder explicitly in your Dockerfile’s entrypoint. A working example with -Xmx64m:

ENTRYPOINT ["/app/application", "-Xmx64m", "-Dio.netty.allocator.maxOrder=8"]

To go further, you can also experiment with io.netty.allocator.numHeapArenas or io.netty.allocator.numDirectArenas. You can find more information about Netty’s PooledByteBufAllocator in the official documentation.

16 Management & Monitoring

Using the CLI

If you create your project using the Micronaut CLI, supply the management feature to configure the management endpoints in your project:

$ mn create-app my-app --features management

Inspired by Spring Boot and Grails, the Micronaut management dependency adds support for monitoring of your application via endpoints: special URIs that return details about the health and state of your application. The management endpoints are also integrated with Micronaut’s security dependency, allowing for sensitive data to be restricted to authenticated users in your security system (see Built-in Endpoints Access in the Security section).

To use the management features described in this section, add this dependency to your build:

implementation("io.micronaut:micronaut-management")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-management</artifactId>
</dependency>

16.1 Creating Endpoints

In addition to the Built-In Endpoints, the management dependency also provides support for creating custom endpoints. These can be enabled and configured like the built-in endpoints, and can be used to retrieve and return any metrics or other application data.

16.1.1 The Endpoint Annotation

An Endpoint can be created by annotating a class with the Endpoint annotation, and supplying it with (at minimum) an endpoint id.

FooEndpoint.java
@Endpoint("foo")
class FooEndpoint {
    ...
}

If a single String argument is supplied to the annotation, it is used as the endpoint id.

It is possible to supply additional (named) arguments to the annotation. Other possible arguments to @Endpoint are described in the table below:

Table 1. Endpoint Arguments
Argument Description Endpoint Example

String id

The endpoint id (or name)

@Endpoint(id = "foo")

String prefix

Prefix used for configuring the endpoint (see Endpoint Configuration)

@Endpoint(prefix = "foo")

boolean defaultEnabled

Sets whether the endpoint is enabled when no configuration is set (see Endpoint Configuration)

@Endpoint(defaultEnabled = false)

boolean defaultSensitive

Sets whether the endpoint is sensitive if no configuration is set (see Endpoint Configuration)

@Endpoint(defaultSensitive = false)

Example of custom Endpoint

The following example Endpoint class creates an endpoint accessible at /date:

CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint;

@Endpoint(id = "date",
          prefix = "custom",
          defaultEnabled = true,
          defaultSensitive = false)
public class CurrentDateEndpoint {

//.. endpoint methods

}
CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint

@Endpoint(id = "date",
          prefix = "custom",
          defaultEnabled = true,
          defaultSensitive = false)
class CurrentDateEndpoint {

//.. endpoint methods

}
CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint

@Endpoint(id = "date", prefix = "custom", defaultEnabled = true, defaultSensitive = false)
class CurrentDateEndpoint {

//.. endpoint methods

}

16.1.2 Endpoint Methods

Endpoints respond to GET ("read"), POST ("write") and DELETE ("delete") requests. To return a response from an endpoint, annotate its public method(s) with one of the following annotations:

Table 1. Endpoint Method Annotations
Annotation Description

Read

Responds to GET requests

Write

Responds to POST requests

Delete

Responds to DELETE requests

Read Methods

Annotating a method with the Read annotation causes it to respond to GET requests.

CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint;

import io.micronaut.management.endpoint.annotation.Read;

@Endpoint(id = "date",
          prefix = "custom",
          defaultEnabled = true,
          defaultSensitive = false)
public class CurrentDateEndpoint {

private Date currentDate;

@Read
public Date currentDate() {
    return currentDate;
}

}
CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint

import io.micronaut.management.endpoint.annotation.Read

@Endpoint(id = "date",
          prefix = "custom",
          defaultEnabled = true,
          defaultSensitive = false)
class CurrentDateEndpoint {

private Date currentDate

@Read
Date currentDate() {
    currentDate
}

}
CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint

import io.micronaut.management.endpoint.annotation.Read

@Endpoint(id = "date", prefix = "custom", defaultEnabled = true, defaultSensitive = false)
class CurrentDateEndpoint {

private var currentDate: Date? = null

@Read
fun currentDate(): Date? {
    return currentDate
}

}

The above method responds to the following request:

$ curl -X GET localhost:55838/date

1526085903689

The Read annotation accepts an optional produces argument, which sets the media type returned from the method (default is application/json):

CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint;

import io.micronaut.management.endpoint.annotation.Read;

import io.micronaut.http.MediaType;
import io.micronaut.management.endpoint.annotation.Selector;

@Endpoint(id = "date",
          prefix = "custom",
          defaultEnabled = true,
          defaultSensitive = false)
public class CurrentDateEndpoint {

private Date currentDate;

@Read(produces = MediaType.TEXT_PLAIN) //(1)
public String currentDatePrefix(@Selector String prefix) {
    return prefix + ": " + currentDate;
}

}
CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint

import io.micronaut.management.endpoint.annotation.Read

import io.micronaut.http.MediaType
import io.micronaut.management.endpoint.annotation.Selector

@Endpoint(id = "date",
          prefix = "custom",
          defaultEnabled = true,
          defaultSensitive = false)
class CurrentDateEndpoint {

private Date currentDate

@Read(produces = MediaType.TEXT_PLAIN) //(1)
String currentDatePrefix(@Selector String prefix) {
    "$prefix: $currentDate"
}

}
CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint

import io.micronaut.management.endpoint.annotation.Read

import io.micronaut.http.MediaType
import io.micronaut.management.endpoint.annotation.Selector

@Endpoint(id = "date", prefix = "custom", defaultEnabled = true, defaultSensitive = false)
class CurrentDateEndpoint {

private var currentDate: Date? = null

@Read(produces = [MediaType.TEXT_PLAIN]) //(1)
fun currentDatePrefix(@Selector prefix: String): String {
    return "$prefix: $currentDate"
}

}
1 Supported media types are represented by MediaType

The above method responds to the following request:

$ curl -X GET localhost:8080/date/the_date_is

the_date_is: Fri May 11 19:24:21 CDT

Write Methods

Annotating a method with the Write annotation causes it to respond to POST requests.

CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint;

import io.micronaut.management.endpoint.annotation.Write;

import io.micronaut.http.MediaType;
import io.micronaut.management.endpoint.annotation.Selector;

@Endpoint(id = "date",
          prefix = "custom",
          defaultEnabled = true,
          defaultSensitive = false)
public class CurrentDateEndpoint {

private Date currentDate;

@Write
public String reset() {
    currentDate = new Date();

    return "Current date reset";
}

}
CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint

import io.micronaut.management.endpoint.annotation.Write

import io.micronaut.http.MediaType
import io.micronaut.management.endpoint.annotation.Selector

@Endpoint(id = "date",
          prefix = "custom",
          defaultEnabled = true,
          defaultSensitive = false)
class CurrentDateEndpoint {

private Date currentDate

@Write
String reset() {
    currentDate = new Date()

    return "Current date reset"
}

}
CurrentDateEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint

import io.micronaut.management.endpoint.annotation.Write

import io.micronaut.http.MediaType
import io.micronaut.management.endpoint.annotation.Selector

@Endpoint(id = "date", prefix = "custom", defaultEnabled = true, defaultSensitive = false)
class CurrentDateEndpoint {

private var currentDate: Date? = null

@Write
fun reset(): String {
    currentDate = Date()

    return "Current date reset"
}

}

The above method responds to the following request:

$ curl -X POST http://localhost:39357/date

Current date reset

The Write annotation accepts an optional consumes argument, which sets the media type accepted by the method (default is application/json):

MessageEndpoint
import io.micronaut.context.annotation.Requires;
import io.micronaut.management.endpoint.annotation.Endpoint;

import io.micronaut.management.endpoint.annotation.Write;

import io.micronaut.http.MediaType;

@Endpoint(id = "message", defaultSensitive = false)
public class MessageEndpoint {

String message;

@Write(consumes = MediaType.APPLICATION_FORM_URLENCODED, produces = MediaType.TEXT_PLAIN)
public String updateMessage(String newMessage) {
    this.message = newMessage;

    return "Message updated";
}

}
MessageEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint

import io.micronaut.management.endpoint.annotation.Write

import io.micronaut.http.MediaType

@Endpoint(id = "message", defaultSensitive = false)
class MessageEndpoint {

String message

@Write(consumes = MediaType.APPLICATION_FORM_URLENCODED, produces = MediaType.TEXT_PLAIN)
String updateMessage(String newMessage) {  //(1)
    message = newMessage

    return "Message updated"
}

}
MessageEndpoint
import io.micronaut.context.annotation.Requires
import io.micronaut.management.endpoint.annotation.Endpoint

import io.micronaut.management.endpoint.annotation.Write

import io.micronaut.http.MediaType

@Endpoint(id = "message", defaultSensitive = false)
class MessageEndpoint {

internal var message: String? = null

@Write(consumes = [MediaType.APPLICATION_FORM_URLENCODED], produces = [MediaType.TEXT_PLAIN])
fun updateMessage(newMessage: String): String {  //(1)
    this.message = newMessage

    return "Message updated"
}

}

The above method responds to the following request:

$ curl -X POST http://localhost:65013/message -H 'Content-Type: application/x-www-form-urlencoded' -d $'newMessage=A new message'

Message updated

Delete Methods

Annotating a method with the Delete annotation causes it to respond to DELETE requests.

MessageEndpoint
import io.micronaut.context.annotation.Requires;
import io.micronaut.management.endpoint.annotation.Endpoint;

import io.micronaut.management.endpoint.annotation.Delete;

@Endpoint(id = "message", defaultSensitive = false)
public class MessageEndpoint {

String message;

@Delete
public String deleteMessage() {
    this.message = null;

    return "Message deleted";
}

}
MessageEndpoint
import io.micronaut.management.endpoint.annotation.Endpoint

import io.micronaut.management.endpoint.annotation.Delete

@Endpoint(id = "message", defaultSensitive = false)
class MessageEndpoint {

String message

@Delete
String deleteMessage() {
    message = null

    return "Message deleted"
}

}
MessageEndpoint
import io.micronaut.context.annotation.Requires
import io.micronaut.management.endpoint.annotation.Endpoint

import io.micronaut.management.endpoint.annotation.Delete

@Endpoint(id = "message", defaultSensitive = false)
class MessageEndpoint {

internal var message: String? = null

@Delete
fun deleteMessage(): String {
    this.message = null

    return "Message deleted"
}

}

The above method responds to the following request:

$ curl -X DELETE http://localhost:65013/message

Message deleted

16.1.3 Endpoint Sensitivity

Endpoint sensitivity can be controlled for the entire endpoint through the endpoint annotation and configuration. Individual methods can be configured independently of the endpoint as a whole, however. The @Sensitive annotation can be applied to methods to control their sensitivity.

AlertsEndpoint
@Endpoint(id = "alerts", defaultSensitive = false) // (1)
public class AlertsEndpoint {

    private final List<String> alerts = new CopyOnWriteArrayList<>();

    @Read
    List<String> getAlerts() {
        return alerts;
    }

    @Delete
    @Sensitive(true)  // (2)
    void clearAlerts() {
        alerts.clear();
    }

    @Write(consumes = MediaType.TEXT_PLAIN)
    @Sensitive(property = "add.sensitive", defaultValue = true) // (3)
    void addAlert(@Body String alert) {
        alerts.add(alert);
    }
}
AlertsEndpoint
import io.micronaut.http.annotation.Body
import io.micronaut.management.endpoint.annotation.Delete
import io.micronaut.management.endpoint.annotation.Endpoint
import io.micronaut.management.endpoint.annotation.Read
import io.micronaut.management.endpoint.annotation.Sensitive
import io.micronaut.management.endpoint.annotation.Write

import java.util.concurrent.CopyOnWriteArrayList


@Endpoint(id = "alerts", defaultSensitive = false) // (1)
class AlertsEndpoint {

    private final List<String> alerts = new CopyOnWriteArrayList<>();

    @Read
    List<String> getAlerts() {
        alerts
    }

    @Delete
    @Sensitive(true) // (2)
    void clearAlerts() {
        alerts.clear()
    }

    @Write(consumes = MediaType.TEXT_PLAIN)
    @Sensitive(property = "add.sensitive", defaultValue = true) // (3)
    void addAlert(@Body String alert) {
        alerts << alert
    }
}
AlertsEndpoint
import io.micronaut.context.annotation.Requires
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
import io.micronaut.management.endpoint.annotation.Delete
import io.micronaut.management.endpoint.annotation.Endpoint
import io.micronaut.management.endpoint.annotation.Read
import io.micronaut.management.endpoint.annotation.Sensitive
import io.micronaut.management.endpoint.annotation.Write
import java.util.concurrent.CopyOnWriteArrayList


@Endpoint(id = "alerts", defaultSensitive = false) // (1)
class AlertsEndpoint {

    private val alerts: MutableList<String> = CopyOnWriteArrayList()

    @Read
    fun getAlerts(): List<String> {
        return alerts
    }

    @Delete
    @Sensitive(true)  // (2)
    fun clearAlerts() {
        alerts.clear()
    }

    @Write(consumes = [MediaType.TEXT_PLAIN])
    @Sensitive(property = "add.sensitive", defaultValue = true)  // (3)
    fun addAlert(@Body alert: String) {
        alerts.add(alert)
    }
}
1 The endpoint is not sensitive by default, and the default prefix of endpoints is used.
2 This method is always sensitive, regardless of any other factors
3 The property value is appended to the prefix and id to look up a configuration value

If the configuration key endpoints.alerts.add.sensitive is set, that value determines the sensitivity of the addAlert method.

  1. endpoint is the first token because that is the default value for prefix in the endpoint annotation and is not set explicitly in this example.

  2. alerts is the next token because that is the endpoint id

  3. add.sensitive is the next token because that is the value set to the property member of the @Sensitive annotation.

If the configuration key is not set, the defaultValue is used (defaults to true).

16.1.4 Endpoint Configuration

Endpoints with the endpoints prefix can be configured through their default endpoint id. If an endpoint exists with the id of foo, it can be configured through endpoints.foo. In addition, default values can be provided through the all prefix.

For example, consider the following endpoint.

FooEndpoint.java
@Endpoint("foo")
class FooEndpoint {
    ...
}

By default, the endpoint is enabled. To disable it, set endpoints.foo.enabled to false. If endpoints.foo.enabled is not set and endpoints.all.enabled is false, the endpoint will be disabled.

The configuration values for the endpoint override those for all. If endpoints.foo.enabled is true and endpoints.all.enabled is false, the endpoint will be enabled.

For all endpoints, the following configuration values can be set.

endpoints.<any endpoint id>.enabled=Boolean
endpoints.<any endpoint id>.sensitive=Boolean
endpoints:
  <any endpoint id>:
    enabled: Boolean
    sensitive: Boolean
[endpoints]
  sensitive="Boolean"
endpoints {
  <any endpoint id> {
    enabled = "Boolean"
    sensitive = "Boolean"
  }
}
{
  endpoints {
    <any endpoint id> {
      enabled = "Boolean"
      sensitive = "Boolean"
    }
  }
}
{
  "endpoints": {
    "<any endpoint id>": {
      "enabled": "Boolean",
      "sensitive": "Boolean"
    }
  }
}
The base path for all endpoints is / by default. If you prefer the endpoints to be available under a different base path, configure endpoints.all.path. For example, if the value is set to /endpoints/, the foo endpoint will be accessible at /endpoints/foo, relative to the context path. Note that the leading and trailing / are required for endpoints.all.path unless micronaut.server.context-path is set, in which case the leading / isn’t necessary.

16.2 Built-In Endpoints

When the management dependency is added to your project, the following built-in endpoints are enabled by default:

Table 1. Default Endpoints
Endpoint URI Description

BeansEndpoint

/beans

Returns information about the loaded bean definitions in the application (see BeansEndpoint)

HealthEndpoint

/health

Returns information about the "health" of the application (see HealthEndpoint)

InfoEndpoint

/info

Returns static information from the state of the application (see InfoEndpoint)

LoggersEndpoint

/loggers

Returns information about available loggers and permits changing the configured log level (see LoggersEndpoint)

MetricsEndpoint

/metrics

Return the application metrics. Requires the micrometer-core configuration on the classpath.

RefreshEndpoint

/refresh

Refreshes the application state (see RefreshEndpoint)

RoutesEndpoint

/routes

Returns information about URIs available to be called for your application (see RoutesEndpoint)

ThreadDumpEndpoint

/threaddump

Returns information about the current threads in the application.

In addition, the following built-in endpoint(s) are provided by the management dependency but are not enabled by default:

Table 2. Disabled Endpoints
Endpoint URI Description

EnvironmentEndpoint

/env

Returns information about the environment and its property sources (see EnvironmentEndpoint)

CachesEndpoint

/caches

Returns information about the caches and permits invalidating them (see CachesEndpoint)

ServerStopEndpoint

/stop

Shuts down the application server (see ServerStopEndpoint)

It is possible to open all endpoints for unauthenticated access defining endpoints.all.sensitive: false but this should be used with care because private and sensitive information will be exposed.

Management Port

By default, all management endpoints are exposed over the same port as the application. You can alter this behaviour by specifying the endpoints.all.port setting:

endpoints.all.port=8085
endpoints:
  all:
    port: 8085
[endpoints]
  [endpoints.all]
    port=8085
endpoints {
  all {
    port = 8085
  }
}
{
  endpoints {
    all {
      port = 8085
    }
  }
}
{
  "endpoints": {
    "all": {
      "port": 8085
    }
  }
}

In the above example the management endpoints are exposed only over port 8085.

JMX

The Micronaut framework provides functionality to register endpoints with JMX. See the section on JMX to get started.

16.2.1 The Beans Endpoint

The beans endpoint returns information about the loaded bean definitions in the application. The bean data returned by default is an object where the key is the bean definition class name and the value is an object of properties about the bean.

To execute the beans endpoint, send a GET request to /beans.

Configuration

To configure the beans endpoint, supply configuration through endpoints.beans.

Beans Endpoint Configuration Example
endpoints.beans.enabled=Boolean
endpoints.beans.sensitive=Boolean
endpoints:
  beans:
    enabled: Boolean
    sensitive: Boolean
[endpoints]
  [endpoints.beans]
    enabled="Boolean"
    sensitive="Boolean"
endpoints {
  beans {
    enabled = "Boolean"
    sensitive = "Boolean"
  }
}
{
  endpoints {
    beans {
      enabled = "Boolean"
      sensitive = "Boolean"
    }
  }
}
{
  "endpoints": {
    "beans": {
      "enabled": "Boolean",
      "sensitive": "Boolean"
    }
  }
}

Customization

The beans endpoint is composed of a bean definition data collector and a bean data implementation. The bean definition data collector (BeanDefinitionDataCollector) is responsible for returning a publisher that returns the data used in the response. The bean definition data (BeanDefinitionData) is responsible for returning data about an individual bean definition.

To override the default behavior for either of the helper classes, either extend the default implementations (DefaultBeanDefinitionDataCollector, DefaultBeanDefinitionData), or implement the relevant interface directly. To ensure your implementation is used instead of the default, add the @Replaces annotation to your class with the value being the default implementation.

16.2.2 The Info Endpoint

The info endpoint returns static information from the state of the application. The info exposed can be provided by any number of "info sources".

To execute the info endpoint, send a GET request to /info.

Configuration

To configure the info endpoint, supply configuration through endpoints.info.

Info Endpoint Configuration Example
endpoints.info.enabled=Boolean
endpoints.info.sensitive=Boolean
endpoints:
  info:
    enabled: Boolean
    sensitive: Boolean
[endpoints]
  [endpoints.info]
    enabled="Boolean"
    sensitive="Boolean"
endpoints {
  info {
    enabled = "Boolean"
    sensitive = "Boolean"
  }
}
{
  endpoints {
    info {
      enabled = "Boolean"
      sensitive = "Boolean"
    }
  }
}
{
  "endpoints": {
    "info": {
      "enabled": "Boolean",
      "sensitive": "Boolean"
    }
  }
}

Customization

The info endpoint consists of an info aggregator and any number of info sources. To add an info source, create a bean class that implements InfoSource. If your info source needs to retrieve data from Java properties files, extend the PropertiesInfoSource interface which provides a helper method for this purpose.

All info source beans are collected together with the info aggregator. To provide your own implementation of the info aggregator, create a class that implements InfoAggregator and register it as a bean. To ensure your implementation is used instead of the default, add the @Replaces annotation to your class with the value being the default implementation.

The default info aggregator returns a map containing the combined properties returned by all the info sources. This map is returned as JSON from the /info endpoint.

Provided Info Sources

Configuration Info Source

The ConfigurationInfoSource returns configuration properties under the info key. In addition to string, integer and boolean values, more complex properties can be exposed as maps in the JSON output (if the configuration format supports it).

Info Source Example (application.groovy)
info.demo.string = "demo string"
info.demo.number = 123
info.demo.map = [key: 'value', other_key: 123]

The above config results in the following JSON response from the info endpoint:

{
  "demo": {
    "string": "demo string",
    "number": 123,
    "map": {
      "key": "value",
      "other_key": 123
    }
  }
}

Configuration

The configuration info source can be disabled using the endpoints.info.config.enabled property.

Git Info Source

If a git.properties file is available on the classpath, the GitInfoSource exposes the values in that file under the git key. Generating of a git.properties file must be configured as part of your build. One easy option for Gradle users is the Gradle Git Properties Plugin. Maven users can use the Maven Git Commit ID Plugin.

Configuration

To specify an alternate path or name of the properties file, supply a custom value in the endpoints.info.git.location property.

The git info source can be disabled using the endpoints.info.git.enabled property.

Build Info Source

If a META-INF/build-info.properties file is available on the classpath, the BuildInfoSource exposes the values in that file under the build key. Generating a build-info.properties file must be configured as part of your build. One easy option for Gradle users is the Gradle Build Info Plugin. An option for Maven users is the Spring Boot Maven Plugin

Configuration

To specify an alternate path/name of the properties file, supply a custom value in the endpoints.info.build.location property.

The build info source can be disabled using the endpoints.info.build.enabled property.

16.2.3 The Health Endpoint

The health endpoint returns information about the "health" of the application, which is determined by any number of "health indicators".

To execute the health endpoint, send a GET request to /health. Additionally, the health endpoint exposes /health/liveness and /health/readiness health indicators.

Configuration

To configure the health endpoint, supply configuration through endpoints.health.

Health Endpoint Configuration Example
endpoints.health.enabled=Boolean
endpoints.health.sensitive=Boolean
endpoints.health.details-visible=String
endpoints.health.status.http-mapping=Map<String, HttpStatus>
endpoints:
  health:
    enabled: Boolean
    sensitive: Boolean
    details-visible: String
    status:
      http-mapping: Map<String, HttpStatus>
[endpoints]
  [endpoints.health]
    enabled="Boolean"
    sensitive="Boolean"
    details-visible="String"
    [endpoints.health.status]
      http-mapping="Map<String, HttpStatus>"
endpoints {
  health {
    enabled = "Boolean"
    sensitive = "Boolean"
    detailsVisible = "String"
    status {
      httpMapping = "Map<String, HttpStatus>"
    }
  }
}
{
  endpoints {
    health {
      enabled = "Boolean"
      sensitive = "Boolean"
      details-visible = "String"
      status {
        http-mapping = "Map<String, HttpStatus>"
      }
    }
  }
}
{
  "endpoints": {
    "health": {
      "enabled": "Boolean",
      "sensitive": "Boolean",
      "details-visible": "String",
      "status": {
        "http-mapping": "Map<String, HttpStatus>"
      }
    }
  }
}

The details-visible setting controls whether health detail will be exposed to users who are not authenticated.

For example, setting:

Using details-visible
endpoints.health.details-visible=ANONYMOUS
endpoints:
  health:
    details-visible: ANONYMOUS
[endpoints]
  [endpoints.health]
    details-visible="ANONYMOUS"
endpoints {
  health {
    detailsVisible = "ANONYMOUS"
  }
}
{
  endpoints {
    health {
      details-visible = "ANONYMOUS"
    }
  }
}
{
  "endpoints": {
    "health": {
      "details-visible": "ANONYMOUS"
    }
  }
}

exposes detailed information from the various health indicators about the health status of the application to anonymous unauthenticated users.

The endpoints.health.status.http-mapping setting controls which status codes to return for each health status. The defaults are described in the table below:

Status HTTP Code

UP

OK (200)

UNKNOWN

OK (200)

DOWN

SERVICE_UNAVAILABLE (503)

You can provide custom mappings in your configuration file (e.g application.yml):

Custom Health Status Codes
endpoints.health.status.http-mapping.DOWN=200
endpoints:
  health:
    status:
      http-mapping:
        DOWN: 200
[endpoints]
  [endpoints.health]
    [endpoints.health.status]
      [endpoints.health.status.http-mapping]
        DOWN=200
endpoints {
  health {
    status {
      httpMapping {
        DOWN = 200
      }
    }
  }
}
{
  endpoints {
    health {
      status {
        http-mapping {
          DOWN = 200
        }
      }
    }
  }
}
{
  "endpoints": {
    "health": {
      "status": {
        "http-mapping": {
          "DOWN": 200
        }
      }
    }
  }
}

The above returns OK (200) even when the HealthStatus is DOWN.

Customization

The health endpoint consists of a health aggregator and any number of health indicators. To add a health indicator, create a bean class that implements HealthIndicator. It is recommended to also use either @Liveness or @Readiness qualifier. If no qualifier is used, the health indicator will be part of /health and /health/readiness endpoints. A base class AbstractHealthIndicator is available to subclass to make the process easier.

All health indicator beans are collected together with the health aggregator. To provide your own implementation of the health aggregator, create a class that implements HealthAggregator and register it as a bean. To ensure your implementation is used instead of the default, add the @Replaces annotation to your class with the value being the default implementation DefaultHealthAggregator.

The default health aggregator returns an overall status calculated based on the health statuses of the indicators. A health status consists of several pieces of information.

Name

The name of the status

Description

The description of the status

Operational

Whether the functionality the indicator represents is functional

Severity

How severe the status is. A higher number is more severe

The "worst" status is returned as the overall status. A non-operational status is selected over an operational status. A higher severity is selected over a lower severity.

The DefaultHealthAggregator also emits log statements for health indicator status and details. To log this information use Level.DEBUG for just health indicator status or use Level.TRACE for both status and details. For example:

<logger name="io.micronaut.management.health.aggregator.DefaultHealthAggregator" level="trace" /> (1)
1 Use level="debug" for health indicator status, use level="trace" to add health indicator details

Health Monitor Task

A continuous health monitor that updates the CurrentHealthStatus in a background thread can be enabled when using EmbeddedServer with the following application configuration:

micronaut.application.name=foo
micronaut.health.monitor.enabled=true
micronaut:
  application:
    name: foo
  health:
    monitor:
      enabled: true
[micronaut]
  [micronaut.application]
    name="foo"
  [micronaut.health]
    [micronaut.health.monitor]
      enabled=true
micronaut {
  application {
    name = "foo"
  }
  health {
    monitor {
      enabled = true
    }
  }
}
{
  micronaut {
    application {
      name = "foo"
    }
    health {
      monitor {
        enabled = true
      }
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "foo"
    },
    "health": {
      "monitor": {
        "enabled": true
      }
    }
  }
}
  • Both configuration properties are required to enable the monitor background task.

Similarly to DefaultHealthAggregator it also emits log statements for health indicator status and details. To log this use the following logger configuration:

<logger name="io.micronaut.management.health.monitor.HealthMonitorTask" level="trace" />

Provided Indicators

All the Micronaut framework provided health indicators are exposed on /health and /health/readiness endpoints.

Disk Space

A health indicator is provided that determines the health of the application based on the amount of free disk space. Configuration for the disk space health indicator can be provided under the endpoints.health.disk-space key.

Disk Space Indicator Configuration Example
endpoints.health.disk-space.enabled=Boolean
endpoints.health.disk-space.path=String
endpoints.health.disk-space.threshold=String | Long
endpoints:
  health:
    disk-space:
      enabled: Boolean
      path: String
      threshold: String | Long
[endpoints]
  [endpoints.health]
    [endpoints.health.disk-space]
      enabled="Boolean"
      path="String"
      threshold="String | Long"
endpoints {
  health {
    diskSpace {
      enabled = "Boolean"
      path = "String"
      threshold = "String | Long"
    }
  }
}
{
  endpoints {
    health {
      disk-space {
        enabled = "Boolean"
        path = "String"
        threshold = "String | Long"
      }
    }
  }
}
{
  "endpoints": {
    "health": {
      "disk-space": {
        "enabled": "Boolean",
        "path": "String",
        "threshold": "String | Long"
      }
    }
  }
}
  • path specifies the path used to determine the disk space

  • threshold specifies the minimum amount of free space

The threshold can be provided as a string like "10MB" or "200KB", or the number of bytes.

JDBC

The JDBC health indicator determines the health of your application based on the ability to successfully create connections to datasources in the application context. The only configuration option supported is to enable or disable the indicator by the endpoints.health.jdbc.enabled key.

Discovery Client

If your application uses service discovery, a health indicator is included to monitor the health of the discovery client. The data returned can include a list of the services available.

16.2.4 The Metrics Endpoint

The Micronaut framework can expose application metrics via integration with Micrometer.

Using the CLI

If you create your project using the Micronaut CLI, supply one of the micrometer features to enable metrics and preconfigure the selected registry in your project. For example:

$ mn create-app my-app --features micrometer-atlas

The metrics endpoint returns information about the "metrics" of the application. To execute the metrics endpoint, send a GET request to /metrics. This returns a list of available metric names.

You can get specific metrics by using /metrics/[name] such as /metrics/jvm.memory.used.

See the documentation for Micronaut Micrometer for a list of registries and information on how to configure, expose and customize metrics output.

16.2.5 The Refresh Endpoint

The refresh endpoint refreshes the application state, causing all Refreshable beans in the context to be destroyed and reinstantiated upon further requests. This is accomplished by publishing a RefreshEvent in the Application Context.

To execute the refresh endpoint, send a POST request to /refresh.

$ curl -X POST http://localhost:8080/refresh

When executed without a body, the endpoint first refreshes the Environment and performs a diff to detect any changes, and then only performs the refresh if changes are detected. To skip this check and refresh all @Refreshable beans regardless of environment changes (e.g., to force refresh of cached responses from third-party services), add a force parameter in the POST request body.

$ curl -X POST http://localhost:8080/refresh -H 'Content-Type: application/json' -d '{"force": true}'

Configuration

To configure the refresh endpoint, supply configuration through endpoints.refresh.

Beans Endpoint Configuration Example
endpoints.refresh.enabled=Boolean
endpoints.refresh.sensitive=Boolean
endpoints:
  refresh:
    enabled: Boolean
    sensitive: Boolean
[endpoints]
  [endpoints.refresh]
    enabled="Boolean"
    sensitive="Boolean"
endpoints {
  refresh {
    enabled = "Boolean"
    sensitive = "Boolean"
  }
}
{
  endpoints {
    refresh {
      enabled = "Boolean"
      sensitive = "Boolean"
    }
  }
}
{
  "endpoints": {
    "refresh": {
      "enabled": "Boolean",
      "sensitive": "Boolean"
    }
  }
}

16.2.6 The Routes Endpoint

The routes endpoint returns information about URIs available to be called for your application. By default, the data returned includes the URI, allowed method, content types produced, and information about the method that would be executed.

To execute the routes endpoint, send a GET request to /routes.

Configuration

To configure the routes endpoint, supply configuration through endpoints.routes.

Routes Endpoint Configuration Example
endpoints.routes.enabled=Boolean
endpoints.routes.sensitive=Boolean
endpoints:
  routes:
    enabled: Boolean
    sensitive: Boolean
[endpoints]
  [endpoints.routes]
    enabled="Boolean"
    sensitive="Boolean"
endpoints {
  routes {
    enabled = "Boolean"
    sensitive = "Boolean"
  }
}
{
  endpoints {
    routes {
      enabled = "Boolean"
      sensitive = "Boolean"
    }
  }
}
{
  "endpoints": {
    "routes": {
      "enabled": "Boolean",
      "sensitive": "Boolean"
    }
  }
}

Customization

The routes endpoint is composed of a route data collector and a route data implementation. The route data collector (RouteDataCollector) is responsible for returning a publisher that returns the data used in the response. The route data (RouteData) is responsible for returning data about an individual route.

To override the default behavior for either of the helper classes, either extend the default implementations (DefaultRouteDataCollector, DefaultRouteData), or implement the relevant interface directly. To ensure your implementation is used instead of the default, add the @Replaces annotation to your class with the value being the default implementation.

16.2.7 The Loggers Endpoint

The loggers endpoint returns information about the available loggers in the application and permits configuring their log level.

The loggers endpoint is disabled by default and must be explicitly enabled with the setting endpoints.loggers.enabled=true.

To get a collection of all loggers by name with their configured and effective log levels, send a GET request to /loggers. This also provides a list of the available log levels.

$ curl http://localhost:8080/loggers

{
    "levels": [
        "ALL", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF", "NOT_SPECIFIED"
    ],
    "loggers": {
        "ROOT": {
            "configuredLevel": "INFO",
            "effectiveLevel": "INFO"
        },
        "io": {
            "configuredLevel": "NOT_SPECIFIED",
            "effectiveLevel": "INFO"
        },
        "io.micronaut": {
            "configuredLevel": "NOT_SPECIFIED",
            "effectiveLevel": "INFO"
        },
        // etc...
    }
}

To get the log levels of a particular logger, include the logger name in your GET request. For example, to access the log levels of the logger 'io.micronaut.http':

$ curl http://localhost:8080/loggers/io.micronaut.http

{
    "configuredLevel": "NOT_SPECIFIED",
    "effectiveLevel": "INFO"
}

If the named logger does not exist, it is created with an unspecified (i.e. NOT_SPECIFIED) configured log level (its effective log level is usually that of the root logger).

To update the log level of a single logger, send a POST request to the named logger URL and include a body providing the log level to configure.

$ curl -i -X POST \
       -H "Content-Type: application/json" \
       -d '{ "configuredLevel": "ERROR" }' \
       http://localhost:8080/loggers/ROOT

HTTP/1.1 200 OK

$ curl http://localhost:8080/loggers/ROOT

{
    "configuredLevel": "ERROR",
    "effectiveLevel": "ERROR"
}

Configuration

To configure the loggers endpoint, supply configuration through endpoints.loggers.

Loggers Endpoint Configuration Example
endpoints.loggers.enabled=Boolean
endpoints.loggers.sensitive=Boolean
endpoints:
  loggers:
    enabled: Boolean
    sensitive: Boolean
[endpoints]
  [endpoints.loggers]
    enabled="Boolean"
    sensitive="Boolean"
endpoints {
  loggers {
    enabled = "Boolean"
    sensitive = "Boolean"
  }
}
{
  endpoints {
    loggers {
      enabled = "Boolean"
      sensitive = "Boolean"
    }
  }
}
{
  "endpoints": {
    "loggers": {
      "enabled": "Boolean",
      "sensitive": "Boolean"
    }
  }
}
By default, the endpoint doesn’t allow changing the log level by unauthorized users (even if sensitive is set to false). To allow this you must set endpoints.loggers.write-sensitive to false.

Customization

The loggers endpoint is composed of two customizable parts: a LoggersManager and a LoggingSystem. See the logging section of the documentation for information on customizing the logging system.

The LoggersManager is responsible for retrieving and setting log levels. If the default implementation is not sufficient for your use case, simply provide your own implementation and replace the DefaultLoggersManager with the @Replaces annotation.

16.2.8 The Caches Endpoint

The caches endpoint documentation is available at the micronaut-cache project.

16.2.9 The Server Stop Endpoint

The stop endpoint shuts down the application server.

To execute the stop endpoint, send a POST request to /stop.

Configuration

To configure the stop endpoint, supply configuration through endpoints.stop.

Stop Endpoint Configuration Example
endpoints.stop.enabled=Boolean
endpoints.stop.sensitive=Boolean
endpoints:
  stop:
    enabled: Boolean
    sensitive: Boolean
[endpoints]
  [endpoints.stop]
    enabled="Boolean"
    sensitive="Boolean"
endpoints {
  stop {
    enabled = "Boolean"
    sensitive = "Boolean"
  }
}
{
  endpoints {
    stop {
      enabled = "Boolean"
      sensitive = "Boolean"
    }
  }
}
{
  "endpoints": {
    "stop": {
      "enabled": "Boolean",
      "sensitive": "Boolean"
    }
  }
}
By default, the stop endpoint is disabled and must be explicitly enabled to be used.

16.2.10 The Environment Endpoint

The environment endpoint returns information about the Environment and its PropertySources.

Configuration

To enable and configure the environment endpoint, supply configuration through endpoints.env.

Environment Endpoint Configuration Example
endpoints.env.enabled=Boolean
endpoints.env.sensitive=Boolean
endpoints:
  env:
    enabled: Boolean
    sensitive: Boolean
[endpoints]
  [endpoints.env]
    enabled="Boolean"
    sensitive="Boolean"
endpoints {
  env {
    enabled = "Boolean"
    sensitive = "Boolean"
  }
}
{
  endpoints {
    env {
      enabled = "Boolean"
      sensitive = "Boolean"
    }
  }
}
{
  "endpoints": {
    "env": {
      "enabled": "Boolean",
      "sensitive": "Boolean"
    }
  }
}
  • defaults are false for enabled and true for sensitive

By default, the endpoint will mask all values. To customize this masking you need to supply a Bean that implements EnvironmentEndpointFilter.

This first example will mask all values except for those that are prefixed by safe

First example of environment masking
@Singleton
public class OnlySafePrefixedEnvFilter implements EnvironmentEndpointFilter {
    private static final Pattern SAFE_PREFIX_PATTERN = Pattern.compile("safe.*", Pattern.CASE_INSENSITIVE);

    @Override
    public void specifyFiltering(@NotNull EnvironmentFilterSpecification specification) {
        specification
                .maskAll() // All values will be masked apart from the supplied patterns
                .exclude(SAFE_PREFIX_PATTERN);
    }
}

It is also possible to allow all values in plain text using maskNone--, and then specify name patterns that will be masked, ie:

Deny instead of allow
@Singleton
public class AllPlainExceptSecretOrMatchEnvFilter implements EnvironmentEndpointFilter {
    // Mask anything starting with `sekrt`
    private static final Pattern SECRET_PREFIX_PATTERN = Pattern.compile("sekrt.*", Pattern.CASE_INSENSITIVE);

    // Mask anything exactly matching `exact-match`
    private static final String EXACT_MATCH = "exact-match";

    // Mask anything that starts with `private.`
    private static final Predicate<String> PREDICATE_MATCH = name -> name.startsWith("private.");

    @Override
    public void specifyFiltering(@NotNull EnvironmentFilterSpecification specification) {
        specification
                .maskNone() // All values will be in plain-text apart from the supplied patterns
                .exclude(SECRET_PREFIX_PATTERN)
                .exclude(EXACT_MATCH)
                .exclude(PREDICATE_MATCH);
    }
}

Sensible defaults can be applied by calling the legacyMasking-- method. This will show all values apart from those that contain the words password, credential, certificate, key, secret or token anywhere in their name.

Getting information about the environment

To execute the endpoint, send a GET request to /env.

Getting information about a particular PropertySource

To execute the endpoint, send a GET request to /env/{propertySourceName}.

16.2.11 The ThreadDump Endpoint

The threaddump endpoint returns information about the threads running in your application.

To execute the threaddump endpoint, send a GET request to /threaddump.

Configuration

To configure the threaddump endpoint, supply configuration through endpoints.threaddump.

Threaddump Endpoint Configuration Example
endpoints.threaddump.enabled=Boolean
endpoints.threaddump.sensitive=Boolean
endpoints:
  threaddump:
    enabled: Boolean
    sensitive: Boolean
[endpoints]
  [endpoints.threaddump]
    enabled="Boolean"
    sensitive="Boolean"
endpoints {
  threaddump {
    enabled = "Boolean"
    sensitive = "Boolean"
  }
}
{
  endpoints {
    threaddump {
      enabled = "Boolean"
      sensitive = "Boolean"
    }
  }
}
{
  "endpoints": {
    "threaddump": {
      "enabled": "Boolean",
      "sensitive": "Boolean"
    }
  }
}

Customization

The thread dump endpoint delegates to a ThreadInfoMapper) that is responsible for transforming the java.lang.management.ThreadInfo objects into any other to be sent for serialization.

17 Security

The Micronaut framework has a full-featured security solution for all common security patterns.

See the documentation for Micronaut Security for more information on how to secure your applications.

18 Multi-Tenancy

See the Micronaut Multitenancy documentation to learn about Micronaut’s support for common tasks such as tenant resolution for multi-tenancy-aware Micronaut applications.

19 Micronaut CLI

The Micronaut CLI is the recommended way to create new Micronaut projects. The CLI includes commands for generating specific categories of projects, allowing you to choose between build tools, test frameworks, and even pick the language to use in your application. The CLI also provides commands for generating artifacts such as controllers, client interfaces, and serverless functions.

We have a website that can be used to generate projects instead of the CLI. Check out Micronaut Launch to get started!

When Micronaut framework is installed on your computer, you can call the CLI with the mn command.

$ mn create-app my-app

A Micronaut framework CLI project can be identified by the micronaut-cli.yml file, which is included at the project root if it was generated via the CLI. This file will include the project’s profile, default package, and other variables. The project’s default package is evaluated based on the project name.

$ mn create-app my-demo-app

results in the default package being my.demo.app.

You can supply your own default package when creating the application by prefixing the application name with the package:

$ mn create-app example.my-demo-app

results in the default package being example.

Interactive Mode

If you run mn without any arguments, the Micronaut CLI launches in interactive mode. This is a shell-like mode which lets you run multiple CLI commands without re-initializing the CLI runtime, and is especially suitable when you use code-generation commands (such as create-controller), create multiple projects, or are just exploring CLI features. Tab-completion is enabled, enabling you to hit the TAB key to see possible options for a given command or flag.

$ mn
| Starting interactive mode...
| Enter a command name to run. Use TAB for completion:
mn>

Help and Info

General usage information can be viewed using the help flag on a command.

mn> create-app -h
Usage: mn create-app [-hivVx] [--list-features] [-b=BUILD-TOOL] [--jdk=<javaVersion>] [-l=LANG]
                     [-t=TEST] [-f=FEATURE[,FEATURE...]]... [NAME]
Creates an application
      [NAME]               The name of the application to create.
  -b, --build=BUILD-TOOL   Which build tool to configure. Possible values: gradle, gradle_kotlin,
                             maven.
  -f, --features=FEATURE[,FEATURE...]
  -h, --help               Show this help message and exit.
  -i, --inplace            Create a service using the current directory
      --jdk, --java-version=<javaVersion>
                           The JDK version the project should target
  -l, --lang=LANG          Which language to use. Possible values: java, groovy, kotlin.
      --list-features      Output the available features and their descriptions
  -t, --test=TEST          Which test framework to use. Possible values: junit, spock, kotest.

A list of available features can be viewed using the --list-features flag on any of the create commands.

mn> create-app --list-features
Available Features
(+) denotes the feature is included by default
  Name                             Description
  -------------------------------  ---------------
  Cache
  cache-caffeine                   Adds support for cache using Caffeine (https://github.com/ben-manes/caffeine)
  cache-ehcache                    Adds support for cache using EHCache (https://www.ehcache.org/)
  cache-hazelcast                  Adds support for cache using Hazelcast (https://hazelcast.org/)
  cache-infinispan                 Adds support for cache using Infinispan (https://infinispan.org/)

19.1 Creating a Project

Creating a project is the primary usage of the CLI. The primary command for creating a new project is create-app, which creates a standard server application that communicates over HTTP. For other types of application, see the documentation below.

Table 1. Micronaut CLI Project Creation Commands
Command Description Options Example

create-app

Creates a basic Micronaut application.

  • -l, --lang

  • -t, --test

  • -b, --build

  • -f, --features

  • -i, --inplace

mn create-app my-project --features
 mongo-reactive,security-jwt --build maven

create-cli-app

Creates a command-line Micronaut application.

  • -l, --lang

  • -t, --test

  • -b, --build

  • -f, --features

  • -i, --inplace

mn create-cli-app my-project --features
 http-client,jdbc-hikari --build maven
 --lang kotlin --test kotest

create-function-app

Creates a Micronaut serverless function, using AWS by default.

  • -l, --lang

  • -t, --test

  • -b, --build

  • -f, --features

  • -i, --inplace

mn create-function-app my-lambda-function
 --lang groovy --test spock

create-messaging-app

Creates a Micronaut application that only communicates via a messaging protocol. Uses Kafka by default but can be switched to RabbitMQ with --features rabbitmq.

  • -l, --lang

  • -t, --test

  • -b, --build

  • -f, --features

  • -i, --inplace

mn create-messaging-app my-broker
 --lang groovy --test spock

create-grpc-app

Creates a Micronaut application that uses gRPC.

  • -l, --lang

  • -t, --test

  • -b, --build

  • -f, --features

  • -i, --inplace

mn create-grpc-app my-grpc-app
 --lang groovy --test spock

Create Command Flags

The create-* commands generate a basic Micronaut project, with optional flags to specify features, language, test framework, and build tool. All projects except functions include a default Application class for starting the application.

Table 2. Flags
Flag Description Example

-l, --lang

Language to use for the project (one of java, groovy, kotlin - default is java)

--lang groovy

-t, --test

Test framework to use for the project (one of junit, spock - default is junit)

--test spock

-b, --build

Build tool (one of gradle, gradle_kotlin, maven - default is gradle for the languages java and groovy; default is gradle_kotlin for language kotlin)

--build maven

-f, --features

Features to use for the project, comma-separated

--features security-jwt,mongo-gorm

or

-f security-jwt -f mongo-gorm

-i, --inplace

If present, generates the project in the current directory (project name is optional if this flag is set)

--inplace

Once created, the application can be started using the Application class, or the appropriate build tool task.

Starting a Gradle project
$ ./gradlew run
Starting a Maven project
$ ./mvnw mn:run

Language/Test Features

By default, the create commands generate a Java application, with JUnit configured as the test framework. All the options chosen and features applied are stored as properties in the micronaut-cli.yml file, as shown below:

micronaut-cli.yml
applicationType: default
defaultPackage: com.example
testFramework: junit
sourceLanguage: java
buildTool: gradle
features: [annotation-api, app-name, application, gradle, http-client, java, junit, logback, netty-server, shade, yaml]

Some commands rely on the data in this file to determine if they should be executable. For example, the create-kafka-listener command requires kafka to be one of the features in the list.

The values in micronaut-cli.yml are used by the CLI for code generation. After a project is generated, you can edit these values to change the project defaults, however you must supply the required dependencies and/or configuration to use your chosen language/framework. For example, you could change the testFramework property to spock to cause the CLI to generate Spock tests when running commands (such as create-controller), but you need to add the Spock dependency to your build.

Groovy

To create an app with Groovy support (which uses Spock by default), supply the appropriate language via the lang flag:

$ mn create-app my-groovy-app --lang groovy

This includes the Groovy and Spock dependencies in your project, and writes the appropriates values in micronaut-cli.yml.

Kotlin

To create an app with Kotlin support (which uses Kotest by default), supply the appropriate language via the lang flag:

$ mn create-app my-kotlin-app --lang kotlin

This includes the Kotlin and Kotest dependencies in your project, and writes the appropriates values in micronaut-cli.yml.

Build Tool

By default, create-app creates a Gradle project, with a build.gradle file in the project root directory. To create an app using the Maven build tool, supply the appropriate option via the build flag:

$ mn create-app my-maven-app --build maven

Create-Cli-App

The create-cli-app command generates a Micronaut command line application project, with optional flags to specify language, test framework, features, profile, and build tool. By default, the project includes the picocli feature to support command line option parsing. The project will include a *Command class (based on the project name, e.g. hello-world generates HelloWorldCommand), and an associated test which instantiates the command and verifies that it can parse command line options.

Once created, the application can be started using the *Command class, or the appropriate build tool task.

Starting a Gradle project
$ ./gradlew run
Starting a Maven project
$ ./mvnw mn:run

Create Function App

The create-function-app command generates a Micronaut function project, optimized for serverless environments, with optional flags to specify language, test framework, features, and build tool. The project will include a *Function class (based on the project name, e.g. hello-world generates HelloWorldFunction), and an associated test which instantiates the function and verifies that it can receive requests.

Currently, AWS Lambda, Micronaut Azure, and Google Cloud are the supported cloud providers for Micronaut functions. To use other providers, add one in the features: --features azure-function or --features google-cloud-function.

Contribute

The CLI source code is at https://github.com/micronaut-projects/micronaut-starter. Information about how to contribute and other resources are there.

19.1.1 Comparing Versions

The easiest way to see version dependency updates and other changes for a new version of Micronaut is to produce one clean application using the older version and another using the newer version of the mn CLI, and then comparing those directories.

19.2 Features

Features consist of additional dependencies and configuration to enable specific functionality in your application. Micronaut profiles define a large number of features, including features for many of the configurations provided by Micronaut, such as the Data Access Configurations

$ mn create-app my-demo-app --features mongo-reactive

This adds the necessary dependencies and configuration for the MongoDB Reactive Driver in your application. You can view the available features using the --list-features flag for whichever create command you use.

$ mn create-app --list-features # Output will be supported features for the create-app command
$ mn create-function-app --list-features # Output will be supported features for the create-function-app command, different from above.

19.3 Commands

You can view a full list of available commands using the help flag, for example:

$ mn -h
Usage: mn [-hvVx] [COMMAND]
Micronaut CLI command line interface for generating projects and services.
Application generation commands are: (1)

*  create-app NAME
*  create-cli-app NAME
*  create-function-app NAME
*  create-grpc-app NAME
*  create-messaging-app NAME

Options:
  -h, --help         Show this help message and exit.
  -v, --verbose      Create verbose output.
  -V, --version      Print version information and exit.
  -x, --stacktrace   Show full stack trace when exceptions occur.

Commands: (2)
  create-app               Creates an application
  create-cli-app           Creates a CLI application
  create-function-app      Creates a Cloud Function
  create-grpc-app          Creates a gRPC application
  create-messaging-app     Creates a messaging application
  create-job               Creates a job with scheduled method
  create-bean              Creates a singleton bean
  create-websocket-client  Creates a Websocket client
  create-client            Creates a client interface
  create-controller        Creates a controller and associated test
  feature-diff             Produces the diff of an original project with an original project with
                             additional features.
  create-websocket-server  Creates a Websocket server
  create-test              Creates a simple test for the project's testing framework
1 Here you can see the project generation commands lists
2 All commands available in the current directory are listed here
3 Note: the things listed after the project creation commands (always available) depend on the current directory context

All the code-generation commands honor the values written in micronaut-cli.yml. For example, assume the following micronaut-cli.yml file.

micronaut-cli.yml
defaultPackage: example
---
testFramework: spock
sourceLanguage: java

With the above settings, the create-bean command (by default) generates a Java class with an associated Spock test, in the example package. Commands accept arguments and these defaults can be overridden on a per-command basis.

Base Commands

These commands are always available within the context of a micronaut project.

Create-Bean

Table 1. Create-Bean Flags
Flag Description Example

-l, --lang

The language used for the bean class

--lang groovy

-f, --force

Whether to overwrite existing files

--force

The create-bean command generates a simple Singleton class. It does not create an associated test.

$ mn create-bean EmailService
| Rendered template Bean.java to destination src/main/java/example/EmailService.java

Create-Job

Table 2. Create-Job Flags
Flag Description Example

-l, --lang

The language used for the job class

--lang groovy

-f, --force

Whether to overwrite existing files

--force

The create-job command generates a simple Scheduled class. It follows a *Job convention for generating the class name. It does not create an associated test.

$ mn create-job UpdateFeeds --lang groovy
| Rendered template Job.groovy to destination src/main/groovy/example/UpdateFeedsJob.groovy

Create-Controller

Table 3. Create-Controller Flags
Flag Description Example

-l, --lang

The language used for the controller

--lang groovy

-f, --force

Whether to overwrite existing files

--force

The create-controller command generates a Controller class. It follows a *Controller convention for generating the class name. It creates an associated test that runs the application and instantiates an HTTP client, which can make requests against the controller.

$ mn create-controller Book
| Rendered template Controller.java to destination src/main/java/example/BookController.java
| Rendered template ControllerTest.java to destination src/test/java/example/BookControllerTest.java

Create-Client

Table 4. Create-Client Flags
Flag Description Example

-l, --lang

The language used for the client

--lang groovy

-f, --force

Whether to overwrite existing files

--force

The create-client command generates a simple Client interface. It follows a *Client convention for generating the class name. It does not create an associated test.

$ mn create-client Book
| Rendered template Client.java to destination src/main/java/example/BookClient.java

Create-Websocket-Server

Table 5. Create-Websocket-Server Flags
Flag Description Example

-l, --lang

The language used for the server

--lang groovy

-f, --force

Whether to overwrite existing files

--force

The create-websocket-server command generates a simple ServerWebSocket class. It follows a *Server convention for generating the class name. It does not create an associated test.

$ mn create-websocket-server MyChat
| Rendered template WebsocketServer.java to destination src/main/java/example/MyChatServer.java

Create-Websocket-Client

Table 6. Create-Websocket-Client Flags
Flag Description Example

-l, --lang

The language used for the client

--lang groovy

-f, --force

Whether to overwrite existing files

--force

The create-websocket-client command generates a simple WebSocketClient abstract class. It follows a *Client convention for generating the class name. It does not create an associated test.

$ mn create-websocket-client MyChat
| Rendered template WebsocketClient.java to destination src/main/java/example/MyChatClient.java

CLI Project Commands

Create-Command

Table 7. Create-Command Flags
Flag Description Example

-l, --lang

The language used for the command

--lang groovy

-f, --force

Whether to overwrite existing files

--force

The create-command command generates a standalone application that can be executed as a picocli Command. It follows a *Command convention for generating the class name. It creates an associated test that runs the application and verifies that a command line option was set.

$ mn create-command print
| Rendered template Command.java to destination src/main/java/example/PrintCommand.java
| Rendered template CommandTest.java to destination src/test/java/example/PrintCommandTest.java

This list is just a small subset of the code generation commands in the Micronaut CLI. To see all context-sensitive commands the CLI has available (and under what circumstances they apply), check out the micronaut-starter project and find the classes that extend CodeGenCommand. The applies method dictates whether a command is available or not.

19.4 Reloading

Reloading (or "hot-loading") refers to the framework reinitializing classes (and parts of the application) when changes to the source files are detected.

Since Micronaut prioritizes startup time and most Micronaut apps can start up within seconds, a productive workflow can often be had by restarting the application as changes are made; for example, by running a test class within an IDE.

However, to have your changes automatically reloaded, Micronaut supports automatic restart and the use of third-party reloading agents.

19.4.1 Automatic Restart

There are various ways to achieve reloading of classes on the JVM, and all have their advantages and disadvantages. The following are possible ways to achieve reloading without restarting the JVM:

  • JVM Agents - A JVM agent like JRebel can be used, however these can produce unusual errors, may not support all JDK versions, and can result in cached or stale classes.

  • ClassLoader Reloading - ClassLoader-based reloading is a popular solution used by most JVM frameworks; however it once again can lead to cached or stale classes, memory leaks, and weird errors if the incorrect classloader is used.

  • Debugger HotSwap - The Java debugger supports hotswapping of changes at runtime, but only supports a few use cases.

Given the problems with existing solutions and a lack of a way built into the JVM to reload changes, the safest and best solution to reloading, and the one recommended by the Micronaut team, is to use automatic application restart via a third-party tool.

Micronaut’s startup time is fast and automatic restart leads to a clean slate without potential hard to debug problems or memory leaks cropping up.

Maven Restart

To have automatic application restarts with Maven, use the Micronaut Maven plugin (included by default when creating new Maven projects) and run the following command:

Using the Micronaut Maven Plugin
$ ./mvnw mn:run

Every time you change a class, the plugin automatically restarts the server.

Gradle Restart

Gradle automatic restart can be activated when using the Micronaut Gradle plugin by activating Gradle’s support for continuous builds via the -t flag:

Using Gradle for Automatic Restart
./gradlew run -t

Every time you make a change to class or resources, Gradle recompiles and restarts the application.

19.4.2 JRebel

JRebel is a proprietary reloading solution that involves an agent library, as well as sophisticated IDE support. The JRebel documentation includes detailed steps for IDE integration and usage. In this section, we show how to install and configure the agent for Maven and Gradle projects.

Using the CLI

If you create your project using the Micronaut CLI, supply the jrebel feature to preconfigure JRebel reloading in your project. Note that you need to install JRebel and supply the correct path to the agent in the gradle.properties file (for Gradle) or pom.xml (for Maven). The necessary steps are described below.

$ mn create-app my-app --features jrebel

Install/configure JRebel Agent

The simplest way to install JRebel is to download the "standalone" installation package from the JRebel download page. Unzip the downloaded file to a convenient location, for example ~/bin/jrebel

The installation directory contains a lib directory with the agent files. For the appropriate agent based on your operating system, see the table below:

Table 1. JRebel Agent
OS Agent

Windows 64-bit JDK

[jrebel directory]\lib\jrebel64.dll

Windows 32-bit JDK

[jrebel directory]\lib\jrebel32.dll

Mac OS X 64-bit JDK

[jrebel directory]/lib/libjrebel64.dylib

Mac OS X 32-bit JDK

[jrebel directory]/lib/libjrebel32.dylib

Linux 64-bit JDK

[jrebel directory]/lib/libjrebel64.so

Linux 32-bit JDK

[jrebel directory]/lib/libjrebel32.so

Note the path to the appropriate agent, and add the value to your project build.

Gradle

Add the path to gradle.properties (create the file if necessary), as the rebelAgent property.

gradle.properties
#Assuming installation path of ~/bin/jrebel/
rebelAgent= -agentpath:~/bin/jrebel/lib/libjrebel64.dylib

Add the appropriate JVM arg to build.gradle (not necessary if using the CLI feature)

run.dependsOn(generateRebel)
if (project.hasProperty('rebelAgent')) {
    run.jvmArgs += rebelAgent
}

You can start the application with ./gradlew run, and it will include the agent. See the section on Gradle Reloading or IDE Reloading to set up the recompilation.

Maven

Configure the Micronaut Maven Plugin accordingly:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <!-- ... -->
  <build>
    <plugins>
      <!-- ... -->
      <plugin>
        <groupId>io.micronaut.build</groupId>
        <artifactId>micronaut-maven-plugin</artifactId>
          <configuration>
            <jvmArguments>
              <jvmArgument>-agentpath:~/bin/jrebel/lib/jrebel6/lib/libjrebel64.dylib</jvmArgument>
            </jvmArguments>
          </configuration>
      </plugin>
      <plugin>
        <groupId>org.zeroturnaround</groupId>
        <artifactId>jrebel-maven-plugin</artifactId>
        <version>1.1.10</version>
        <executions>
          <execution>
            <id>generate-rebel-xml</id>
            <phase>process-resources</phase>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <!-- ... -->
    </plugins>
  </build>
</project>

19.4.3 Recompiling with Gradle

Gradle supports continuous builds, letting you run a task that will be rerun whenever source files change. To use this with a reloading agent (configured as described above), run the application normally (with the agent), and then run a recompilation task in a separate terminal with continuous mode enabled.

Run the app
$ ./gradlew run
Run the recompilation
$ ./gradlew -t classes

The classes task will be rerun every time a source file is modified, allowing the reloading agent to pick up the change.

19.4.4 Recompiling with an IDE

If you use a build tool such as Maven which does not support automatic recompilation on file changes, you may use your IDE to recompile classes in combination with a reloading agent (as configured in the above sections).

IntelliJ

IntelliJ unfortunately does not have an automatic rebuild option that works for a running application. However, you can trigger a "rebuild" of the project with CMD-F9 (Mac) or CTRL-F9 (Windows/Linux).

Eclipse

Under the Project menu, check the Build Automatically option. This will trigger a recompilation of the project whenever file changes are saved to disk.

19.5 Proxy Configuration

To configure the CLI to use an HTTP proxy there are two steps. Configuration options can be passed to the cli through the MN_OPTS environment variable.

For example on *nix systems:

export MN_OPTS="-Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=3128 -Dhttp.proxyUser=test -Dhttp.proxyPassword=test"

The profile dependencies are resolved over HTTPS so the proxy port and host are configured with https., however the user and password are specified with http..

For Windows systems the environment variable can be configured under My Computer/Advanced/Environment Variables.

20 Internationalization

20.1 Resource Bundles

A resource bundle is a Java .properties file that contains locale-specific data.

Given this Resource Bundle:

src/main/resources/io/micronaut/docs/i18n/messages_en.properties
hello=Hello
hello.name=Hello {0}
src/main/resources/io/micronaut/docs/i18n/messages_es.properties
hello=Hola
hello.name=Hola {0}

You can use ResourceBundleMessageSource, an implementation of MessageSource which eases accessing Resource Bundles and provides cache functionality, to access the previous messages.

Do not instantiate a new ResourceBundleMessageSource each time you retrieve a message. Instantiate it once, for example in a factory.
MessageSource Factory Example
import io.micronaut.context.MessageSource;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.i18n.ResourceBundleMessageSource;
import jakarta.inject.Singleton;

@Factory
class MessageSourceFactory {
    @Singleton
    MessageSource createMessageSource() {
        return new ResourceBundleMessageSource("io.micronaut.docs.i18n.messages");
    }
}
MessageSource Factory Example
import io.micronaut.context.MessageSource
import io.micronaut.context.annotation.Factory
import io.micronaut.context.i18n.ResourceBundleMessageSource
import jakarta.inject.Singleton

@Factory
class MessageSourceFactory {
    @Singleton
    MessageSource createMessageSource() {
        new ResourceBundleMessageSource("io.micronaut.docs.i18n.messages")
    }
}
MessageSource Factory Example
import io.micronaut.context.MessageSource
import io.micronaut.context.annotation.Factory
import io.micronaut.context.i18n.ResourceBundleMessageSource
import jakarta.inject.Singleton

@Factory
internal class MessageSourceFactory {
    @Singleton
    fun createMessageSource(): MessageSource = ResourceBundleMessageSource("io.micronaut.docs.i18n.messages")
}

Then you can retrieve the messages supplying the locale:

ResourceBundleMessageSource Example
assertEquals("Hola", messageSource.getMessage("hello", MessageContext.of(new Locale("es"))).get());
assertEquals("Hello", messageSource.getMessage("hello", MessageContext.of(Locale.ENGLISH)).get());
ResourceBundleMessageSource Example
expect:
messageSource.getMessage("hello", MessageContext.of(new Locale("es"))).get() == 'Hola'

and:
messageSource.getMessage("hello", MessageContext.of(Locale.ENGLISH)).get() == 'Hello'
ResourceBundleMessageSource Example
Assertions.assertEquals("Hola", messageSource.getMessage("hello", MessageSource.MessageContext.of(Locale("es"))).get())
Assertions.assertEquals("Hello", messageSource.getMessage("hello", MessageSource.MessageContext.of(Locale.ENGLISH)).get())

20.2 Localized Message Source

LocalizedMessageSource is a @RequestScope bean which you can inject in your Controllers and which uses Micronaut Locale Resolution to resolve the localized message for the current HTTP request.

See the guide for Localize your Application to learn more.

21 Resources

Micronaut Framework provides support for loading resources from files into memory, rooted at the ResourceLoader API. Two built-in implementations include DefaultFileSystemResourceLoader and DefaultClassPathResourceLoader.

A convenience class ResourceResolver is provided that leverages both of these implementations. The following example illustrates using the API to read a text file from the classpath.

ResourceResolver Example
@Singleton
public class MyResourceLoader {

    private final ResourceResolver resourceResolver;

    public MyResourceLoader(ResourceResolver resourceResolver) {  // (1)
        this.resourceResolver = resourceResolver;
    }

    public Optional<String> getClasspathResourceAsText(String path) throws Exception {
        Optional<URL> url = resourceResolver.getResource("classpath:" + path); // (2)
        if (url.isPresent()) {
            return Optional.of(IOUtils.readText(new BufferedReader(new InputStreamReader(url.get().openStream())))); // (3)
        } else {
            return Optional.empty();
        }
    }
}
ResourceResolver Example
@Singleton
class MyResourceLoader {

    private final ResourceResolver resourceResolver // (1)

    MyResourceLoader(ResourceResolver resourceResolver) { // (1)
        this.resourceResolver = resourceResolver
    }

    Optional<String> getClasspathResourceAsText(String path) throws Exception {
        Optional<URL> url = resourceResolver.getResource('classpath:' + path) // (2)
        if (url.isPresent()) {
            return Optional.of(IOUtils.readText(new BufferedReader(new InputStreamReader(url.get().openStream()))))  // (3)
        } else {
            return Optional.empty()
        }
    }
}
ResourceResolver Example
@Singleton
class MyResourceLoader(private val resourceResolver: ResourceResolver) { // (1)

    @Throws(Exception::class)
    fun getClasspathResourceAsText(path: String): Optional<String> {
        val url = resourceResolver.getResource("classpath:$path") // (2)
        return if (url.isPresent) {
            Optional.of(IOUtils.readText(BufferedReader(InputStreamReader(url.get().openStream()))))  // (3)
        } else {
            Optional.empty()
        }
    }
}
1 Injects an instance of ResourceResolver
2 Uses the ResourceResolver API to get a URL to a file in the classpath, if it exists.
3 IOUtils provides further support for I/O operations, in this case reading the file contents into a String.

22 Appendices

22.1 Micronaut Architecture

The following documentation describes the Micronaut framework’s architecture and is designed for those who are looking for information on the internal workings of the Micronaut framework and how it is architected. This is not intended as end-user developer documentation, but for those interested in the inner workings of the Micronaut framework.

This documentation is divided into sections that describe the compiler, introspections, application container, dependency injection and so on.

Since this documentation covers the internal workings of Micronaut, many APIs referenced and described are regarded as internal, non-public API and are annotated as such with the @Internal. Internal APIs can change between patch releases of Micronaut and are not covered by Micronaut’s semantic versioning release policy.

22.1.1 Compiler

The Micronaut Compiler is a set of extensions to existing language compilers:

To keep this documentation simple, the remaining sections will describe the interaction with the Java compiler.

The Micronaut Compiler visits end user code and generates additional bytecode that sits alongside the user code in the same package structure.

The AST of user source is visited using implementations of TypeElementVisitor which are loaded via the standard Java service loader mechanism.

Each TypeElementVisitor implementation can override one or more of the visit* methods which receive an instance of Element.

The package-info provides a language-neutral abstraction over the AST and computation of the AnnotationMetadata for a given element (class, method, field etc).

22.1.2 Annotation Metadata

Micronaut framework is an implementation of an annotation-based programming model. That is to say annotations form a fundamental part of the API design of the framework.

Given this design decision, a compilation-time model was formulated to address the challenges of evaluating annotations at runtime.

The AnnotationMetadata API is a construct that is used both a compilation time and at runtime by framework components. AnnotationMetadata represents the computed fusion of annotation information for a particular type, field, constructor, method or bean property and may include both annotations declared in the source code, but also synthetic meta-annotations that can be used at runtime to implement framework logic.

When visiting source code within the Micronaut Compiler using the package-info for each ClassElement, FieldElement, MethodElement, ConstructorElement and PropertyElement an instance of AnnotationMetadata is computed.

The AnnotationMetadata API tries to address the following challenges:

  • Annotations can be inherited from types and interfaces into implementations. To avoid the need to traverse the class/interface hierarchy at runtime Micronaut will at build time compute inherited annotations and deal with member overriding rules

  • Annotations can be annotated with other annotations. These annotations are often referred to as meta-annotations or stereotypes. The AnnotationMetadata API provides methods to understand whether a particular annotation is declared as meta-annotation and to find out what annotations are meta-annotated with other annotations

  • It is often necessary to fuse annotation metadata together from different sources. For example, for JavaBean properties you want to combine the metadata from the private field, public getter and public setters into a single view otherwise you have to run logic to runtime to somehow combine this metadata from 3 distinct sources.

  • Repeatable annotations are combined and normalized. If inherited the annotations are combined from parent interfaces or classes providing a single API to evaluate repeatable annotations instead of requiring runtime logic to perform normalization.

When the source for a type is visited an instance of ClassElement is constructed via the ElementFactory API.

The ElementFactory uses an instance of AbstractAnnotationMetadataBuilder which contains language specific implementations to construct AnnotationMedata for the underlying native type in the AST. In the case of Java this would be a javax.model.element.TypeElement.

The basic flow is visualized below:

annotationmetadata

Additionally, the AbstractAnnotationMetadataBuilder will load via the standard Java service loader mechanism one or more instances of the following types that allow manipulating how an annotation is represented in the AnnotationMetadata:

  • AnnotationMapper - A type that can map the value of one annotation to another, retaining the original annotation in the AnnotationMetadata

  • AnnotationTransformer - A type that can transform the value of one annotation to another, eliminating the original annotation from the AnnotationMetadata

  • AnnotationRemapper - A type that can transform the values of all annotations in a given package, eliminating the original annotations from the AnnotationMetadata

Note that at compilation time the AnnotationMetadata is mutable and can be further altered by implementations of TypeElementVisitor by invoking the annotate(..) method of the Element API. However, at runtime the AnnotationMetadata is immutable and fixed. The purpose of this design to allow the compiler to be extended and for Micronaut to be able to interpret different source-level annotation-based programming models.

In practice this effectively allows decoupling the source code level annotation model from what is used at runtime such that different annotations can be used to represent the same annotation.

For example jakarata.inject.Inject or Spring’s @Autowired are supported as synonyms for jakarta.inject.Inject by transforming the source level annotation to jakarta.inject.Inject which is the only annotation represented at runtime.

Finally, annotations in Java also allow the definition of default values. These defaults are not retained in individual instances of AnnotationMetadata but instead stored in a shared, static application-wide map for later retrieval for annotations known to be used by the application.

22.1.3 Bean Introspections

The goal of Bean Introspections is to provide an alternative to reflection and the JDK’s Introspector API that is coupled to the java.desktop module in recent versions of Java.

Many libraries in Java need to programmatically discover what methods represent properties of a class in some way and whilst the JavaBeans specification tried to establish a standard convention, the language itself has evolved to include other constructs like Records that represent properties as components.

In addition, other languages like Kotlin and Groovy have native support for class properties that need to be supported at the framework level.

The IntrospectedTypeElementVisitor visits declarations of the @Introspected annotation on types and generates at compilation time implementations of BeanIntrospection that are associated with each annotated type:

introspections

This generation happens via the io.micronaut.inject.beans.visitor.BeanIntrospectionWriter, an internal class that uses the ASM bytecode generation library to generate two additional classes.

For example, given a class called example.Person the classes generated are:

  • example.$Person$IntrospectionRef - an implementation of BeanIntrospectionReference that allows the application to soft load the introspection without loading all metadata or the class itself (in the case where the introspected class is itself not on the classpath). Since references are loaded via ServiceLoader an entry in a generated META-INF/services/io.micronaut.core.beans.BeanIntrospectionReference referring to this type is also generated at compilation time.

  • example.$Person$Introspection - an implementation of BeanIntrospection which contains the actual runtime introspection information.

The following example demonstrates usage of the BeanIntrospection API:

final BeanIntrospection<Person> introspection = BeanIntrospection.getIntrospection(Person.class); // (1)
Person person = introspection.instantiate("John"); // (2)
System.out.println("Hello " + person.getName());

final BeanProperty<Person, String> property = introspection.getRequiredProperty("name", String.class); // (3)
property.set(person, "Fred"); // (4)
String name = property.get(person); // (5)
System.out.println("Hello " + person.getName());
def introspection = BeanIntrospection.getIntrospection(Person) // (1)
Person person = introspection.instantiate("John") // (2)
println("Hello $person.name")

BeanProperty<Person, String> property = introspection.getRequiredProperty("name", String) // (3)
property.set(person, "Fred") // (4)
String name = property.get(person) // (5)
println("Hello $person.name")
val introspection = BeanIntrospection.getIntrospection(Person::class.java) // (1)
val person : Person = introspection.instantiate("John") // (2)
print("Hello ${person.name}")

val property : BeanProperty<Person, String> = introspection.getRequiredProperty("name", String::class.java) // (3)
property.set(person, "Fred") // (4)
val name = property.get(person) // (5)
print("Hello ${person.name}")
1 A BeanIntrospection is looked up by type. When this occurs the introspection will be searched for amongst the BeanIntrospectionReference instances loaded via ServiceLoader.
2 The instantiate method allows creating instances
3 Properties for bean can be loaded via one of the available methods, in this case getRequiredProperty
4 The referenced BeanProperty can be used to write mutable properties
5 And read readable properties
The Person class is only initialized when the getBeanType() method is called. If the class is not present on the classpath then a NoClassDefFoundError will occur, to prevent this the developer can call the isPresent() method on the BeanIntrospectionReference prior to trying to obtain the type.

An implementation of BeanIntrospection performs two critical functions:

  1. The introspection holds Bean metadata about the properties and constructor arguments for a particular type that is abstracted away from the actual implementation (JavaBean property, Java 17+ Record, Kotlin data classes, Groovy properties etc.) and which also provide access to AnnotationMetadata without needing to use reflection to load the annotations themselves.

  2. The introspection enables the ability to instantiate and read/write bean properties without the use of Java reflection, based purely on the subset of build-time generated information.

Optimized reflection-free method dispatch is generated by overriding the dispatchOne method of AbstractInitializableBeanIntrospection, for example:

protected final Object dispatchOne(int propertyIndex, Object bean, Object value
) {
    switch(propertyIndex) { (1)
    case 0:
        return ((Person) bean).getName(); (2)
    case 1:
        ((Person) bean).setName((String) value); (3)
        return null;
    default:
        throw this.unknownDispatchAtIndexException(propertyIndex); (4)
    }
}
1 Each read or write method is assigned an index
2 The index is used in read methods to obtain the value directly without relying on reflection
3 The index is used for write methods to set a property without using reflection
4 If no property exists at the index an exception is thrown, though this is implementation detail and the codepath should never arrive to this point.
The approach to use a dispatch method with an index was used to avoid the need to generate a class per method (which would consume more memory) or introduce the overhead of lambdas.

In order to enable type instantiation the io.micronaut.inject.beans.visitor.BeanIntrospectionWriter will also generate an implementation of the instantiateInternal method which contains the reflection-free code to instantiate a given type based on known valid argument types:

public Object instantiateInternal(Object[] args) {
    return new Person(
        (String)args[0],
        (Integer)args[1]
    );
}

22.1.4 Bean Definitions

Micronaut framework is an implementation of the JSR-330 specification for Dependency Injection.

Dependency Injection (or Inversion of Control) is a widely adopted and common pattern in Java that allows loosely decoupling components to allow applications to be easily extended and tested.

The way in which objects are wired together is decoupled from the objects themselves in this model by a separate programming model. In the case of Micronaut this model is based on annotations defined within the JSR-330 specification plus an extended set of annotations located within the package-info package.

These annotations are visited by the Micronaut Compiler which traverses the source code language AST and builds a model used to wire objects together at runtime.

It is important to note that the actual object wiring is deferred until runtime.

For Java code BeanDefinitionInjectProcessor (which is a Java Annotation Processor) is invoked from the Java compiler for each class annotated with a bean definition annotation.

What constitutes a bean defining annotation is complex as it takes into account meta-annotations, but in general it is any annotation annotated with a JSR-330 bean @Scope

The BeanDefinitionInjectProcessor will visit each bean in the user code source and generate additional byte code using the ASM byte code generation library that sits alongside the annotated class in the same package.

For historic reasons the dependency injection processor does not use the TypeElementVisitor API but will likely do so in the future

Byte code generation is implemented in the BeanDefinitionWriter which contains methods to "visit" different aspects of the way is bean is defined (the BeanDefinition).

The following diagram illustrates the flow:

beanwriter

For example given the following type:

@Singleton
public class Vehicle {
    private final Engine engine;

    public Vehicle(Engine engine) {// (3)
        this.engine = engine;
    }

    public String start() {
        return engine.start();
    }
}
@Singleton
class Vehicle {
    final Engine engine

    Vehicle(Engine engine) { // (3)
        this.engine = engine
    }

    String start() {
        engine.start()
    }
}
@Singleton
class Vehicle(private val engine: Engine) { // (3)
    fun start(): String {
        return engine.start()
    }
}

The following is generated:

  • A example.$Vehicle$Definition$Reference class that implements the BeanDefinitionReference interface that allows the application to soft load the bean definition without loading all metadata or the class itself (in the case where the introspected class is itself not on the classpath). Since references are loaded via ServiceLoader an entry in a generated META-INF/services/io.micronaut.inject.BeanDefinitionReference referring to this type is also generated at compilation time.

  • A example.$Vehicle$Definition which contains the actual BeanDefinition information.

A BeanDefinition is a type that holds metadata about the particular type including:

In addition, the BeanDefinition contains logic which knows how the bean is wired together, including how the type is constructed and fields and/or methods injected.

During compilation the ASM byte code library is used to fill out the details of the BeanDefinition, including a build method that, for the previous example, looks like:

public Vehicle build(
    BeanResolutionContext resolution, (1)
    BeanContext context,
    BeanDefinition definition) {
    Vehicle bean = new Vehicle(
        (Engine) super.getBeanForConstructorArgument( (2)
            resolution,
            context,
            0, (3)
            (Qualifier)null)
    );
    return bean;
}
1 The BeanResolutionContext is passed around to track circular bean references and improve error reporting.
2 The type is instantiated and each constructor argument looked up by calling methods of AbstractInitializableBeanDefinition
3 In this case the index of the constructor argument is tracked
Special handling is required when a Java field or method has private access. In this case Micronaut has no option but to fall back to using Java reflection to perform dependency injection.

Configuration Properties Handling

The Micronaut Compiler handles beans declared with the meta-annotation @ConfigurationReader such as @ConfigurationProperties and @EachProperty distinctly to other beans.

In order to support binding Application Configuration to types annotated with one of the aforementioned annotations each discovered mutable bean property is dynamically annotated with the @Property annotation with the computed and normalized property name.

For example given the below type:

@ConfigurationProperties Example
import io.micronaut.context.annotation.ConfigurationProperties;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import java.util.Optional;

@ConfigurationProperties("my.engine") // (1)
public class EngineConfig {

    public String getManufacturer() {
        return manufacturer;
    }

    public void setManufacturer(String manufacturer) {
        this.manufacturer = manufacturer;
    }

    public int getCylinders() {
        return cylinders;
    }

    public void setCylinders(int cylinders) {
        this.cylinders = cylinders;
    }

    public CrankShaft getCrankShaft() {
        return crankShaft;
    }

    public void setCrankShaft(CrankShaft crankShaft) {
        this.crankShaft = crankShaft;
    }

    @NotBlank // (2)
    private String manufacturer = "Ford"; // (3)

    @Min(1L)
    private int cylinders;

    private CrankShaft crankShaft = new CrankShaft();

    @ConfigurationProperties("crank-shaft")
    public static class CrankShaft { // (4)

        private Optional<Double> rodLength = Optional.empty(); // (5)

        public Optional<Double> getRodLength() {
            return rodLength;
        }

        public void setRodLength(Optional<Double> rodLength) {
            this.rodLength = rodLength;
        }
    }
}
@ConfigurationProperties Example
import io.micronaut.context.annotation.ConfigurationProperties

import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank

@ConfigurationProperties('my.engine') // (1)
class EngineConfig {

    @NotBlank // (2)
    String manufacturer = "Ford" // (3)

    @Min(1L)
    int cylinders

    CrankShaft crankShaft = new CrankShaft()

    @ConfigurationProperties('crank-shaft')
    static class CrankShaft { // (4)
        Optional<Double> rodLength = Optional.empty() // (5)
    }
}
@ConfigurationProperties Example
import io.micronaut.context.annotation.ConfigurationProperties
import java.util.Optional
import jakarta.validation.constraints.Min
import jakarta.validation.constraints.NotBlank

@ConfigurationProperties("my.engine") // (1)
class EngineConfig {

    @NotBlank // (2)
    var manufacturer = "Ford" // (3)

    @Min(1L)
    var cylinders: Int = 0

    var crankShaft = CrankShaft()

    @ConfigurationProperties("crank-shaft")
    class CrankShaft { // (4)
        var rodLength: Optional<Double> = Optional.empty() // (5)
    }
}

The setManufacturer(String) method will be annotated with @Property(name="my.engine.manufacturer") the value of which will be resolved from the configured Environment.

The injectBean method of AbstractInitializableBeanDefinition is subsequently overridden with logic to handle looking up the normalized property name my.engine.manufacturer from the current BeanContext and inject the value if it is present in a reflection-free manner.

Property names are normalized into kebab case (lower case hyphen separated) which is the format used to store their values.
Configuration Properties Injection
@Generated
protected Object injectBean(
    BeanResolutionContext resolution,
    BeanContext context,
    Object bean) {
    if (this.containsProperties(resolution, context)) { (1)
        EngineConfig engineConfig = (EngineConfig) bean;
        if (this.containsPropertyValue(resolution, context, "my.engine.manufacturer")) { (2)
            String value = (String) super.getPropertyValueForSetter( (3)
                resolution,
                context,
                "setManufacturer",
                Argument.of(String.class, "manufacturer"), (4)
                "my.engine.manufacturer", (5)
                (String)null (6)
            )
            engineConfig.setManufacturer(value);
        }
    }
}
1 A top level check to see if any properties exist with the prefix defined in the @ConfigurationProperties annotation is added.
2 A check is performed to see if the property actually exists
3 If it does the value is looked up by calling the getPropertyValueForSetter method of AbstractInitializableBeanDefinition
4 An instance of Argument is created which is used for conversion to the target type (in this case String). The Argument may also contain generics information.
5 The computed and normalized path to the property
6 The default value if the Bindable annotation is used to specify a default.

22.1.5 AOP Proxies

Micronaut supports annotation-based Aspect Oriented Programming (AOP) which allows decorating or introducing type behaviour through the use of interceptors defined in user code.

The use of the AOP terminogy originates from AspectJ and historical use in Spring.

Any annotation defined by the framework can be meta-annotated with the @InterceptorBinding annotation which supports different kinds of interception including:

  • AROUND - A annotation can be used to decorate an existing method invocation

  • AROUND_CONSTRUCT - An annotation can be used to intercept the construction of any type

  • INTRODUCTION - An annotation can be used to "introduce" new behaviour to abstract or interface types

  • POST_CONSTRUCT - An annotation can be used to intercept @PostConstruct calls which are invoked after the object is instantiated.

  • PRE_DESTROY - An annotation can be used to intercept @PreDestroy calls which are invoked after the object is about to be disposed of.

One or many instances of Interceptor can be associated with an @InterceptorBinding allowing the user to implement behaviour that applies cross-cutting concerns.

At an implementation level, the Micronaut Compiler will visit types that are meta-annotated with @InterceptorBinding and construct a new instance of AopProxyWriter which uses the ASM bytecode generation library to generate a subclass (or an implementation in the case of interfaces) of the annotated type.

Micronaut at no point modifies existing user byte code, the use of build-time generated proxies allows Micronaut to generate additional code that sits alongside user code and enhances behaviour. This approach does have limitations however, for example it is required that annotated types are non-final and AOP advice cannot be applied to final or effectively final types such as Java 17 Records.

For example given the following annotation:

Around Advice Annotation Example
import io.micronaut.aop.Around;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME) // (1)
@Target({TYPE, METHOD}) // (2)
@Around // (3)
public @interface NotNull {
}
Around Advice Annotation Example
import io.micronaut.aop.Around
import java.lang.annotation.*
import static java.lang.annotation.ElementType.*
import static java.lang.annotation.RetentionPolicy.RUNTIME

@Documented
@Retention(RUNTIME) // (1)
@Target([TYPE, METHOD]) // (2)
@Around // (3)
@interface NotNull {
}
Around Advice Annotation Example
import io.micronaut.aop.Around
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.annotation.AnnotationTarget.FILE
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER

@MustBeDocumented
@Retention(RUNTIME) // (1)
@Target(CLASS, FILE, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER) // (2)
@Around // (3)
annotation class NotNull
1 The retention policy of the annotation must be RUNTIME
2 Generally you want to be able to apply advice at the class or method level so the target types are TYPE and METHOD
3 The @Around annotation is used here which itself is annotated with @InterceptorBinding(kind=AROUND) and can be thought of as a simple shortcut for defining an @InterceptorBinding for AROUND advice.

When this annotation is used on a type or method, for example:

Around Advice Usage Example
import jakarta.inject.Singleton;

@Singleton
public class NotNullExample {

    @NotNull
    void doWork(String taskName) {
        System.out.println("Doing job: " + taskName);
    }
}
Around Advice Usage Example
import jakarta.inject.Singleton

@Singleton
class NotNullExample {

    @NotNull
    void doWork(String taskName) {
        println "Doing job: $taskName"
    }
}
Around Advice Usage Example
import jakarta.inject.Singleton

@Singleton
open class NotNullExample {

    @NotNull
    open fun doWork(taskName: String?) {
        println("Doing job: $taskName")
    }
}

The compiler will visit the type and the AopProxyWriter will generate additional bytecode using the ASM bytecode generation library.

During compilation the AopProxyWriter instance essentially proxies the BeanDefinitionWriter (see Bean Definitions), decorating the existing bytecode generation with additional behaviour. This is illustrated with the below diagram:

aop

The BeanDefinitionWriter will generate the regular classes generated for every bean including:

  • $NotNullExample$Definition.class - The original undecorated bean definition (see Bean Definitions)

  • $NotNullExample$Definition$Exec.class - An implementation of ExecutableMethodsDefinition containing logic that allows dispatching to each intercepted method without using reflection.

And the AopProxyWriter will decorate this behaviour and generate 3 additional classes:

  • $NotNullExample$Definition$Intercepted.class - A subclass of the decorated class that holds references to applied MethodInterceptor instances and overrides all the intercepted methods, constructing the MethodInterceptorChain instance and invoking the applied interceptors

  • $NotNullExample$Definition$Intercepted$Definition.class - A BeanDefinition that subclasses the original undecorated bean definition. (see Bean Definitions)

  • $NotNullExample$Definition$Intercepted$Definition$Reference.class - A BeanDefinitionReference that is capable of soft loading the intercepted BeanDefinition. (see Bean Definitions)

The majority of the classes generated are metadata for loading and resolving the BeanDefinition. The actual build time proxy is the class that ends with $Intercepted. This class implements the Intercepted interface and subclasses the proxied type, overriding any non-final and non-private methods to invoke the MethodInterceptorChain.

An implementation will create a constructor which is used to wire in the dependencies on the intercepted type that looks like:

An intercepted type constructor
@Generated
class $NotNullExample$Definition$Intercepted
extends NotNullExample implements Intercepted { (1)
    private final Interceptor[][] $interceptors = new Interceptor[1][];
    private final ExecutableMethod[] $proxyMethods = new ExecutableMethod[1];

    public $NotNullExample$Definition$Intercepted(
        BeanResolutionContext resolution,
        BeanContext context,
        Qualifier qualifier,
        List<Interceptor> interceptors) {
        Exec executableMethods = new Exec(true); (2)
        this.$proxyMethods[0] = executableMethods.getExecutableMethodByIndex(0); (3)
        this.$interceptors[0] = InterceptorChain
            .resolveAroundInterceptors(
                context,
                this.$proxyMethods[0],
                interceptors
        );  (4)
    }
}
1 The @Generated subclass extends from the decorated type and implements the Intercepted interface
2 An instance of ExecutableMethodsDefinition is constructed to resolve reflection-free dispatchers to the original method.
3 An internal array called $proxyMethods holds a reference for to each ExecutableMethod instance used to proxy the invocation.
4 An internal array called $interceptors holds references to which Interceptor instances apply to each method since an @InterceptorBinding can be type or method level these may differ for each method.

Each non-final and non-private method of the proxied type that has an @InterceptorBinding associated with it (either type level or method level) is overridden with logic that proxies the original method, for example:

@Overrides
public void doWork(String taskName) {
    ExecutableMethod method = this.$proxyMethods[0];
    Interceptor[] interceptors = this.$interceptors[0]; (1)
    MethodInterceptorChain chain = new MethodInterceptorChain( (2)
        interceptors,
        this,
        method,
        new Object[]{taskName}
    );
    chain.proceed(); (3)
}
1 The ExecutableMethod and array of Interceptor instances for the method is located.
2 A new MethodInterceptorChain is constructed with the interceptors, a reference to the intercepted instance, the method and the arguments.
3 The proceed() method is invoked on the MethodInterceptorChain.

Note that the default behaviour of the @Around annotation is to invoke the original overridden method of the target type by calling the super implementation via a generated synthetic bridge method that allows access to the super implementation (in the above case NotNullExample).

In this arrangement the proxy and the proxy target are the same object, with interceptors being invoked and the call to proceed() invoke the original implementation via a call to super.doWork() in the case above.

However, this behaviour can be customized using the @Around annotation.

By setting @Around(proxyTarget=true) the generated code will also implement the InterceptedProxy interface which defines a single method called interceptedTarget() that resolves the target object the proxy should delegate method calls to.

The default behaviour (proxyTarget=false) is more efficient memory wise as only a single BeanDefinition is required and a single instance of the proxied type.

The evaluation of the proxy target is eager and done when the proxy is first created, however it can be made lazy by setting @Around(lazy=true, proxyTarget=true) in which case the proxy will only be retrieved when a proxied method is invoked.

The difference in behaviour between proxying the target with proxyTarget=true is illustrated in the following diagram:

aop proxies

The sequence on the left hand side of the diagram (proxyTarget=false) invokes the proxied method via a call to super whilst the sequence on the right looks up a proxy target from the BeanContext and invokes the method on the target.

One final customization option is @Around(hotswap=true) which triggers the compiler to produce a compile-time proxy that implements HotSwappableInterceptedProxy which defines a single method called swap(..) that allows swapping out the target of the proxy with a new instance (to allow this to be thread safe the generated code uses a ReentrantReadWriteLock).

Security Considerations

Method interception via AROUND advice is typically used to define logic that addresses cross-cutting concerns, one of which is security.

When multiple Interceptor instances apply to a single method it may be important from a security perspective that these interceptors execute in a specific order.

The Interceptor interface extends the Ordered interface to enable the developer to control interceptor ordering by overriding the getOrder() method.

When the MethodInterceptorChain is constructed and multiple interceptors are present they are ordered with HIGHEST priority interceptors executed first.

To aid the developer who defines their own Around Advice the InterceptPhase enumeration defines various constants that can be used to correctly declare the value of getOrder() (for example security typically falls within the VALIDATE phase).

Trace level logging can be enabled for the io.micronaut.aop.chain package to debug resolved interceptor order.

22.1.6 Application Context

Once the job of the Micronaut Compiler is complete and the required classes generated, it is up to the BeanContext to load the classes for runtime execution.

Whilst the standard Java service loader mechanism is used to define instances of BeanDefinitionReference, the instances themselves are instead loaded with SoftServiceLoader which is a more lenient implementation that allows checking if the service is actually present before loading and also allows parallel loading of services.

The BeanContext performs the following steps:

  1. Soft load all BeanDefinitionReference instances in parallel

  2. Instantiate all beans annotated with @Context (beans scoped to the whole context)

  3. Run each ExecutableMethodProcessor for each discovered processed ExecutableMethod. A method is regarded as "processed" if it is meta-annotated with @Executable(processOnStartup = true)

  4. Publish an event on type StartupEvent for when the context is started.

The basic flow is illustrated below:

beancontext

The ApplicationContext is a specialized version of the BeanContext that adds the notion of one or more active environments (encapsulated by Environment) and conditional bean loading based on this environment.

The Environment is loaded from one or more defined PropertySource instances that are discovered via the standard Java service loader mechanism by loading instances of PropertySourceLoader.

A developer can extend Micronaut to load a PropertySource through an entirely custom mechanism by adding another implementation and the associated META-INF/services/io.micronaut.context.env.PropertySourceLoader file referencing this class.

A high level different between a BeanContext and an ApplicationContext is illustrated below:

applicationcontext

As seen above the ApplicationContext loads the Environment which is used for multiple purposes including:

22.1.7 HTTP Server

The Micronaut HTTP server can be considered a Micronaut Module - that is a component of Micronaut that builds on the fundamental building blocks including Dependency Injection and the lifecycle of the ApplicationContext.

The HTTP server includes a set of abstract interfaces and common code contained with the micronaut-http and micronaut-http-server modules respectively (the former includes HTTP primitives shared across the client and the server).

A default implementation of these interfaces is provided based on the Netty I/O toolkit the architecture of which is described in the image below:

components

The Netty API is in general a very low-level I/O networking API designed for integrators to use to build clients and servers that present a higher abstraction layer. The Micronaut HTTP server is one such abstraction layer.

An architecture diagram of the Micronaut HTTP server and the components used in its implementation is described below:

httpserver

The main entry point for running the server is the Micronaut class which implements ApplicationContextBuilder. Typically, the developer places the following call into the main entry point of their application:

Defining a main entry point
public static void main(String[] args) {
    Micronaut.run(Application.class, args);
}
The passed arguments a transformed into a CommandLinePropertySource and available for dependency injection via @Value.

Executing run will start the Micronaut ApplicationContext with the default settings and then search for a bean of type EmbeddedServer which is an interface that exposes information about a runnable server including host and port information. This design decouples Micronaut from the actual server implementation and whilst the default server is Netty (described above), other servers can be implemented by third-parties simply by providing an implementation of EmbeddedServer.

A sequence diagram for how the server is started is illustrated below:

embeddedserver

In the case of the Netty implementation the EmbeddedServer interface is implemented by NettyHttpServer.

Server Configuration

The NettyHttpServer reads the Server Configuration including:

Server Configuration Security Considerations

Netty’s SslContext provides an abstraction which allows using either the JDK-provided javax.net.ssl.SSLContext or an OpenSslEngine that requires the developer to additionally add netty-tcnative as a dependency (netty-tcnative is a fork of Tomcat’s OpenSSL binding).

The ServerSslConfiguration allows configuring the application to a secure, readable location on disk where valid certificates exist to correctly configure the javax.net.ssl.TrustManagerFactory and javax.net.ssl.KeyManagerFactory by loading the configurtion from disk.

Netty Server Initialization

When the NettyHttpServer executes the start() sequence, it will perform the following steps:

  1. Read the EventLoopGroupConfiguration and create the parent and worker EventLoopGroup instances required to start a Netty server.

  2. Compute a platform specific ServerSocketChannel to use (depending on Operating System this could either be Epoll or KQueue, falling back to Java NIO if no native binding is possible)

  3. Creates the instance of ServerBootstrap used to initialze the SocketChannel (the connection between client and server).

  4. The SocketChannel is initialized by a Netty ChannelInitializer that creates the customized Netty ChannelPipeline used to Micronaut to server HTTP/1.1 or HTTP/2 requests depending on configuration.

  5. The Netty ServerBootstrap is bound to one or more configured ports, effectively making the server available to receive requests.

  6. Two Bean Events are fired, first ServerStartupEvent to indicate the server has started, then finally once all these events are processed a ServiceReadyEvent only if the property micronaut.application.name is set.

This startup sequence is illustrated below:

nettybootstrap

A NettyHttpServerInitializer class is used to initialize the ChannelPipeline that handles incoming HTTP/1.1 or HTTP/2 requests.

ChannelPipeline Security Considerations

The ChannelPipeline can be customized by the user by implementing a bean that implements the ChannelPipelineCustomizer interface and adding a new Netty ChannelHandler to the pipeline.

Adding a ChannelHandler allows performing tasks such as wire-level logging of incoming and outgoing data packets and may be used when wire-level security requirements are required such as validating the bytes of the incoming request body or outgoing response body.

Netty Server Routing

Micronaut defines a set of package-info that allow binding user code to incoming HttpRequest instances and customizing the resulting HttpResponse.

One or many configured RouteBuilder implementations construct instances of UriRoute which is used by the Router components to route incoming requests methods of annotated classes such as:

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/hello") // (1)
public class HelloController {

    @Get(produces = MediaType.TEXT_PLAIN) // (2)
    public String index() {
        return "Hello World"; // (3)
    }
}
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller('/hello') // (1)
class HelloController {

    @Get(produces = MediaType.TEXT_PLAIN) // (2)
    String index() {
        'Hello World' // (3)
    }
}
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/hello") // (1)
class HelloController {

    @Get(produces = [MediaType.TEXT_PLAIN]) // (2)
    fun index(): String {
        return "Hello World" // (3)
    }
}

Request binding annotations can be used to bind method parameters to the HTTP body, headers, parameters etc. and the framework will automatically deal with correctly escaping the data before it passed to the receiving method.

An incoming request is received by Netty and a ChannelPipeline initialized by NettyHttpServerInitializer. The incoming raw packets are transformed into a Netty HttpRequest which is subsequently wrapped in a Micronaut NettyHttpRequest which abstracts over the underlying Netty request.

The NettyHttpRequest is passed through the chain of Netty ChannelHandler instances until it arrives at RoutingInBoundHandler which uses the aforementioned Router to match the request a method of an annotated @Controller type.

The RoutingInBoundHandler delegates to RouteExecutor for actual execution of the route, which deals with all the logic to dispatch to a method of an annotated @Controller type.

Once executed, if the return value is not null an appropriate MediaTypeCodec is looked up from the MediaTypeCodecRegistry for the response Content-Type (defaulting to application/json). The MediaTypeCodec is used to encode the return value into a byte[] and include it as the body of the resulting HttpResponse.

The following diagram illustrates this flow for an incoming request:

http server requestflow

The RouteExecutor will construct a FilterChain to execute one or many HttpServerFilter prior executing the target method of an annotated @Controller type.

Once all of the HttpServerFilter instances have been executed the RouteExecutor will attempt to satisfy the requirements of the target method’s parameters, including any Request binding annotations. If the parameters cannot be satisfied then a HTTP 400 - Bad Request HttpStatus response is returned to the calling client.

Netty Server Routing Security Considerations

A HttpServerFilter instance can be used by the developer to control access to server resources. By not proceeding with the FilterChain an alternative response (such as a 403 - Forbidden) can be returned to the client barring access to sensitive resources.

Note that the HttpServerFilter interface extends from the Ordered interface since it is frequently the case that multiple filters exist within a FilterChain. By implementing the getOrder() method the developer can return an appropriate priority to control ordering. In addition, the ServerFilterPhase enum provides a set of constants developers can use to correctly position a filter, including a SECURITY phase where security rules are commonly placed.

22.2 Frequently Asked Questions (FAQ)

The following section covers frequently asked questions that you may find yourself asking while considering to use or using Micronaut.

Does Micronaut modify my bytecode?

No. Your classes are your classes. Micronaut does not transform classes or modify the bytecode generated from the code you write. Micronaut produces additional classes at compile time in the same package as your original unmodified classes.

Why Doesn’t Micronaut use Spring?

When asking why Micronaut doesn’t use Spring, it is typically in reference to the Spring Dependency Injection container.

The Spring ecosystem is very broad and there are many Spring libraries you can use directly in Micronaut without requiring the Spring container.

The reason Micronaut features its own native JSR-330 compliant dependency injection is that the cost of these features in Spring (and any reflection-based DI/AOP container) is too great in terms of memory consumption and the impact on startup time. To support dependency injection at runtime, Spring:

The result is a progressive degradation of startup time and memory consumption as your application incorporates more features.

For Microservices and Serverless functions where it is critical that startup time and memory consumption remain low, the above behaviour is an undesirable reality of using the Spring container, hence the designers of Micronaut chose not to use Spring.

Does Micronaut support Scala?

Micronaut supports any JVM language that supports the Annotation Processor API. Scala currently does not support this API. However, Groovy also doesn’t support this API and special support has been built that processes the Groovy AST. It may be technically possible to support Scala in the future if a module similar to inject-groovy is built, but as of this writing Scala is not supported.

Can Micronaut be used for purposes other than Microservices?

Yes. Micronaut is very modular, and you can choose to use just the Dependency Injection and AOP implementation by including the micronaut-inject-java (or micronaut-inject-groovy for Groovy) dependency in your application.

In fact Micronaut’s support for Serverless Computing uses this exact approach.

What are the advantages of Micronaut’s Dependency Injection and AOP implementation?

Micronaut processes your classes and produces all metadata at compile time. This eliminates the need for reflection, cached reflective metadata, and the requirement to analyze your classes at runtime, all of which lead to slower startup performance and greater memory consumption.

In addition, Micronaut builds reflection-free AOP proxies at compile time, which improves performance, reduces stack trace sizes, and reduces memory consumption.

Why does Micronaut have its own Consul and Eureka client implementations?

The majority of Consul and Eureka clients that exist are blocking and include many external dependencies that inflate your JAR files.

Micronaut’s DiscoveryClient uses Micronaut’s native HTTP client, greatly reducing the need for external dependencies and providing a reactive API onto both discovery servers.

Why am I encountering a NoSuchMethodError occurs loading my beans (Groovy)?

Groovy by default imports classes in the groovy.lang package, including one named @Singleton, an AST transformation class that makes your class a singleton by adding a private constructor and static retrieval method. This annotation is easily confused with the jakarta.inject.Singleton annotation used to define singleton beans in Micronaut. Make sure you use the correct annotation in your Groovy classes.

Why is it taking much longer than it should to start the application

Micronaut’s startup time is typically very fast. At the application level however, it is possible to affect startup time. If you are seeing slow startup, review any application startup listeners or @Context scope beans that are slowing startup.

Some network issues can also cause slow startup. On the Mac for example, misconfiguration of your /etc/hosts file can cause issues. See the following stackoverflow answer.

22.3 Using Snapshots

Micronaut milestone and stable releases are distributed to Maven Central.

The following snippet shows how to use Micronaut SNAPSHOT versions with Gradle. The latest snapshot will always be the next patch version plus 1 with -SNAPSHOT appended. For example if the latest release is "1.0.1", the current snapshot would be "1.0.2-SNAPSHOT".

ext {
    micronautVersion = '2.4.0-SNAPSHOT'
}
repositories {
    mavenCentral() (1)
    maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/" } (2)
}
1 Micronaut releases are available on Maven Central
2 Micronaut snapshots are available on Sonatype OSS Snapshots

In the case of Maven, edit pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
  ...

  <parent>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-parent</artifactId>
    <version>2.4.0-SNAPSHOT</version>
  </parent>

  <properties>
    <micronaut.version>2.4.0-SNAPSHOT</micronaut.version> (1)
    ...
  </properties>

  <repositories>
    <repository>
      <id>sonatype-snapshots</id>
      <url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url> (2)
    </repository>
  </repositories>

  ...
</project>
1 Set the snapshot version.
2 Micronaut snapshots are available on Sonatype OSS Snapshots
Previously snapshots were published to Bintray, however due to JFrog shutting the service down, snapshots are now being published to Sonatype.

22.4 Common Problems

The following section covers common problems developers encounter when using Micronaut.

Dependency injection is not working

The most common causes of Dependency Injection failing to work are not having the appropriate annotation processor configured, or an incorrectly configured IDE. See the section on Language Support for how to get setup in your language.

A NoSuchMethodError occurs loading beans (Groovy)

By default, Groovy imports classes in the groovy.lang package which includes a class called Singleton. This is an AST transformation annotation that makes your class a singleton by adding a private constructor and static retrieval method. This annotation is easily confused with the jakarta.inject.Singleton annotation used to define singleton beans in Micronaut. Make sure you use the correct annotation in your Groovy classes.

It is taking much longer to start my application than it should (*nix OS)

This is likely due to a bug related to java.net.InetAddress.getLocalHost() calls causing a long delay. The solution is to edit your /etc/hosts file to add an entry containing your host name. To find your host name, run hostname in a terminal. Then edit your /etc/hosts file to add or change entries like the example below, replacing <hostname> with your host name.

127.0.0.1       localhost <hostname>
::1             localhost <hostname>

To learn more about this issue, see this stackoverflow answer

22.5 Breaking Changes

This section documents breaking changes between Micronaut versions

4.0.0

Core Changes

Further Micronaut Modularization

The micronaut-runtime module has been split into separate modules depending on the application’s use case:

Micronaut Discovery Core

micronaut-discovery-core - The base service discovery features are now a separate module. If your application listens for events such as ServiceReadyEvent or HeartBeatEvent this module should be added to the application classpath.

implementation("io.micronaut:micronaut-discovery-core")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-discovery-core</artifactId>
</dependency>

Micronaut Retry

micronaut-retry - The retry implementation including annotations such as @Retryable is now a separate module that can be optionally included in a Micronaut application.

In addition, since micronaut-retry is now optional declarative clients annotated with @Client no longer invoke fallbacks by default. To restore the previous behaviour add micronaut-retry to your classpath and annotate any declarative clients with @Recoverable.

To use the Retry functionality, add the following dependency:

implementation("io.micronaut:micronaut-retry")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-retry</artifactId>
</dependency>

Calling registerSingleton(bean) no longer overrides existing beans

If you call registerSingleton(bean) on the BeanContext it will no longer override existing beans if the type and qualifier match; instead, two beans will exist which may lead to a NonUniqueBeanException.

If you require replacing an existing bean you must formalize the replacement using the RuntimeBeanDefinition API, for example:

context.registerBeanDefinition(
    RuntimeBeanDefinition.builder(Codec.class, ()-> new OverridingCodec())
            .singleton(true)
            // the type of the bean to replace
            .replaces(ToBeReplacedCodec.class)
            .build()
);

WebSocket No Longer Required

io.micronaut:micronaut-http-server no longer exposes micronaut-websocket transitively. If you are using annotations such as @ServerWebSocket, you should add the micronaut-websocket dependency to your application classpath:

implementation("io.micronaut:micronaut-websocket")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-websocket</artifactId>
</dependency>

Reactor Instrumentation Moved to Reactor Module

The instrumentation features for Reactor have been moved to the micronaut-reactor module. If you require instrumentation of reactive code paths (for distributed tracing for example) you should make sure your application depends on micronaut-reactor:

implementation("io.micronaut.reactor:micronaut-reactor")
<dependency>
    <groupId>io.micronaut.reactor</groupId>
    <artifactId>micronaut-reactor</artifactId>
</dependency>

Validation Support Moved to Validation Module

The validation features have been moved to a separate module. Moreover, the new validation module requires you to use micronaut-validation-processor in the annotation processor classpath.

annotationProcessor("io.micronaut.validation:micronaut-validation-processor")
<annotationProcessorPaths>
    <path>
        <groupId>io.micronaut.validation</groupId>
        <artifactId>micronaut-validation-processor</artifactId>
    </path>
</annotationProcessorPaths>

implementation("io.micronaut.validation:micronaut-validation")
<dependency>
    <groupId>io.micronaut.validation</groupId>
    <artifactId>micronaut-validation</artifactId>
</dependency>

Session Support Moved to Session Module

The Session handling features have been moved to their own module. If you use the HTTP session module, change the maven coordinates from io.micronaut:micronaut-session to io.micronaut.session:micronaut-session.

implementation("io.micronaut.session:micronaut-session")
<dependency>
    <groupId>io.micronaut.session</groupId>
    <artifactId>micronaut-session</artifactId>
</dependency>

Kotlin Flow Support Moved to Kotlin Module

Support for the Kotlin Flow type has been moved to the micronaut-kotlin module. If your application uses Kotlin Flow you should ensure the micronaut-kotlin-runtime module is on your application classpath:

implementation("io.micronaut.kotlin:micronaut-kotlin-runtime")
<dependency>
    <groupId>io.micronaut.kotlin</groupId>
    <artifactId>micronaut-kotlin-runtime</artifactId>
</dependency>

Compilation Time API Split into new module

In order to keep the runtime small all types and interfaces that are used at compilation time only (like the io.micronaut.inject.ast API) have been moved into a separate module:

implementation("io.micronaut:micronaut-core-processor")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-core-processor</artifactId>
</dependency>

If you are using types and interfaces from this module you should take care to split the compilation time and runtime logic of your module into separate modules.

ASM No Longer Shaded

ASM is no longer shaded into the io.micronaut.asm package. If you depend on this library you should directly depend on the latest version of ASM.

Caffeine No Longer Shaded

Caffeine is no longer shaded into the io.micronaut.caffeine package. If you depend on this library you should directly depend on the latest version of Caffeine.

Environment Deduction Disabled by Default

In previous versions of the Micronaut framework, probes were used to attempt to deduce the running environment and establish whether the application was running in the Cloud. These probes involved network calls resulting in issues with startup performance and security concerns. These probes are disabled by default and can be re-enabled as necessary by calling ApplicationContextBuilder.deduceCloudEnvironment(true), setting the system property micronaut.env.cloud-deduction to true or setting the environment MICRONAUT_ENV_CLOUD_DEDUCTION to true if your application still requires this functionality.

Update to Groovy 4

Micronaut now uses Groovy 4. This means that Groovy 4 is now the minimum version required to run Groovy Micronaut applications. There have been several core differences in Groovy parsing and behavior for version 4 which can be found in the breaking changes section of the 4.0.0 release notes.

SnakeYAML no longer a direct dependency

SnakeYAML is no longer a direct dependency, if you need YAML configuration you should add SnakeYAML to your classpath explicitly

javax.annotation no longer a directory dependency

The javax.annotation library is no longer a directory dependency. Any references to types in the javax.anotation package should be changed to jakarta.annotation

Kotlin base version updated to 1.8.21

Kotlin has been updated to 1.8.21, which may cause issues when compiling or linking to Kotlin libraries.

Bean Introspection changes

Before, when both METHOD and FIELD were set as the access kind, the bean introspection would choose the same access type to get and set the property value. In Micronaut 4, the accessors can be of different kinds: a field to get and a method to set, and vice versa.

Annotations with retention CLASS are excluded at runtime

Annotations with the retention CLASS are not available in the annotation metadata at the runtime.

Interceptors with multiple interceptor bindings annotations

Interceptors with multiple interceptor binding annotations now require the same set of annotations to be present at the intercepted point. In the Micronaut 3 an interceptor with multiple binding annotations would need at least one of the binding annotations to be present at the intercepted point.

ConversionService and ConversionService.SHARED is no longer mutable

New type converters can be added to MutableConversionService retrieved from the bean context or by declaring a bean of type TypeConverter. To register a type converter into ConversionService.SHARED, the registration needs to be done via the service loader.

ExceptionHandler with POJO response type no longer results in an error response

Previously if you had an ExceptionHandler such as:

@Singleton
public class MyExceptionHandler implements ExceptionHandler<MyException, String> {

    @Override
    public String handle(HttpRequest request, MyException exception) {
        return "caught!";
    }
}

This would result in an internal server error response with caught! as the body. This now returns an OK response. If you want to return a POJO response as an error, you should use the HttpResponse type:

@Singleton
public class MyExceptionHandler implements ExceptionHandler<MyException, HttpResponse<String>> {

    @Override
    public HttpResponse<String> handle(HttpRequest request, MyException exception) {
        return HttpResponse.badRequest("caught!");
    }
}

HttpContentProcessor superseded by MessageBodyHandler API

The netty-specific HttpContentProcessor API has been replaced by a new, experimental MessageBodyHandler API that does not rely on netty and is more powerful. There is no compatibility layer, so the old HttpContentProcessor will stop working and need to be rewritten.

@Body annotation on controller parameters

Before 4.0, the binding logic for controller parameters was more lax. A bare parameter, e.g. void test(String title), could either match a part of the request body (foo if the request body is {"title":"foo"}), come from a query parameter, or could bind to the full request body ({"x":"y"} if the request body is {"x":"y"}).

Binding from the full body to these bare parameters is no longer supported. If you wish to bind the full body, the parameter must be annotated with @Body.

Additionally, it is no longer permitted to mix body component binding with full body binding. For example, void test(@Body Bean bean, String title) will not work anymore if title needs to come from the body that is already bound to bean.

These changes also apply to functions that are exposed using micronaut-function-web.

Delayed body access

When accessing the request body in two places, for example once as a normal controller @Body parameter and then in an error handler, Micronaut HTTP is now stricter about allowed types. If in doubt, for the second body access, call HttpRequest.getBody() and you will get the same body type the first access requested.

text/plain messages are more restrictive about allowed types

For text/plain request and response body reading and writing, in 3.x any type was allowed. For writing, the object was converted using toString, and for reading, the object was converted using ConversionService. For example, if you have a controller that returns an Instant as text/plain, it would write it using toString like 2023-05-25T13:25:02.925Z. In the other direction, if you have a controller with a @Body Instant instant parameter, the same text would be converted to Instant using ConversionService.

This is not permitted anymore for 4.x, except for some restricted types. The recommended fix is to move to application/json as the content type. toString is not a stable serialization format, JSON is more reliable.

Alternatively, you can set the micronaut.http.legacy-text-conversion configuration option to true to restore the old behavior.

OncePerRequestHttpServerFilter removed

Since Micronaut 3.0 the OncePerRequestHttpServerFilter class was deprecated and marked for removal. This class is now removed. Implement HttpServerFilter instead, and replace any usages of micronaut.once attributes with a custom attribute name.

CORS support with the @CrossOrigin annotation

Micronaut Framework 4 changes @CrossOrigin behavior to match configuration-based CORS behavior. A method annotated with @CrossOrigin allows any origin if you don’t specify any value for the allowedOrigins and allowedOriginsRegex members.

@EachBean requires a `@Named qualifier

@EachBean throws a "multiple possible bean candidates found" exception if any parent bean lacks a name qualifier.

Manual Context Propagation

In Micronaut Framework 4, users need to extend the propagation context manually.

Micronaut 3 libraries not compatible with Micronaut 4 applications

In order for Micronaut 3 library beans to be discoverable in an application running Micronaut 4, the library must be recompiled with Micronaut 4 - https://github.com/micronaut-projects/micronaut-core/discussions/9758