@Inject
public Vehicle(Engine<V8> engine) {
this.engine = engine;
}
Table of Contents
Micronaut
Natively Cloud Native
Version:
1 Introduction
Micronaut is a modern, JVM-based, full stack Java framework designed for building modular, easily testable JVM applications with support for Java, Kotlin, and Groovy.
Micronaut is developed by the creators of the Grails framework and takes inspiration from lessons learnt over the years building real-world applications from monoliths to microservices using Spring, Spring Boot and Grails.
Micronaut 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 Micronaut 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 Micronaut 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, Micronaut 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 and Client built on Netty. 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 Micronaut are heavily inspired by Spring and Grails. This is by design, and helps bring developers up to speed quickly.
1.1 What's New?
Micronaut 3.0.0-M5 includes the following changes:
Core Features
Jakarta Inject
The jakarta.inject
annotations are now the default injection annotations for Micronaut 3
Support for JSR-330 Bean Import
Using the @Import annotation it is now possible to import bean definitions into your application where JSR-330 (either javax.inject
or jakarta.inject
annotations) are used in an external library.
See the documentation on Bean Import for more information.
Support for Controlling Annotation Inheritance
AnnotationMetadata inheritance can now be controlled via Java’s @Inherited
annotation. If an annotation is not explicitly annotated with @Inherited
it will not be included in the metadata. See the Annotation Inheritance section of the documentation for more information.
This is an important behavioural change from Micronaut 2.x, see the Breaking Changes section for information on how to upgrade. |
Support Narrowing Injection by Generic Type Arguments
Micronaut can now resolve the correct bean to inject based on the generic type arguments specified on the injection point:
@Inject
Vehicle(Engine<V8> engine) {
this.engine = engine
}
@Singleton
class Vehicle(val engine: Engine<V8>) {
For more information see the section on Qualifying by Generic Type Arguments.
Support for using Annotation Members in Qualifiers
You can now use annotation members in qualifiers and specify which members should be excluded with the new @NonBinding annotation.
For more information see the section on Qualifying By Annotation Members.
Support for Limiting the Injectable Types
You can now limit the exposed types of a bean using the typed
member of the @Bean annotation:
@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
}
For more information see the section on Limiting Injectable Types.
Factories can produce bean from fields
Beans defined with the @Factory annotation can now produce beans from public or package protected fields, for example:
@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)
}
}
@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)
}
}
@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)
}
}
For more information see the Bean Factories section of the documentation.
Enhanced BeanProvider
Interface
The BeanProvider interface has been enhanced with new methods such as iterator()
and stream()
as well as methods to check for bean existence and uniqueness.
New @Any
Qualifier for use in Bean Factories
A new @Any qualifier has been introduced to allow injecting any available instance into an injection point and can be used in combination with the new BeanProvider
interface mentioned above to allow more dynamic behaviour.
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)
}
}
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)
}
}
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)
}
}
The annotation can also be used on @Factory methods to allow customization of how objects are injected via the InjectionPoint API.
Support for Fields in Bean Introspections
Bean introspections on public or package protected fields are now supported:
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
}
}
For more information see the "Bean Fields" section of the Bean Introspections documentation.
ApplicationEventPublisher
has now a generic event type
For the performance reasons it’s advised to inject an instance of ApplicationEventPublisher
with a generic type parameter - ApplicationEventPublisher<MyEvent>
.
AOP Features
Support for Constructor Interception
It is now possible to intercept bean construction invocations through the ConstructorInterceptor interface and @AroundConstruct annotation.
See the section on Bean Life Cycle Advice for more information.
Support for @PostConstruct
& @PreDestroy
Interception
It is now possible to intercept @PostConstruct
and @PreDestroy
method invocations through the MethodInterceptor interface and @InterceptorBinding annotation.
See the section on Bean Life Cycle Advice for more information.
Random Configuration Values
It is now possible to set a max and a range for random numbers in configuration. For example to set an integer between 0 and 9, ${random.int(10)}
can be used as the configuration value. See the documentation under "Using Random Properties" for more information.
Project Reactor used internally instead of RxJava2
Micronaut 3 uses internally Project Reactor instead RxJava 2. Project Reactor allows Micronaut 3 to simplify instrumentation, thanks to Reactor’s Context, simplifies conversion login and eases the integration with R2DBC drivers. We recommend users to migrate to Reactor. However, it is possible to continue to use RxJava. See Reactive Programming section.
Module Upgrades
Micronaut Data 2.4.0
Huge Micronaut Data update including many new features including:
-
Full support for immutable entities. You can use Java 16 records or Kotlin immutable data classes
-
Integrated support for R2DBC, now the
data-r2dbc
module is a part of the data project and shares the same code with JDBC -
Optimistic locking for JDBC/R2DBC
-
Repositories now support batch insert/update/delete even with a custom query
-
Rewritten entity mapper allows more complex mapping for JDBC/R2DBC entities
-
Support for
@JoinTable
and@JoinColumn
annotations -
A lot of bugfixes!
Micronaut Micrometer 3.4.0
The Micrometer module has been upgraded and now supports repeated definitions of the @Timed annotation as well as also supporting the @Counted
annotation for counters when you add the micronaut-micrometer-annotation
dependency to your annotation processor classpath.
Micronaut Oracle Cloud 1.3.0
Micronaut’s Oracle Cloud Integration has been updated with support for Cloud Monitoring and Tracing.
Micronaut Cassandra 4.0.0
The Micronaut Cassandra integration now includes support for GraalVM out of the box.
Other Modules
-
Micronaut Security 2.4.2
-
Micronaut Azure 2.2.0
-
Micronaut Aws 2.6.0
-
Micronaut Grpc 2.4.0
-
Micronaut OpenApi 2.4.0
-
Micronaut Kafka 3.3.0
-
Micronaut Flyway 3.6.0
-
Micronaut Liquibase 3.3.1
-
Micronaut Discovery Client 2.4.0
-
Micronaut ElasticSearch 2.3.0
-
Micronaut Cache 3.0.0
-
Micronaut SQL 4.0.0
Dependency Upgrades
-
Cassandra 4.11.1
-
Kafka 2.8.0
-
GraalVM 21.2.0
-
Liquibase 4.3.4
-
Flyway 7.7.3
-
Elasticsearch 7.12.0
-
Snake YAML 1.29
-
Jackson 2.12.4
-
SLF4J 1.7.29
-
Jaeger 1.6.0
-
Testcontainers 1.15.3
-
Infinispan 12.1.6.Final
-
Hazelcast 4.2.1
-
Caffeine 2.9.1
-
Hibernate 5.5.3.Final
-
JDBI 3.20.1
-
JOOQ 3.14.12
-
Vertx SQL Drivers 4.1.1
-
JAsync 1.2.2
-
Tomcat JDBC 10.0.8
-
Hikari 4.0.3
-
Postgres Driver 42.2.23
-
MySQL Driver 8.0.25
-
MariaDB Driver 2.7.3
-
MS SQL Driver 9.2.1.jre8
-
H2 Database 1.4.200
1.2 Upgrading to Micronaut 2.x
This section covers the steps required to upgrade a Micronaut 2.x application to Micronaut 3.0.0.
Inject Annotations
The javax.inject
annotations are no longer a transitive dependency. Micronaut now ships with the Jakarta inject annotations. To upgrade, either add a dependency on javax-inject
, or replace all javax.inject
imports with jakarta.inject
.
implementation("javax.inject:javax.inject:1")
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
Any code that relied on the presence of the inject annotations to be present in the annotation metadata will still work as expected, however any code that interacts with them will need to be changed to no longer reference the annotation classes themselves. A set of static variables in the AnnotationUtil class can be used in place of the annotation classes when working with annotation metadata.
Nullability Annotations
Micronaut now only comes with its own set of annotations to declare nullability. The findbugs, javax, or jetbrains annotations are all still supported, however it will require an explicit dependency in your build to use them. To upgrade, either add the dependency to the set of annotations you wish to use or switch to the Micronaut @Nullable / @NonNull annotations.
RxJava2
Micronaut no longer ships any reactive implementation as a default in any of our modules or core libraries. Upgrading to Micronaut 3 requires a choice to be made about which reactive streams implementation to use and then to add the relevant dependency to allow for usage of it with the framework.
For those already using RxJava3 or Project Reactor, there should be no changes required to upgrade to Micronaut 3. For those using RxJava2 and wish to continue using RxJava2, a dependency must be added:
implementation("io.micronaut.rxjava2:micronaut-rxjava2")
<dependency>
<groupId>io.micronaut.rxjava2</groupId>
<artifactId>micronaut-rxjava2</artifactId>
</dependency>
In addition, if any of the Rx
HTTP client interfaces were used, a dependency must be added and the imports must be updated.
implementation("io.micronaut.rxjava2:micronaut-rxjava2-http-clent")
<dependency>
<groupId>io.micronaut.rxjava2</groupId>
<artifactId>micronaut-rxjava2-http-clent</artifactId>
</dependency>
Old | New |
---|---|
io.micronaut.http.client.RxHttpClient |
io.micronaut.rxjava2.http.client.RxHttpClient |
io.micronaut.http.client.RxProxyHttpClient |
io.micronaut.rxjava2.http.client.proxy.RxProxyHttpClient |
io.micronaut.http.client.RxStreamingHttpClient |
io.micronaut.rxjava2.http.client.RxStreamingHttpClient |
io.micronaut.http.client.sse.RxSseClient |
io.micronaut.rxjava2.http.client.sse.RxSseClient |
io.micronaut.websocket.RxWebSocketClient |
io.micronaut.rxjava2.http.client.websockets.RxWebSocketClient |
If the Netty based server implementation is being used, another dependency must be added:
implementation("io.micronaut.rxjava2:micronaut-rxjava2-http-server-netty")
<dependency>
<groupId>io.micronaut.rxjava2</groupId>
<artifactId>micronaut-rxjava2-http-server-netty</artifactId>
</dependency>
We recommend switching to Project Reactor as that is the implementation used internally by Micronaut. Adding a dependency to RxJava2 will result in both implementations in the runtime classpath of your application. |
2 Quick Start
The following sections walk you through a Quick Start on how to use Micronaut to setup 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.
To follow the Quick Start it is also recommended that you have the Micronaut CLI installed.
2.1 Build/Install the CLI
The best way to install Micronaut 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
Although not required to use Micronaut, the Micronaut CLI is the quickest way to create a new server application.
Using the 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
|
The previous command creates 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 972ms. 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
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:

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.
Micronaut 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 Visual Studio Code
Micronaut can be set up within Visual Studio Code. 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
Once the extension pack is installed and if you have setup terminal integration just type code .
in any project directory and the project will be automatically setup.
2.4 Creating a Client
As mentioned previously, Micronaut 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.
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)
}
}
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)
}
}
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, Micronaut features a declarative, compile-time HTTP client, powered by the Client annotation.
To create a client, create an interface annotated with @Client
, for example:
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)
}
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)
}
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:
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import io.micronaut.core.async.annotation.SingleResult;
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)
}
}
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)
}
}
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-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
3 Inversion of Control
Unlike other frameworks which rely on runtime reflection and proxies, Micronaut 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 Micronaut 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:
plugins {
id 'io.micronaut.library' version '1.3.2' (1)
}
version "0.1"
group "com.example"
repositories {
mavenCentral()
}
micronaut {
version = "3.0.0-M5" (2)
}
1 | Define the Micronaut Library plugin |
2 | Specify the Micronaut version to use |
The entry point for IoC is then the ApplicationContext interface, which includes a run
method. The following example demonstrates using it:
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 (javax.inject) - Dependency Injection for Java specification, hence to use Micronaut you simply use the annotations provided by javax.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 BeanContext context = BeanContext.run();
Vehicle vehicle = context.getBean(Vehicle.class);
System.out.println(vehicle.start());
def context = BeanContext.run()
Vehicle vehicle = context.getBean(Vehicle)
println vehicle.start()
val context = BeanContext.run()
val vehicle = context.getBean(Vehicle::class.java)
println(vehicle.start())
Micronaut automatically discovers dependency injection metadata on the classpath and wires the beans together according to injection points you define.
Micronaut 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
3.2 How Does it Work?
At this point, you may be wondering how Micronaut 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 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 of the 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 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 javax.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 natively supports injecting the following types:
Type | Description | Example |
---|---|---|
An |
|
|
An |
|
|
A lazy |
|
|
A native array of beans of a given type |
|
|
A |
|
|
A |
|
|
A |
|
There are 3 different provider types supported, however the BeanProvider is the one we suggest to use.
|
When injecting a
In this example, the injected member variable |
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 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.
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);
}
}
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);
}
}
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 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 javax.inject.Named annotation indicates that the V8Engine implementation is required |
5 | Calling the start method prints: "Starting V8" |
Micronaut 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 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 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 7;
}
}
class V6 implements CylinderProvider {
@Override
int getCylinders() { 7 }
}
class V6 : CylinderProvider {
override val cylinders: Int = 7
}
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 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:
import io.micronaut.context.annotation.Primary;
import jakarta.inject.Singleton;
@Primary
@Singleton
class Green implements ColorPicker {
@Override
public String color() {
return "green";
}
}
import io.micronaut.context.annotation.Primary
import jakarta.inject.Singleton
@Primary
@Singleton
class Green implements ColorPicker {
@Override
String color() {
return "green"
}
}
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
.
import jakarta.inject.Singleton;
@Singleton
public class Blue implements ColorPicker {
@Override
public String color() {
return "blue";
}
}
import jakarta.inject.Singleton
@Singleton
class Blue implements ColorPicker {
@Override
String color() {
return "blue"
}
}
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.
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:
@Inject @Any
Engine engine;
@Inject @Any
Engine engine
@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:
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)
}
}
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)
}
}
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:
void startAll() {
if (engineProvider.isPresent()) { (1)
engineProvider.stream().forEach(Engine::start); (2)
}
}
void startAll() {
if (engineProvider.isPresent()) { (1)
engineProvider.each {it.start() } (2)
}
}
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 features an extensible bean scoping mechanism based on JSR-330. The following default scopes are supported:
3.7.1 Built-In Scopes
Type | Description |
---|---|
Singleton scope indicates only one instance of the bean will exist |
|
Context scope indicates that the bean will be created at the same time as the |
|
Prototype scope indicates that a new instance of the bean is created each time it is injected |
|
Infrastructure scope represents a bean that cannot be overridden or replaced using @Replaces because it is critical to the functioning of the system. |
|
|
|
|
|
|
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:
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 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
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
:
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:
-
/refresh
endpoint. -
Publication of a RefreshEvent.
The following example illustrates @Refreshable
scope behavior.
@Refreshable (1)
public 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
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:
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 {
}
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 {
}
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:
@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:
@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 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.
|
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.
|
Beans from Fields
With Micronaut 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.
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:
@Singleton
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public class JdbcBookService implements BookService {
DataSource dataSource;
public JdbcBookService(DataSource dataSource) {
this.dataSource = dataSource;
}
@Singleton
@Requires(beans = DataSource)
@Requires(property = "datasource.url")
class JdbcBookService implements BookService {
DataSource dataSource
@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:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PACKAGE, ElementType.TYPE})
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public @interface RequiresJdbc {
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.PACKAGE, ElementType.TYPE])
@Requires(beans = DataSource)
@Requires(property = "datasource.url")
@interface RequiresJdbc {
}
@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:
@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:
Requirement | Example |
---|---|
Require the presence of one or more classes |
|
Require the absence of one or more classes |
|
Require the presence one or more beans |
|
Require the absence of one or more beans |
|
Require the environment to be applied |
|
Require the environment to not be applied |
|
Require the presence of another configuration package |
|
Require the absence of another configuration package |
|
Require particular SDK version |
|
Requires classes annotated with the given annotations to be available to the application via package scanning |
|
Require a property with an optional value |
|
Require a property to not be part of the configuration |
|
Require the presence of one or more files in the file system |
|
Require the presence of one or more classpath resources |
|
Require the current operating system to be in the list |
|
Require the current operating system to not be in the list |
|
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 |
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.
<logger name="io.micronaut.context.condition" level="DEBUG"/>
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:
@Singleton
@Requires(beans = DataSource.class)
@Requires(property = "datasource.url")
public class JdbcBookService implements BookService {
DataSource dataSource;
public JdbcBookService(DataSource dataSource) {
this.dataSource = dataSource;
}
@Singleton
@Requires(beans = DataSource)
@Requires(property = "datasource.url")
class JdbcBookService implements BookService {
DataSource dataSource
@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:
@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);
}
}
@Replaces(JdbcBookService.class) (1)
@Singleton
class MockBookService implements BookService {
Map<String, Book> bookMap = [:]
@Override
Book findBook(String title) {
bookMap.get(title)
}
}
@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:
@Factory
public class BookFactory {
@Singleton
Book novel() {
return new Book("A Great Novel");
}
@Singleton
TextBook textBook() {
return new TextBook("Learning 101");
}
}
@Factory
class BookFactory {
@Singleton
Book novel() {
new Book('A Great Novel')
}
@Singleton
TextBook textBook() {
new TextBook('Learning 101')
}
}
@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
.
@Factory
@Replaces(factory = BookFactory.class)
public class CustomBookFactory {
@Singleton
Book otherNovel() {
return new Book("An OK Novel");
}
}
@Factory
@Replaces(factory = BookFactory)
class CustomBookFactory {
@Singleton
Book otherNovel() {
new Book('An OK Novel')
}
}
@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.
@Factory
public class TextBookFactory {
@Singleton
@Replaces(value = TextBook.class, factory = BookFactory.class)
TextBook textBook() {
return new TextBook("Learning 305");
}
}
@Factory
class TextBookFactory {
@Singleton
@Replaces(value = TextBook, factory = BookFactory)
TextBook textBook() {
new TextBook('Learning 305')
}
}
@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 Micronaut that the beans defined with the package form a logical grouping.
The @Configuration
annotation is typically applied to package-info
classes. For example:
@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:
@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 auto-configuration 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 Context Starts
To invoke a method when a 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 Context Closes
To invoke a method when the context is closed, use the javax.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 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.
|
3.13 Context Events
Micronaut 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.
Publishing Events
The ApplicationEventPublisher API supports events of any type, although all events that Micronaut 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.
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());
}
}
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())
}
}
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.
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.Test;
import static org.junit.Assert.assertEquals;
public class SampleEventListenerSpec {
@Test
public 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());
}
}
}
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()
}
}
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:
@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;
}
}
@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
}
}
@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:
@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.Test;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
public class SampleEventListenerSpec {
@Test
public 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));
}
}
}
@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()
}
}
@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 application.yml
:
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 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.
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:3.0.0-M5")
<annotationProcessorPaths>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-java</artifactId>
<version>3.0.0-M5</version>
</path>
</annotationProcessorPaths>
For Kotlin, add the micronaut-inject-java dependency in kapt scope, and for Groovy add micronaut-inject-groovy in compileOnly scope.
|
runtime("io.micronaut:micronaut-core:3.0.0-M5")
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-core</artifactId>
<version>3.0.0-M5</version>
<scope>runtime</scope>
</dependency>
Once your build is configured you have a few ways to generate introspection data.
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 |
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
}
}
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. |
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. |
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. |
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.
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.
|
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.
|
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. |
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. |
This feature is currently experimental and may be subject to change in the future. |
3.16 Bean Validation
Since Micronaut 1.2, Micronaut has built-in support for validating beans annotated with javax.validation
annotations. At a minimum include the micronaut-validation
module as a compile dependency:
implementation("io.micronaut:micronaut-validation")
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
</dependency>
Note that Micronaut’s implementation is not currently fully compliant with the Bean Validator specification as the specification heavily relies on reflection-based APIs.
The following features are unsupported at this time:
-
Annotations on generic argument types, since only the Java language supports this feature.
-
Any interaction with the constraint metadata API, since Micronaut uses compile-time generated metadata.
-
XML-based configuration
-
Instead of using
javax.validation.ConstraintValidator
, use ConstraintValidator (io.micronaut.validation.validator.constraints.ConstraintValidator) to define custom constraints, which supports validating annotations at compile time.
Micronaut’s implementation includes the following benefits:
-
Reflection and Runtime Proxy free validation, resulting in reduced memory consumption
-
Smaller JAR size since Hibernate Validator adds another 1.4MB
-
Faster startup since Hibernate Validator adds 200ms+ startup overhead
-
Configurability via Annotation Metadata
-
Support for Reactive Bean Validation
-
Support for validating the source AST at compile time
-
Automatic compatibility with GraalVM native without additional configuration
If you require full Bean Validator 2.0 compliance, add the micronaut-hibernate-validator
module to your build, which replaces Micronaut’s implementation.
implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")
<dependency>
<groupId>io.micronaut.beanvalidation</groupId>
<artifactId>micronaut-hibernate-validator</artifactId>
</dependency>
Validating Bean Methods
You can validate methods of any class declared as a Micronaut bean by applying javax.validation
annotations to arguments:
import jakarta.inject.Singleton;
import javax.validation.constraints.NotBlank;
@Singleton
public class PersonService {
public void sayHello(@NotBlank String name) {
System.out.println("Hello " + name);
}
}
import jakarta.inject.Singleton
import javax.validation.constraints.NotBlank
@Singleton
class PersonService {
void sayHello(@NotBlank String name) {
println "Hello $name"
}
}
import jakarta.inject.Singleton
import javax.validation.constraints.NotBlank
@Singleton
open class PersonService {
open fun sayHello(@NotBlank name: String) {
println("Hello $name")
}
}
The above example declares that the @NotBlank
annotation will be validated when invoking the sayHello
method.
If you use Kotlin, the class and method must be declared open so Micronaut can create a compile-time subclass. Alternatively you can annotate the class with @Validated and configure the Kotlin all-open plugin to open classes annotated with this type. See the Compiler plugins section.
|
A javax.validation.ConstraintViolationException
is thrown if a validation error occurs. For example:
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;
import javax.validation.ConstraintViolationException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@MicronautTest
class PersonServiceSpec {
@Inject PersonService personService;
@Test
void testThatNameIsValidated() {
final ConstraintViolationException exception =
assertThrows(ConstraintViolationException.class, () ->
personService.sayHello("") (1)
);
assertEquals("sayHello.name: must not be blank", exception.getMessage()); (2)
}
}
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification
import jakarta.inject.Inject
import javax.validation.ConstraintViolationException
@MicronautTest
class PersonServiceSpec extends Specification {
@Inject PersonService personService
void "test person name is validated"() {
when:"The sayHello method is called with a blank string"
personService.sayHello("") (1)
then:"A validation error occurs"
def e = thrown(ConstraintViolationException)
e.message == "sayHello.name: must not be blank" // (2)
}
}
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import jakarta.inject.Inject
import javax.validation.ConstraintViolationException
@MicronautTest
class PersonServiceSpec {
@Inject
lateinit var personService: PersonService
@Test
fun testThatNameIsValidated() {
val exception = assertThrows(ConstraintViolationException::class.java) {
personService.sayHello("") (1)
}
assertEquals("sayHello.name: must not be blank", exception.message) (2)
}
}
1 | The method is called with a blank string |
2 | An exception occurs |
Validating Data Classes
To validate data classes, e.g. POJOs (typically used in JSON interchange), the class must be annotated with @Introspected (see the previous section on Bean Introspection) or, if the class is external, be imported by the @Introspected
annotation.
import io.micronaut.core.annotation.Introspected;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
@Introspected
public class Person {
private String name;
@Min(18)
private int age;
@NotBlank
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
@Introspected
class Person {
@NotBlank
String name
@Min(18L)
int age
}
import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
@Introspected
data class Person(
@field:NotBlank var name: String,
@field:Min(18) var age: Int
)
The @Introspected annotation can be used as a meta-annotation; common annotations like @javax.persistence.Entity are treated as @Introspected
|
The above example defines a Person
class that has two properties (name
and age
) that have constraints applied. Note that in Java the annotations can be on the field or the getter, and with Kotlin data classes, the annotation should target the field.
To validate the class manually, inject an instance of Validator:
@Inject
Validator validator;
@Test
void testThatPersonIsValidWithValidator() {
Person person = new Person();
person.setName("");
person.setAge(10);
final Set<ConstraintViolation<Person>> constraintViolations = validator.validate(person); (1)
assertEquals(2, constraintViolations.size()); (2)
}
@Inject Validator validator
void "test person is validated with validator"() {
when:"The person is validated"
def constraintViolations = validator.validate(new Person(name: "", age: 10)) (1)
then:"A validation error occurs"
constraintViolations.size() == 2 // (2)
}
@Inject
lateinit var validator: Validator
@Test
fun testThatPersonIsValidWithValidator() {
val person = Person("", 10)
val constraintViolations = validator.validate(person) (1)
assertEquals(2, constraintViolations.size) (2)
}
1 | The validator validates the person |
2 | The constraint violations are verified |
Alternatively on Bean methods you can use javax.validation.Valid
to trigger cascading validation:
@Singleton
public class PersonService {
public void sayHello(@Valid Person person) {
System.out.println("Hello " + person.getName());
}
}
@Singleton
class PersonService {
void sayHello(@Valid Person person) {
println "Hello $person.name"
}
}
@Singleton
open class PersonService {
open fun sayHello(@Valid person: Person) {
println("Hello ${person.name}")
}
}
The PersonService
now validates the Person
class when invoked:
@Inject
PersonService personService;
@Test
void testThatPersonIsValid() {
Person person = new Person();
person.setName("");
person.setAge(10);
final ConstraintViolationException exception =
assertThrows(ConstraintViolationException.class, () ->
personService.sayHello(person) (1)
);
assertEquals(2, exception.getConstraintViolations().size()); (2)
}
@Inject PersonService personService
void "test person name is validated"() {
when:"The sayHello method is called with an invalid person"
personService.sayHello(new Person(name: "", age: 10)) (1)
then:"A validation error occurs"
def e = thrown(ConstraintViolationException)
e.constraintViolations.size() == 2 // (2)
}
@Inject
lateinit var personService: PersonService
@Test
fun testThatPersonIsValid() {
val person = Person("", 10)
val exception = assertThrows(ConstraintViolationException::class.java) {
personService.sayHello(person) (1)
}
assertEquals(2, exception.constraintViolations.size) (2)
}
1 | A validated method is invoked |
2 | The constraint violations are verified |
Validating Configuration Properties
You can also validate the properties of classes that are annotated with @ConfigurationProperties to ensure configuration is correct.
It is recommended that you annotate @ConfigurationProperties that features validation with @Context to ensure that the validation occurs at startup. |
Defining Additional Constraints
To define additional constraints, create a new annotation, for example:
import javax.validation.Constraint;
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.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
@Constraint(validatedBy = { }) (1)
public @interface DurationPattern {
String message() default "invalid duration ({validatedValue})"; (2)
/**
* Defines several constraints on the same element.
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
DurationPattern[] value(); (3)
}
}
import javax.validation.Constraint
import java.lang.annotation.Retention
import static java.lang.annotation.RetentionPolicy.RUNTIME
@Retention(RUNTIME)
@Constraint(validatedBy = []) (1)
@interface DurationPattern {
String message() default "invalid duration ({validatedValue})" (2)
}
import javax.validation.Constraint
import kotlin.annotation.AnnotationRetention.RUNTIME
@Retention(RUNTIME)
@Constraint(validatedBy = []) (1)
annotation class DurationPattern(
val message: String = "invalid duration ({validatedValue})" (2)
)
1 | The annotation should be annotated with javax.validation.Constraint |
2 | A message template can be provided in a hard-coded manner as above. If none is specified, Micronaut tries to find a message using ClassName.message using the MessageSource interface (optional) |
3 | To support repeated annotations you can define an inner annotation (optional) |
You can add messages and message bundles using the MessageSource and ResourceBundleMessageSource classes. |
Once you have defined the annotation, implement a ConstraintValidator that validates the annotation. You can either create a bean class that implements the interface directly or define a factory that returns one or more validators.
The latter approach is recommended if you plan to define multiple validators:
import io.micronaut.context.annotation.Factory;
import io.micronaut.validation.validator.constraints.ConstraintValidator;
import jakarta.inject.Singleton;
@Factory
public class MyValidatorFactory {
@Singleton
ConstraintValidator<DurationPattern, CharSequence> durationPatternValidator() {
return (value, annotationMetadata, context) -> {
context.messageTemplate("invalid duration ({validatedValue}), additional custom message"); (1)
return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$");
};
}
}
import io.micronaut.context.annotation.Factory
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.validation.validator.constraints.ConstraintValidator
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext
import jakarta.inject.Singleton
@Factory
class MyValidatorFactory {
@Singleton
ConstraintValidator<DurationPattern, CharSequence> durationPatternValidator() {
return { CharSequence value,
AnnotationValue<DurationPattern> annotation,
ConstraintValidatorContext context ->
context.messageTemplate("invalid duration ({validatedValue}), additional custom message") (1)
return value == null || value.toString() ==~ /^PT?[\d]+[SMHD]{1}$/
} as ConstraintValidator<DurationPattern, CharSequence>
}
}
import io.micronaut.context.annotation.Factory
import io.micronaut.validation.validator.constraints.ConstraintValidator
import jakarta.inject.Singleton
@Factory
class MyValidatorFactory {
@Singleton
fun durationPatternValidator() : ConstraintValidator<DurationPattern, CharSequence> {
return ConstraintValidator { value, annotation, context ->
context.messageTemplate("invalid duration ({validatedValue}), additional custom message") (1)
value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex())
}
}
}
1 | Override the default message template with an inline call for more control over the validation error message. (Since 2.5.0 ) |
The above example implements a validator that validates any field, parameter etc. that is annotated with DurationPattern
, ensuring that the string can be parsed with java.time.Duration.parse
.
Generally null is regarded as valid and @NotNull is used to constrain a value as not being null . The example above regards null as a valid value.
|
For example:
@Singleton
public class HolidayService {
public String startHoliday(@NotBlank String person,
@DurationPattern String duration) {
final Duration d = Duration.parse(duration);
return "Person " + person + " is off on holiday for " + d.toMinutes() + " minutes";
}
}
@Singleton
class HolidayService {
String startHoliday(@NotBlank String person,
@DurationPattern String duration) {
final Duration d = Duration.parse(duration)
return "Person $person is off on holiday for ${d.toMinutes()} minutes"
}
}
@Singleton
open class HolidayService {
open fun startHoliday(@NotBlank person: String,
@DurationPattern duration: String): String {
val d = Duration.parse(duration)
return "Person $person is off on holiday for ${d.toMinutes()} minutes"
}
}
To verify the above examples validates the duration
parameter, define a test:
@Inject HolidayService holidayService;
@Test
void testCustomValidator() {
final ConstraintViolationException exception =
assertThrows(ConstraintViolationException.class, () ->
holidayService.startHoliday("Fred", "junk") (1)
);
assertEquals("startHoliday.duration: invalid duration (junk), additional custom message", exception.getMessage()); (2)
}
void "test test custom validator"() {
when:"A custom validator is used"
holidayService.startHoliday("Fred", "junk") (1)
then:"A validation error occurs"
def e = thrown(ConstraintViolationException)
e.message == "startHoliday.duration: invalid duration (junk), additional custom message" // (2)
}
@Inject
lateinit var holidayService: HolidayService
@Test
fun testCustomValidator() {
val exception = assertThrows(ConstraintViolationException::class.java) {
holidayService.startHoliday("Fred", "junk") (1)
}
assertEquals("startHoliday.duration: invalid duration (junk), additional custom message", exception.message) (2)
}
1 | A validated method is invoked |
2 | THe constraint violations are verified |
Validating Annotations at Compile Time
You can use Micronaut’s validator to validate annotation elements at compile time by including micronaut-validation
in the annotation processor classpath:
annotationProcessor("io.micronaut:micronaut-validation")
<annotationProcessorPaths>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
</path>
</annotationProcessorPaths>
Then Micronaut will at compile time validate annotation values that are themselves annotated with javax.validation
. For example consider the following annotation:
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
public @interface TimeOff {
@DurationPattern
String duration();
}
import java.lang.annotation.Retention
import static java.lang.annotation.RetentionPolicy.RUNTIME
@Retention(RUNTIME)
@interface TimeOff {
@DurationPattern
String duration()
}
import kotlin.annotation.AnnotationRetention.RUNTIME
@Retention(RUNTIME)
annotation class TimeOff(
@DurationPattern val duration: String
)
If you attempt to use @TimeOff(duration="junk")
in your source, Micronaut will fail compilation due to the duration
value violating the DurationPattern
constraint.
If duration is a property placeholder such as @TimeOff(duration="${my.value}") , validation is deferred until runtime.
|
Note that to use a custom ConstraintValidator
at compile time you must instead define the validator as a class:
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.validation.validator.constraints.ConstraintValidator;
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;
public class DurationPatternValidator implements ConstraintValidator<DurationPattern, CharSequence> {
@Override
public boolean isValid(
@Nullable CharSequence value,
@NonNull AnnotationValue<DurationPattern> annotationMetadata,
@NonNull ConstraintValidatorContext context) {
return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$");
}
}
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.annotation.Nullable
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.validation.validator.constraints.ConstraintValidator
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext
class DurationPatternValidator implements ConstraintValidator<DurationPattern, CharSequence> {
@Override
boolean isValid(
@Nullable CharSequence value,
@NonNull AnnotationValue<DurationPattern> annotationMetadata,
@NonNull ConstraintValidatorContext context) {
return value == null || value.toString() ==~ /^PT?[\d]+[SMHD]{1}$/
}
}
import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.validation.validator.constraints.ConstraintValidator
import io.micronaut.validation.validator.constraints.ConstraintValidatorContext
class DurationPatternValidator : ConstraintValidator<DurationPattern, CharSequence> {
override fun isValid(
value: CharSequence?,
annotationMetadata: AnnotationValue<DurationPattern>,
context: ConstraintValidatorContext): Boolean {
return value == null || value.toString().matches("^PT?[\\d]+[SMHD]{1}$".toRegex())
}
}
Additionally:
-
Define a
META-INF/services/io.micronaut.validation.validator.constraints.ConstraintValidator
file that references the class. -
The class must be public and have a public no-argument constructor
-
The class must be on the annotation processor classpath of the project to be validated.
3.17 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.
Micronaut 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:
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:
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
Micronaut will respect the rules defined in Java’s AnnotatedElement API with regards 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 thegetDeclaredAnnotation*
methods. -
Annotations not meta-annotated with Inherited will not be included in the metadata
Micronaut differs from the AnnotatedElement API in that it extends these rules to methods and method parameters such that:
-
Any annotations annotated with Inherited and present on a method of interface or super class
A
that is overridden by child interface or classB
will be inherited into the AnnotationMetadata retrievable via the ExecutableMethod API from a BeanDefinition or an AOP interceptor. -
Any annotations annotated with Inherited and present on a method parameter of interface or super class
A
that is overridden by child interface or classB
will be inherited into the AnnotationMetadata retrievable via the Argument interface from thegetArguments
method of the ExecutableMethod API.
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:
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 "";
}
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 "";
}
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:
@SqlRepository
public abstract class BaseSqlRepository {
}
@SqlRepository
abstract class BaseSqlRepository {
}
@SqlRepository
abstract class BaseSqlRepository
And then a subclass will inherit all the annotations:
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;
}
}
import jakarta.inject.Named
import javax.sql.DataSource
@Named("bookRepository")
class BookRepository extends BaseSqlRepository {
private final DataSource dataSource
BookRepository(DataSource dataSource) {
this.dataSource = dataSource
}
}
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:
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.
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)
final AnnotationValueBuilder<Introspected> builder = AnnotationValue.builder(Introspected.class)
// don't bother with transients properties
.member("excludedAnnotations", "javax.persistence.Transient"); (2)
return Arrays.asList(
builder.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, in this case @Transient . |
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.18 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 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 Micronaut will not recurse through sub-packages so sub-packages need to be listed explicitly |
3 | By default Micronaut 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.19 Micronaut Beans And Spring
Micronaut has integrations with Spring in several forms. See the Micronaut Spring Documentation for more information.
3.20 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:
dependencies {
...
annotationProcessor "io.micronaut:micronaut-inject-java:3.0.0-M5"
compileOnly "io.micronaut:micronaut-inject-java:3.0.0-M5"
...
}
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 javax.inject
package as invalid unless you use Dagger:
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:
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
Configuration in Micronaut takes inspiration from both Spring Boot and Grails, integrating configuration properties from multiple sources directly into the core IoC container.
Configuration can by default be provided in Java properties, YAML, JSON, or Groovy files. The convention is to search for a file named application.yml
, application.properties
, application.json
or application.groovy
.
In addition, like Spring and Grails, Micronaut 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.
ApplicationContext applicationContext = ApplicationContext.run("test", "android");
Environment environment = applicationContext.getEnvironment();
assertTrue(environment.getActiveNames().contains("test"));
assertTrue(environment.getActiveNames().contains("android"));
when:
ApplicationContext applicationContext = ApplicationContext.run("test", "android")
Environment environment = applicationContext.getEnvironment()
then:
environment.activeNames.contains("test")
environment.activeNames.contains("android")
val applicationContext = ApplicationContext.run("test", "android")
val environment = applicationContext.environment
assertTrue(environment.activeNames.contains("test"))
assertTrue(environment.activeNames.contains("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, Micronaut 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:
$ java -Dmicronaut.environments=foo,bar -jar myapp.jar
The above activates environments called foo
and bar
.
Finally, Cloud environment names are also detected. See the section on Cloud Configuration for more information.
Environment Priority
Micronaut 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.
Micronaut 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 Micronaut from detecting current environments, while still using any environments that are specifically provided as shown above.
$ java -Dmicronaut.env.deduction=false -jar myapp.jar
Alternatively, you can disable environment deduction using the ApplicationContextBuilder deduceEnvironment
method when setting up your application.
@Test
public void testDisableEnvironmentDeductionViaBuilder() {
ApplicationContext ctx = ApplicationContext.builder().deduceEnvironment(false).start();
assertFalse(ctx.getEnvironment().getActiveNames().contains(Environment.TEST));
ctx.close();
}
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()
}
"test disable environment deduction via builder"() {
val ctx = ApplicationContext.builder().deduceEnvironment(false).start()
assertFalse(ctx.environment.activeNames.contains(Environment.TEST))
ctx.close()
}
Default Environment
Micronaut 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, modify your application main method:
public static void main(String[] args) {
Micronaut.build(args)
.mainClass(Application.class)
.defaultEnvironments("dev")
.start();
}
Micronaut Banner
Since Micronaut 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 (3.0.0-M5)
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.
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")
);
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")
val applicationContext = ApplicationContext.run(
PropertySource.of(
"test",
mapOf(
"micronaut.server.host" to "foo",
"micronaut.server.port" to 8080
)
),
"test", "android"
)
val environment = applicationContext.environment
assertEquals(
"foo",
environment.getProperty("micronaut.server.host", String::class.java).orElse("localhost")
)
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 by default contains PropertySourceLoader
implementations that load properties from the given locations and priority:
-
Command line arguments
-
Properties from
SPRING_APPLICATION_JSON
(for Spring compatibility) -
Properties from
MICRONAUT_APPLICATION_JSON
-
Java System Properties
-
OS environment variables
-
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 aclasspath:
prefix. -
Environment-specific properties from
application-{environment}.{extension}
-
Application-specific properties from
application.{extension}
.properties , .json , .yml are supported out of the box. For Groovy users .groovy is supported as well.
|
Supplying Configuration via Command Line
Configuration can be supplied at the command line using Gradle or our Maven plugin. For example:
$ ./gradlew run --args="-endpoints.health.enabled=true -config.property=test"
$ ./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
}
}
Property Value Placeholders
Micronaut 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 in application.yml
:
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:
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:
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
.
Micronaut 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:
Configuration Value | Resulting Properties | Property Source |
---|---|---|
|
|
Properties, YAML etc. |
|
|
Properties, YAML etc. |
|
|
Properties, YAML etc. |
|
|
Environment Variable |
|
|
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.
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();
}
}
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()
}
}
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
instance:
id: ${random.shortuuid}
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]}
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. |
Controlling Log Levels with Properties
Log levels can be configured via properties defined in application.yml
(and environment variables) with the logger.levels
prefix:
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
.
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: OFF (1)
-
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, Micronaut 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.
4.3 Configuration Injection
You can inject configuration values into beans using the @Value annotation.
Using the @Value
Annotation
Consider the following 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";
}
}
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"
}
}
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). |
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:
@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:
@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:
@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:
@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:
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;
}
}
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
}
}
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.url
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:
application.yml
configurationdatasources:
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:
@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 Configuration Properties
You can create type-safe configuration by creating classes that are annotated with @ConfigurationProperties.
Micronaut 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:
import io.micronaut.context.annotation.ConfigurationProperties;
import javax.validation.constraints.Min;
import javax.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;
}
}
}
import io.micronaut.context.annotation.ConfigurationProperties
import javax.validation.constraints.Min
import javax.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)
}
}
import io.micronaut.context.annotation.ConfigurationProperties
import java.util.Optional
import javax.validation.constraints.Min
import javax.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 javax.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:
@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;
}
}
@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)}]"
}
}
@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:
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());
ApplicationContext applicationContext = ApplicationContext.run(
['my.engine.cylinders': '8'],
"test"
)
def vehicle = applicationContext.getBean(Vehicle)
println(vehicle.start())
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]"
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 (setConnectionTimeout
→ connectionTimeout
).
Property Type Conversion
Micronaut 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.
Micronaut 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:
Configuration Value | Resulting Value |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
For example to configure the default HTTP client read timeout:
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:
my:
app:
integers:
- 1
- 2
urls:
- http://foo.com
- http://bar.com
Or in Java properties file format:
my.app.integers=1,2
my.app.urls=http://foo.com,http://bar.com
Alternatively you can use an index:
my.app.integers[0]=1
my.app.integers[1]=2
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:
@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:
Configuration Value | Resulting Value |
---|---|
|
10 megabytes |
|
10 kilobytes |
|
10 gigabytes |
|
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.
@Format
for Datespublic 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 Micronaut where to look for configuration values. By default, builder methods use the configuration prefix specified in a class-level @ConfigurationProperties annotation.
For 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;
}
}
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
}
}
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.
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());
}
}
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)
}
}
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:
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());
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())
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).
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.convert.format.MapFormat;
import javax.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;
}
}
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.convert.format.MapFormat
import javax.validation.constraints.Min
@ConfigurationProperties('my.engine')
class EngineConfig {
@Min(1L)
int cylinders
@MapFormat(transformation = MapFormat.MapTransformation.FLAT) (1)
Map<Integer, String> sensors
}
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.convert.format.MapFormat
import javax.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) |
@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;
}
}
@Singleton
class EngineImpl implements Engine {
@Inject EngineConfig config
@Override
Map getSensors() {
config.sensors
}
@Override
String start() {
"Engine Starting V$config.cylinders [sensors=${sensors.size()}]"
}
}
@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.
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());
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())
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]"
4.5 Custom Type Converters
Micronaut 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;
@BeforeClass
public static void setupCtx() {
ctx = ApplicationContext.run(
new LinkedHashMap<String, Object>() {{
put("myapp.updatedAt", (1)
new LinkedHashMap<String, Integer>() {{
put("day", 28);
put("month", 10);
put("year", 1982);
}}
);
}}
);
}
@AfterClass
public 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.core.convert.ConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.TypeConverter;
import jakarta.inject.Singleton;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.util.Map;
import java.util.Optional;
@Singleton
public class MapToLocalDateConverter implements TypeConverter<Map, LocalDate> { (1)
@Override
public Optional<LocalDate> convert(Map propertyMap, Class<LocalDate> targetType, ConversionContext context) {
Optional<Integer> day = ConversionService.SHARED.convert(propertyMap.get("day"), Integer.class);
Optional<Integer> month = ConversionService.SHARED.convert(propertyMap.get("month"), Integer.class);
Optional<Integer> year = ConversionService.SHARED.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())); (2)
} catch (DateTimeException e) {
context.reject(propertyMap, e); (3)
return Optional.empty();
}
}
return Optional.empty();
}
}
import io.micronaut.core.convert.ConversionContext
import io.micronaut.core.convert.ConversionService
import io.micronaut.core.convert.TypeConverter
import jakarta.inject.Singleton
import java.time.DateTimeException
import java.time.LocalDate
@Singleton
class MapToLocalDateConverter implements TypeConverter<Map, LocalDate> { (1)
@Override
Optional<LocalDate> convert(Map propertyMap, Class<LocalDate> targetType, ConversionContext context) {
Optional<Integer> day = ConversionService.SHARED.convert(propertyMap.day, Integer)
Optional<Integer> month = ConversionService.SHARED.convert(propertyMap.month, Integer)
Optional<Integer> year = ConversionService.SHARED.convert(propertyMap.year, Integer)
if (day.present && month.present && year.present) {
try {
return Optional.of(LocalDate.of(year.get(), month.get(), day.get())) (2)
} catch (DateTimeException e) {
context.reject(propertyMap, e) (3)
return Optional.empty()
}
}
return Optional.empty()
}
}
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
import jakarta.inject.Singleton
@Singleton
class MapToLocalDateConverter : TypeConverter<Map<*, *>, LocalDate> { (1)
override fun convert(propertyMap: Map<*, *>, targetType: Class<LocalDate>, context: ConversionContext): Optional<LocalDate> {
val day = ConversionService.SHARED.convert(propertyMap["day"], Int::class.java)
val month = ConversionService.SHARED.convert(propertyMap["month"], Int::class.java)
val year = ConversionService.SHARED.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())) (2)
} catch (e: DateTimeException) {
context.reject(propertyMap, e) (3)
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 implementation delegates to the default shared conversion service to convert the values from the Map used to create a LocalDate |
3 | If an exception occurs during binding, call reject(..) which propagates additional information to the container |
4.6 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 property. As an example consider the following class:
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;
}
}
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
}
}
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. |
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:
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")
));
ApplicationContext applicationContext = ApplicationContext.run(PropertySource.of(
"test",
[
"test.datasource.one.url": "jdbc:mysql://localhost/one",
"test.datasource.two.url": "jdbc:mysql://localhost/two"
]
))
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:
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()
);
when:
Collection<DataSourceConfiguration> beansOfType = applicationContext.getBeansOfType(DataSourceConfiguration.class)
assertEquals(2, beansOfType.size()) (1)
DataSourceConfiguration firstConfig = applicationContext.getBean(
DataSourceConfiguration.class,
Qualifiers.byName("one") (2)
)
then:
new URI("jdbc:mysql://localhost/one") == firstConfig.getUrl()
val beansOfType = applicationContext.getBeansOfType(DataSourceConfiguration::class.java)
assertEquals(2, beansOfType.size) (1)
val firstConfig = applicationContext.getBean(
DataSourceConfiguration::class.java,
Qualifiers.byName("one") (2)
)
assertEquals(
URI("jdbc:mysql://localhost/one"),
firstConfig.url
)
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 Micronaut that the class is bound from a list. Simply set the list
member on the annotation to true.
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;
}
}
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
}
}
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.7 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:
@Factory (1)
public class DataSourceFactory {
@EachBean(DataSourceConfiguration.class) (2)
DataSource dataSource(DataSourceConfiguration configuration) { (3)
URI url = configuration.getUrl();
return new DataSource(url);
}
@Factory (1)
class DataSourceFactory {
@EachBean(DataSourceConfiguration) (2)
DataSource dataSource(DataSourceConfiguration configuration) { (3)
URI url = configuration.url
return new DataSource(url)
}
@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 |
Note that @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:
Collection<DataSource> beansOfType = applicationContext.getBeansOfType(DataSource.class);
assertEquals(2, beansOfType.size()); (1)
DataSource firstConfig = applicationContext.getBean(
DataSource.class,
Qualifiers.byName("one") (2)
);
when:
Collection<DataSource> beansOfType = applicationContext.getBeansOfType(DataSource)
assertEquals(2, beansOfType.size()) (1)
DataSource firstConfig = applicationContext.getBean(
DataSource,
Qualifiers.byName("one") (2)
)
val beansOfType = applicationContext.getBeansOfType(DataSource::class.java)
assertEquals(2, beansOfType.size) (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.8 Immutable Configuration
Since 1.3, Micronaut supports the definition of immutable configuration.
There are two ways to define immutable configuration. The preferred way is to define an interface annotated with @ConfigurationProperties. For example:
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.bind.annotation.Bindable;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.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)
}
}
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.bind.annotation.Bindable
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.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)
}
}
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.bind.annotation.Bindable
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.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 Micronaut 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:
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 javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.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);
}
}
}
import io.micronaut.context.annotation.ConfigurationInject
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.bind.annotation.Bindable
import javax.annotation.Nullable
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.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)
}
}
}
import io.micronaut.context.annotation.ConfigurationInject
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.core.bind.annotation.Bindable
import java.util.Optional
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.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 Micronaut 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 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:
1 | Inject the EngineConfig bean |
2 | Use the configuration properties |
Configuration values can then be supplied when running the application. For example:
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());
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())
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]"
4.9 JMX Support
Micronaut 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 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:
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 {
}
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 {
}
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 Micronaut 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:
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)
}
}
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)
}
}
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 the javax.inject.Singleton annotation is optional, and a new interceptor would be created for each instance (prototype scope). |
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:
import jakarta.inject.Singleton;
@Singleton
public class NotNullExample {
@NotNull
void doWork(String taskName) {
System.out.println("Doing job: " + taskName);
}
}
import jakarta.inject.Singleton
@Singleton
class NotNullExample {
@NotNull
void doWork(String taskName) {
println "Doing job: $taskName"
}
}
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
:
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void testNotNull() {
try (ApplicationContext applicationContext = ApplicationContext.run()) {
NotNullExample exampleBean = applicationContext.getBean(NotNullExample.class);
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Null parameter [taskName] not allowed");
exampleBean.doWork(null);
}
}
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()
}
@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 tofalse
) - If set totrue
, instead of a subclass that callssuper
, the proxy delegates to the original bean instance -
hotswap
(defaults tofalse
) - Same asproxyTarget=true
, but in addition the proxy implements HotSwappableInterceptedProxy which wraps each method call in aReentrantReadWriteLock
and allows swapping the target instance at runtime. -
lazy
(defaults tofalse
) - By default Micronaut eagerly initializes the proxy target when the proxy is created. If set totrue
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:
Consider the following two examples:
@Factory
@Timed
@Factory
public class MyFactory {
@Prototype
public MyBean myBean() {
return new MyBean();
}
}
@Factory
@Timed
@Factory
class MyFactory {
@Prototype
MyBean myBean() {
new MyBean()
}
}
@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:
@Factory
@Factory
public class MyFactory {
@Prototype
@Timed
public MyBean myBean() {
return new MyBean();
}
}
@Factory
@Factory
class MyFactory {
@Prototype
@Timed
MyBean myBean() {
new MyBean()
}
}
@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’s Client annotation is another example of introduction advice where Micronaut 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:
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 "";
}
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 ""
}
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:
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)
}
}
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)
}
}
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
:
@Stub
public interface StubExample {
@Stub("10")
int getNumber();
LocalDateTime getDate();
}
@Stub
interface StubExample {
@Stub("10")
int getNumber()
LocalDateTime getDate()
}
@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
:
StubExample stubExample = applicationContext.getBean(StubExample.class);
assertEquals(10, stubExample.getNumber());
assertNull(stubExample.getDate());
when:
def stubExample = applicationContext.getBean(StubExample)
then:
stubExample.number == 10
stubExample.date == null
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 Micronaut 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. |
Micronaut 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
Micronaut supports these 3 use cases by allowing the definition of additional @InterceptorBinding meta-annotations.
Consider the following annotation definition:
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 {
}
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 {
}
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:
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;
}
}
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
}
}
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:
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;
};
}
}
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
}
}
}
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().toUpperCase()
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:
@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();
}
};
}
@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()
}
}
}
@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 javax.validation
annotations such as @NotNull
, @Min
, and @Max
.
Micronaut provides native support for the javax.validation
annotations with the micronaut-validation
dependency:
implementation("io.micronaut:micronaut-validation")
<dependency>
<groupId>io.micronaut</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, Micronaut 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 with application.yml
:
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 |
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, Micronaut 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, Micronaut includes a Retryable annotation.
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 an exponential delay of one second between each retry. (first attempt with 1s delay, second attempt with 2s delay, third attempt with 3s delay).
For example:
@Retryable
public List<Book> listBooks() {
// ...
@Retryable
List<Book> listBooks() {
// ...
@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.
Note also that the @Retryable
annotation can be applied on interfaces, and the behaviour is inherited through annotation metadata. The implication of this is that @Retryable
can be used in combination with Introduction Advice such as the HTTP Client annotation.
To customize retry behaviour, set the attempts
and delay
members, For example to configure five attempts with a two second delay:
@Retryable(attempts = "5",
delay = "2s")
public Book findBook(String title) {
// ...
@Retryable(attempts = "5",
delay = "2s")
Book findBook(String title) {
// ...
@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:
@Retryable(attempts = "${book.retry.attempts:3}",
delay = "${book.retry.delay:1s}")
public Book getBook(String title) {
// ...
@Retryable(attempts = '${book.retry.attempts:3}',
delay = '${book.retry.delay:1s}')
Book getBook(String title) {
// ...
@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:
@Retryable
public Publisher<Book> streamBooks() {
// ...
@Retryable
Flux<Book> streamBooks() {
// ...
@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).
@CircuitBreaker(reset = "30s")
public List<Book> findBooks() {
// ...
@CircuitBreaker(reset = "30s")
List<Book> findBooks() {
// ...
@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, Micronaut 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.
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:
@Scheduled(fixedRate = "5m")
void everyFiveMinutes() {
System.out.println("Executing everyFiveMinutes()");
}
@Scheduled(fixedRate = "5m")
void everyFiveMinutes() {
println "Executing everyFiveMinutes()"
}
@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:
@Scheduled(fixedDelay = "5m")
void fiveMinutesAfterLastExecution() {
System.out.println("Executing fiveMinutesAfterLastExecution()");
}
@Scheduled(fixedDelay = "5m")
void fiveMinutesAfterLastExecution() {
println "Executing fiveMinutesAfterLastExecution()"
}
@Scheduled(fixedDelay = "5m")
internal fun fiveMinutesAfterLastExecution() {
println("Executing fiveMinutesAfterLastExecution()")
}
Scheduling a Cron Task
To schedule a Cron task use the cron
member:
@Scheduled(cron = "0 15 10 ? * MON")
void everyMondayAtTenFifteenAm() {
System.out.println("Executing everyMondayAtTenFifteenAm()");
}
@Scheduled(cron = "0 15 10 ? * MON")
void everyMondayAtTenFifteenAm() {
println "Executing everyMondayAtTenFifteenAm()"
}
@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:
@Scheduled(initialDelay = "1m")
void onceOneMinuteAfterStartup() {
System.out.println("Executing onceOneMinuteAfterStartup()");
}
@Scheduled(initialDelay = "1m")
void onceOneMinuteAfterStartup() {
println "Executing onceOneMinuteAfterStartup()"
}
@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:
@Scheduled(fixedRate = "${my.task.rate:5m}",
initialDelay = "${my.task.delay:1m}")
void configuredTask() {
System.out.println("Executing configuredTask()");
}
@Scheduled(fixedRate = '${my.task.rate:5m}',
initialDelay = '${my.task.delay:1m}')
void configuredTask() {
println "Executing configuredTask()"
}
@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 using application.yml
, for example:
micronaut:
executors:
scheduled:
type: scheduled
core-pool-size: 30
Property | Type | Description |
---|---|---|
|
java.lang.Integer |
|
|
||
|
java.lang.Integer |
|
|
java.lang.Integer |
|
|
java.lang.Class |
|
|
java.lang.String |
Sets the executor name. |
|
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, Micronaut 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 Micronaut’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 Micronaut and configure beans to be used within Micronaut.
You may also wish to leverage existing AOP advice from Spring. One example of this is Spring’s support for declarative transactions with @Transactional
.
Micronaut 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>
If you use Micronaut’s Hibernate support you already get this dependency and a HibernateTransactionManager is configured for you.
|
This is done by defining a Micronaut @Transactional annotation that uses @AliasFor in a manner that every time you set a value with @Transactional it aliases the value to the equivalent value in Spring’s version of @Transactional
.
The benefit here is you can use Micronaut’s compile-time, reflection-free AOP to declare programmatic Spring transactions. For example:
import io.micronaut.spring.tx.annotation.*;
...
@Transactional
public Book saveBook(String title) {
...
}
Micronaut’s version of @Transactional is meta-annotated with @Blocking, ensuring that all methods annotated with it use the I/O thread pool when executing within the HTTP server |
6 The HTTP Server
Using the CLI
If you create your project using the Micronaut CLI |
Micronaut includes both non-blocking HTTP server and client APIs based on Netty.
The design of the HTTP server in Micronaut 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:
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)
}
}
To run the application from a unit test, use the EmbeddedServer interface:
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)
}
}
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)
}
}
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
This is also configurable from an environment variable, e.g. MICRONAUT_SERVER_PORT=8086
|
To run on a random port:
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 a 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:
UriMatchTemplate template = UriMatchTemplate.of("/hello/{name}");
assertTrue(template.match("/hello/John").isPresent()); (1)
assertEquals("/hello/John", template.expand( (2)
Collections.singletonMap("name", "John")
));
given:
UriMatchTemplate template = UriMatchTemplate.of("/hello/{name}")
expect:
template.match("/hello/John").isPresent() (1)
template.expand(["name": "John"]) == "/hello/John" (2)
val template = UriMatchTemplate.of("/hello/{name}")
assertTrue(template.match("/hello/John").isPresent) (1)
assertEquals("/hello/John", template.expand(mapOf("name" to "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. For 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(@PathVariable Integer number) { (3)
return "Issue # " + number + "!"; (4)
}
}
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(@PathVariable Integer number) { (3)
"Issue # " + number + "!" (4)
}
}
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(@PathVariable number: Int): String { (3)
return "Issue # $number!" (4)
}
}
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 can optionally be annotated with PathVariable |
4 | The value of the URI variable is referenced in the implementation |
Micronaut maps the URI /issues/{number}
for the above controller. We can assert this is the case by writing unit tests:
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.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class IssuesControllerTest {
private static EmbeddedServer server;
private static HttpClient client;
@BeforeClass (1)
public static void setupServer() {
server = ApplicationContext.run(EmbeddedServer.class);
client = server
.getApplicationContext()
.createBean(HttpClient.class, server.getURL());
}
@AfterClass (2)
public static void stopServer() {
if (server != null) {
server.stop();
}
if (client != null) {
client.stop();
}
}
@Test
public void testIssue() {
String body = client.toBlocking().retrieve("/issues/12"); (3)
assertNotNull(body);
assertEquals("Issue # 12!", body); (4)
}
@Test
public void testShowWithInvalidInteger() {
HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () ->
client.toBlocking().exchange("/issues/hello"));
assertEquals(400, e.getStatus().getCode()); (5)
}
@Test
public void testIssueWithoutNumber() {
HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () ->
client.toBlocking().exchange("/issues/"));
assertEquals(404, e.getStatus().getCode()); (6)
}
}
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 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 "/issues/{number} with an invalid Integer number responds 400"() {
when:
client.toBlocking().exchange("/issues/hello")
then:
def e = thrown(HttpClientResponseException)
e.status.code == 400 (5)
}
void "/issues/{number} without number responds 404"() {
when:
client.toBlocking().exchange("/issues/")
then:
def e = thrown(HttpClientResponseException)
e.status.code == 404 (6)
}
}
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 with invalid integer" {
val e = shouldThrow<HttpClientResponseException> {
client.toBlocking().exchange<Any>("/issues/hello")
}
e.status.code shouldBe 400 (5)
}
"test issue without number" {
val e = shouldThrow<HttpClientResponseException> {
client.toBlocking().exchange<Any>("/issues/")
}
e.status.code shouldBe 404 (6)
}
}
}
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 asserts a 400 response is returned when an invalid number is sent in the URL |
6 | 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
.
The following table provides examples of URI templates and what they match:
Template | Description | Matching URI |
---|---|---|
|
Simple match |
|
|
A variable of two characters max |
|
|
An optional URI variable |
|
|
An optional URI variable with regex |
|
|
Optional query parameters |
|
|
Regex path match with extension |
|
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
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:
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;
}
}
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"
}
}
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, Micronaut 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:
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)
}
}
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)
}
}
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 GET 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
Micronaut supports validating route arguments at compile time with the validation library. To get started, add the validation
dependency to your build:
annotationProcessor "io.micronaut:micronaut-validation" // Java only
kapt "io.micronaut:micronaut-validation" // Kotlin only
implementation "io.micronaut:micronaut-validation"
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 anjava.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 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.
@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.
6.4 Simple Request Binding
The examples in the previous section demonstrate how Micronaut 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:
Annotation | Description | Example |
---|---|---|
Binds from the body of the request |
|
|
Binds a parameter from a cookie |
|
|
Binds a parameter from an HTTP header |
|
|
Binds from a request query parameter |
|
|
Binds from a part of a multipart request |
|
|
Binds from an attribute of the request. Attributes are typically created in filters |
|
|
Binds from the path of the request |
|
|
Binds any Bindable value to single Bean object |
|
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
Micronaut 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.
@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)
}
@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)
}
@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:
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.core.annotation.Nullable;
import javax.validation.Valid;
@Controller("/api")
public class BookmarkController {
@Get("/bookmarks/list{?paginationCommand*}")
public HttpStatus list(@Valid @Nullable PaginationCommand paginationCommand) {
return HttpStatus.OK;
}
}
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import javax.annotation.Nullable
import javax.validation.Valid
@Controller("/api")
class BookmarkController {
@Get("/bookmarks/list{?paginationCommand*}")
HttpStatus list(@Valid @Nullable PaginationCommand paginationCommand) {
HttpStatus.OK
}
}
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import javax.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:
@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;
}
}
@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
}
}
@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:
@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;
}
}
@Introspected
class MovieTicketBean {
private HttpRequest<?> httpRequest
@PathVariable
String movieId
@Nullable
@QueryValue
@PositiveOrZero
Double minPrice
@Nullable
@QueryValue
@PositiveOrZero
Double maxPrice
}
@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:
-
Mutable Bean class with setters and getters
-
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 additional TypeConverter instances can be registered by creating @Singleton
beans of type TypeConverter
.
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 a 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 |
---|---|---|
Allows binding of basic authorization credentials |
|
Variable resolution
Micronaut tries to populate method arguments in the following order:
-
URI variables like
/{id}
. -
From query parameters if the request is a
GET
request (e.g.?foo=bar
). -
If there is a
@Body
and request allows the body, bind the body to it. -
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. -
Finally, if the method arguments cannot be populated return
400 BAD REQUEST
.
6.5 Custom Argument Binding
Micronaut 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 Micronaut tries to find an argument binder that supports the argument type.
An argument binder returns a BindingResult. The binding result gives Micronaut 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 UNSATISFIED result. Returning an 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 UNSATISFIED, it is considered 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:
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 "";
}
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 ""
}
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 |
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);
});
}
}
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)
})
}
}
}
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.
@Get("/annotated")
HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { (1)
return HttpResponse.ok("Session:" + sessionId);
}
// end::method
}
@Get("/annotated")
HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { (1)
HttpResponse.ok("Session:" + sessionId)
}
// end::method
}
@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:
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;
}
}
import io.micronaut.core.annotation.Introspected
@Introspected
class ShoppingCart {
String sessionId
Integer total
}
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:
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)
}
}
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)
}
}
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 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:
@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);
}
@Get("/typed")
HttpResponse<Map<String, Object>> loadCart(ShoppingCart shoppingCart) { (1)
HttpResponse.ok(
sessionId: shoppingCart.sessionId,
total: shoppingCart.total)
}
@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. Micronaut includes an implementation of the HttpHostResolver interface.
The default implementation looks for host information in the following places in order:
-
The supplied configuration
-
The
Forwarded
header -
X-Forwarded-
headers. If theX-Forwarded-Host
header is not present, the otherX-Forwarded
headers are ignored. -
The
Host
header -
The properties on the request URI
-
The properties on the embedded server URI
The behavior of which headers to pull the relevant data can be changed with the following configuration:
Property | Type | Description |
---|---|---|
|
The host resolution configuration |
|
|
java.lang.String |
The header name that stores the host |
|
java.lang.String |
The header name that stores the protocol |
|
java.lang.String |
The header name that stores the port |
|
boolean |
True if the host header supports a port |
|
java.util.List |
The list of allowed host regex patterns. Any resolved |
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
Micronaut 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:
Property | Type | Description |
---|---|---|
|
HttpServerConfiguration$HttpLocaleResolutionConfigurationProperties |
The locale resolution configuration |
|
java.util.Locale |
Set the language tag for the locale. Supports BCP 47 language tags (e.g. "en-US") and ISO standard (e.g "en_US"). |
|
java.lang.String |
Sets the key in the session to look for the locale. |
|
java.lang.String |
Sets the name of the cookie that is used to store the locale. |
|
boolean |
Set to true if the locale should be resolved from the |
|
java.util.Locale |
Sets the locale that will be used if the locale cannot be resolved through any means. Defaults to the system default. |
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. Micronaut includes an implementation of HttpClientAddressResolver.
The default implementation resolves the client address in the following places in order:
-
The configured header
-
The
Forwarded
header -
The
X-Forwarded-For
header -
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:
Interface | Description | Example |
---|---|---|
The full |
|
|
All HTTP headers present in the request |
|
|
All HTTP parameters (either from URI variables or request parameters) present in the request |
|
|
All Cookies present in the request |
|
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:
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
@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)
}
}
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
@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)
}
}
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
@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. |
HttpRequest is also available from static context via ServerRequestContext.
Generally ServerRequestContext is available within reactive flow, but the recommended approach is to propagate the necessary state through lambdas. |
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:
additionalTypes:
- 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 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):
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 declared at the type or method level to indicate which configured thread pool to run the method(s) of the controller on:
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);
}
}
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)
}
}
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:
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)
}
}
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)
}
}
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 Micronaut 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:
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 javax.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)
}
}
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 javax.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)
}
}
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 javax.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 javax.validation.constraints.Size that limits the size of the body to at most 1MB. 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 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:
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 javax.validation.constraints.Size;
@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()));
}
}
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 javax.validation.constraints.Size
@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()) });
}
}
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 javax.validation.constraints.Size
@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. Micronaut 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 Micronaut 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.
Micronaut supports returning common reactive types such as reactor: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’s Reactor dependency to your project to include the necessary converters.
|
To use RxJava's Flowable , Single or Maybe you need to add the Micronaut’s 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, Micronaut 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 Micronaut sets up and how to configure them. |
To summarize, the following table illustrates some common response types and their handling:
Type | Description | Example Signature |
---|---|---|
Any type that implements the Publisher interface |
|
|
A Java |
|
|
An HttpResponse and optional response body |
|
|
Any implementation of |
|
|
T |
Any simple POJO type |
|
When returning a Reactive type, its type affects the returned response. For example, when returning a reactor:Flux[], Micronaut 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 reactor:Mono[] the Content-Length header is populated.
|
6.14 JSON Binding with Jackson
The most common data interchange format nowadays is JSON.
In fact, the defaults in the Controller annotation specify that the controllers in Micronaut consume and produce JSON by default.
To do so in a non-blocking manner, Micronaut builds on the Jackson Asynchronous JSON parsing API and Netty, such that the reading of incoming JSON is done in a non-blocking manner.
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:
@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)
}
);
}
}
@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)
})
}
}
@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:
$ 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:
@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);
}
);
}
}
@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)
})
}
}
@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:
@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);
}
}
@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)
}
}
@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)
}
}
Micronaut only executes your method once the data has been read in a non-blocking manner.
The output produced by Jackson can be customized in a variety of ways, from defining Jackson modules to using Jackson annotations |
Jackson Configuration
The Jackson 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: |
timeZone |
String |
Uses TimeZone.getTimeZone. Example: |
serializationInclusion |
String |
One of JsonInclude.Include. Example: |
propertyNamingStrategy |
String |
Name of an instance of PropertyNamingStrategy. Example: |
defaultTyping |
String |
The global defaultTyping for polymorphic type handling from enum ObjectMapper.DefaultTyping. Example: |
Example:
jackson:
serializationInclusion: ALWAYS
Features
All features can be configured with their name as the key and a boolean to indicate enabled or disabled.
serialization |
Map |
|
deserialization |
Map |
|
mapper |
Map |
|
parser |
Map |
|
generator |
Map |
Example:
jackson:
serialization:
indentOutput: true
writeDatesAsTimestamps: false
deserialization:
useBigIntegerForInts: true
failOnUnknownProperties: false
Support for @JsonView
You can use the @JsonView
annotation on controller methods if you set jackson.json-view.enabled
to true
in 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
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.
6.15 Data Validation
It is easy to validate incoming data with Micronaut controllers using Validation Advice.
First, add the Hibernate Validator configuration to your application:
implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")
<dependency>
<groupId>io.micronaut.beanvalidation</groupId>
<artifactId>micronaut-hibernate-validator</artifactId>
</dependency>
We can validate parameters using javax.validation
annotations and the Validated annotation at the class level.
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.validation.Validated;
import javax.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"));
}
}
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.validation.Validated
import javax.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")
}
}
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.validation.Validated
import javax.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 javax.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:
@Test
public void testParametersAreValidated() {
HttpClientResponseException e = Assertions.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());
}
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
}
"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 javax.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 javax.validation.constraints.NotBlank
@Introspected
class Email {
@NotBlank (1)
String subject
@NotBlank (1)
String recipient
}
import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.NotBlank
@Introspected
open class Email {
@NotBlank (1)
var subject: String? = null
@NotBlank (1)
var recipient: String? = null
}
1 | You can use javax.validation annotations in your POJOs. |
Annotate your controller with Validated, and annotate the binding POJO with @Valid
.
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 javax.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"));
}
}
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 javax.validation.Valid
@Validated (1)
@Controller("/email")
class EmailController {
@Post("/send")
HttpResponse send(@Body @Valid Email email) { (2)
HttpResponse.ok(msg: "OK")
}
}
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 javax.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
public 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 Serving Static Resources
Static resource resolution is enabled by default. Micronaut supports resolving resources from the classpath or the file system.
See the information below for available configuration options:
Property | Type | Description |
---|---|---|
|
boolean |
Sets whether this specific mapping is enabled. Default value (true). |
|
java.util.List |
A list of paths either starting with |
|
java.lang.String |
The path resources should be served from. Uses ant path matching. Default value ("/**"). |
6.17 Error Handling
Sometimes with distributed applications, bad things happen. Having a good way to handle errors is important.
6.17.1 Status Handlers
The @Error annotation supports defining either an exception class or an HTTP status. Methods annotated with @Error must be defined with in 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. |
6.17.2 Local Error Handling
For example, the following method handles JSON parse exceptions from Jackson for the scope of the declaring controller:
@Error
public HttpResponse<JsonError> jsonError(HttpRequest request, JsonParseException 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)
}
@Error
HttpResponse<JsonError> jsonError(HttpRequest request, JsonParseException 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)
}
@Error
fun jsonError(request: HttpRequest<*>, e: JsonParseException): 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 JsonParseException is declared |
2 | An instance of JsonError is returned. |
3 | A custom response is returned to handle the error |
@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)
}
@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)
}
@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 |
6.17.3 Global Error Handling
@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)
}
@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)
}
@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 |
@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)
}
@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)
}
@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.17.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.17.4.1 Built-In Exception Handlers
Micronaut ships with several built-in handlers:
Exception |
Handler |
|
|
|
|
|
|
|
6.17.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 JsonErrorResponseFactory is used to create the body of the response |
6.17.5 Error Formatting
Micronaut 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.18 API Versioning
Since 1.1.x, Micronaut supports API versioning via a dedicated @Version annotation.
The following example demonstrates how to version 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";
}
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"
}
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 application.yml
:
micronaut:
router:
versioning:
enabled: true
By default Micronaut 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:
micronaut:
router:
versioning:
enabled: true (1)
parameter:
enabled: false (2)
names: 'v,api-version' (3)
header:
enabled: true (4)
names: (5)
- 'X-API-VERSION'
- 'Accept-Version'
1 | Enables versioning |
2 | Enables or disables parameter-based versioning |
3 | Specify the parameter names as a comma-separated list |
4 | Enables or disables header-based versioning |
5 | Specify 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.
micronaut:
router:
versioning:
enabled: true
default-version: 3 (1)
1 | 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:
Property | Type | Description |
---|---|---|
|
java.util.List |
The list of request header names. |
|
java.util.List |
The list of request query parameter names. |
For example to use Accept-Version
as the header name:
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:
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.19 Handling Form Data
To make data binding model customizations consistent between form data and JSON, Micronaut 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:
@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);
}
}
@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)
}
}
@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 application.yml
|
Alternatively, instead of using a POJO you can bind form data directly to method parameters (which works with JSON too!):
@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);
}
}
@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)
}
}
@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.20 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 |
---|---|
|
A Publisher that emits each chunk of content as a String |
|
A reactor:Flux[] that emits each chunk of content as a |
|
A Reactor |
|
When emitting a POJO, each emitted object is encoded as JSON by default without blocking |
|
A reactor:Flux[] that emits each chunk of content as a |
|
A Reactor |
|
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’s Reactor dependency to your project to include the necessary converters.
|
To use RxJava's Flowable , Single or Maybe you need to add the Micronaut’s 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:
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");
}
}
}
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")
}
}
}
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")
}
}
}
InputStream
Another option is to return an input stream. This is useful for many scenarios that interact with other APIs that expose a stream.
@Get(value = "/write", produces = MediaType.TEXT_PLAIN)
InputStream write() {
byte[] bytes = "test".getBytes(StandardCharsets.UTF_8);
return new ByteArrayInputStream(bytes); (1)
}
@Get(value = "/write", produces = MediaType.TEXT_PLAIN)
InputStream write() {
byte[] bytes = "test".getBytes(StandardCharsets.UTF_8);
new ByteArrayInputStream(bytes) (1)
}
@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.21 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<PartData> 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<PartData> that also has file information such as the content type and file name.
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 io.micronaut.core.async.annotation.SingleResult;
import java.io.File;
import java.io.IOException;
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");
}
});
}
}
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")
}
})
}
}
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 reactor.core.publisher.Mono
import java.io.File
@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(java.lang.String) method transfers the file to the server. The method returns a Publisher |
4 | The returned reactor: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.
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");
}
}
}
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")
}
}
}
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.
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");
}
}
}
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")
}
}
}
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:
-
CompletedPart for attributes
-
Any POJO, assuming a media codec that supports the content type exists
-
Another Publisher that accepts one of the chunked data types described above
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:
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) {
String originalFileName = ((CompletedFileUpload) completedPart).getFilename();
}
}
@Override
public void onError(Throwable t) {
emitter.error(t);
}
@Override
public void onComplete() {
emitter.success("Uploaded");
}
});
});
}
}
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")
}
})
})
}
}
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.22 File Transfers
Micronaut 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:
@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), Micronaut 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.
|
@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, Micronaut 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.
Property | Type | Description |
---|---|---|
|
int |
Cache Seconds. Default value (60). |
Property | Type | Description |
---|---|---|
|
Sets the cache control configuration. |
|
|
boolean |
Sets whether the cache control is public. Default value (false) |
6.23 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.
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. |
Writing a Filter
Suppose you wish to trace each request to the Micronaut "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:
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);
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();
}
}
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)
}
}
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 reactor: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 |
You can then inject this implementation into your filter definition:
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;
}
}
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
}
}
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 {
}
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 |
The final step is to write the doFilter
implementation of the HttpServerFilter interface.
@Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
ServerFilterChain chain) {
return Flux.from(traceService
.trace(request)) (1)
.switchMap(aBoolean -> chain.proceed(request)) (2)
.doOnNext(res ->
res.getHeaders().add("X-Trace-Enabled", "true") (3)
);
}
@Override
Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request,
ServerFilterChain chain) {
traceService
.trace(request) (1)
.switchMap({ aBoolean -> chain.proceed(request) }) (2)
.doOnNext({ res ->
res.headers.add("X-Trace-Enabled", "true") (3)
})
}
override fun doFilter(request: HttpRequest<*>,
chain: ServerFilterChain): Publisher<MutableHttpResponse<*>> {
return traceService.trace(request) (1)
.switchMap { aBoolean -> chain.proceed(request) } (2)
.doOnNext { res ->
res.headers.add("X-Trace-Enabled", "true") (3)
}
}
1 | TraceService is invoked to trace the request |
2 | If the call succeeds, the filter resumes request processing using Project Reactor's switchMap method, which invokes the proceed method of the ServerFilterChain |
3 | 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 |
The Filter annotation uses AntPathMatcher for path matching. The mapping matches URLs using the following rules:
-
? matches one character
-
* matches zero or more characters
-
** matches zero or more subdirectories in a path
Pattern | Example Matched Paths |
---|---|
|
any path |
|
customer/joy, customer/jay |
|
customer/adam/id, com/amy/id |
|
customer/adam, customer/adam/id, customer/adam/name |
|
customer/index.html, customer/adam/profile.html, customer/adam/job/description.html == Error States The publisher returned from |
6.24 HTTP Sessions
By default Micronaut is a stateless HTTP server, however depending on your application requirements you may need the notion of HTTP sessions.
Micronaut includes a session
module inspired by Spring Session that enables this which currently has two implementations:
-
In-Memory sessions - which you should combine with an a sticky session proxy if you plan to run multiple instances.
-
Redis sessions - In this case Redis stores sessions, and non-blocking I/O is used to read/write sessions to Redis.
Enabling Sessions
To enable support for in-memory sessions you just need the session
dependency:
implementation("io.micronaut:micronaut-session")
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-session</artifactId>
</dependency>
Redis Sessions
To store Session instances in Redis, use the Micronaut Redis module which includes detailed instructions.
To quickly get up and running with Redis sessions you must also have the redis-lettuce
dependency in your build:
compile "io.micronaut:micronaut-session"
compile "io.micronaut.redis:micronaut-redis-lettuce"
And enable Redis sessions via configuration in application.yml
:
redis:
uri: redis://localhost:6379
micronaut:
session:
http:
redis:
enabled: true
Configuring Session Resolution
Session resolution can be configured with HttpSessionConfiguration.
By default, sessions are resolved using an HttpSessionFilter that looks for session identifiers via either an HTTP header (using the Authorization-Info
or X-Auth-Token
headers) or via a Cookie named SESSION
.
You can disable either header resolution or cookie resolution via configuration in application.yml
:
micronaut:
session:
http:
cookie: false
header: true
The above configuration enables header resolution, but disables cookie resolution. You can also configure header and cookie names.
Working with Sessions
A Session can be retrieved with a parameter of type Session in a controller method. For example consider the following controller:
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.session.Session;
import io.micronaut.session.annotation.SessionValue;
import io.micronaut.core.annotation.Nullable;
import javax.validation.constraints.NotBlank;
@Controller("/shopping")
public class ShoppingController {
private static final String ATTR_CART = "cart"; (1)
@Post("/cart/{name}")
Cart addItem(Session session, @NotBlank String name) { (2)
Cart cart = session.get(ATTR_CART, Cart.class).orElseGet(() -> { (3)
Cart newCart = new Cart();
session.put(ATTR_CART, newCart); (4)
return newCart;
});
cart.getItems().add(name);
return cart;
}
}
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.session.Session
import io.micronaut.session.annotation.SessionValue
import javax.annotation.Nullable
import javax.validation.constraints.NotBlank
@Controller("/shopping")
class ShoppingController {
private static final String ATTR_CART = "cart" (1)
@Post("/cart/{name}")
Cart addItem(Session session, @NotBlank String name) { (2)
Cart cart = session.get(ATTR_CART, Cart).orElseGet({ -> (3)
Cart newCart = new Cart()
session.put(ATTR_CART, newCart) (4)
newCart
})
cart.items << name
cart
}
}
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.session.Session
import io.micronaut.session.annotation.SessionValue
@Controller("/shopping")
class ShoppingController {
companion object {
private const val ATTR_CART = "cart" (1)
}
@Post("/cart/{name}")
internal fun addItem(session: Session, name: String): Cart { (2)
require(name.isNotBlank()) { "Name cannot be blank" }
val cart = session.get(ATTR_CART, Cart::class.java).orElseGet { (3)
val newCart = Cart()
session.put(ATTR_CART, newCart) (4)
newCart
}
cart.items.add(name)
return cart
}
}
1 | ShoppingController declares a Session attribute named cart |
2 | The Session is declared as a method parameter |
3 | The cart attribute is retrieved |
4 | Otherwise a new Cart instance is created and stored in the session |
Note that because the Session is declared as a required parameter, to execute the controller action a Session will be created and saved to the SessionStore.
If you don’t want to create unnecessary sessions, declare the Session as @Nullable
in which case a session will not be created and saved unnecessarily. For example:
@Post("/cart/clear")
void clearCart(@Nullable Session session) {
if (session != null) {
session.remove(ATTR_CART);
}
}
@Post("/cart/clear")
void clearCart(@Nullable Session session) {
session?.remove(ATTR_CART)
}
@Post("/cart/clear")
internal fun clearCart(session: Session?) {
session?.remove(ATTR_CART)
}
The above method only injects a new Session if one already exists.
Session Clients
If the client is a web browser, sessions should work if cookies are enabled. However, for programmatic HTTP clients you need to propagate the session ID between HTTP calls.
For example, when invoking the viewCart
method of the StoreController
in the previous example, the HTTP client receives by default a AUTHORIZATION_INFO
header. The following example, using a Spock test, demonstrates this:
HttpResponse<Cart> response = Flux.from(client.exchange(HttpRequest.GET("/shopping/cart"), Cart.class)) (1)
.blockFirst();
Cart cart = response.body();
assertNotNull(response.header(HttpHeaders.AUTHORIZATION_INFO)); (2)
assertNotNull(cart);
assertTrue(cart.getItems().isEmpty());
when: "The shopping cart is retrieved"
HttpResponse<Cart> response = client.exchange(HttpRequest.GET('/shopping/cart'), Cart) (1)
.blockFirst()
Cart cart = response.body()
then: "The shopping cart is present as well as a session id header"
response.header(HttpHeaders.AUTHORIZATION_INFO) != null (2)
cart != null
cart.items.isEmpty()
var response = Flux.from(client.exchange(HttpRequest.GET<Cart>("/shopping/cart"), Cart::class.java)) (1)
.blockFirst()
var cart = response.body()
assertNotNull(response.header(HttpHeaders.AUTHORIZATION_INFO)) (2)
assertNotNull(cart)
cart.items.isEmpty()
1 | A request is made to /shopping/cart |
2 | The AUTHORIZATION_INFO header is present in the response |
You can then pass this AUTHORIZATION_INFO
in subsequent requests to reuse the existing Session:
String sessionId = response.header(HttpHeaders.AUTHORIZATION_INFO); (1)
response = Flux.from(client.exchange(HttpRequest.POST("/shopping/cart/Apple", "")
.header(HttpHeaders.AUTHORIZATION_INFO, sessionId), Cart.class)) (2)
.blockFirst();
cart = response.body();
String sessionId = response.header(HttpHeaders.AUTHORIZATION_INFO) (1)
response = client.exchange(HttpRequest.POST('/shopping/cart/Apple', "")
.header(HttpHeaders.AUTHORIZATION_INFO, sessionId), Cart) (2)
.blockFirst()
cart = response.body()
val sessionId = response.header(HttpHeaders.AUTHORIZATION_INFO) (1)
response = Flux.from(client.exchange(HttpRequest.POST("/shopping/cart/Apple", "")
.header(HttpHeaders.AUTHORIZATION_INFO, sessionId), Cart::class.java)) (2)
.blockFirst()
cart = response.body()
1 | The AUTHORIZATION_INFO is retrieved from the response |
2 | And then sent as a header in the subsequent request |
Using @SessionValue
Rather than explicitly injecting the Session into a controller method, you can instead use @SessionValue. For example:
@Get("/cart")
@SessionValue(ATTR_CART) (1)
Cart viewCart(@SessionValue @Nullable Cart cart) { (2)
if (cart == null) {
cart = new Cart();
}
return cart;
}
@Get("/cart")
@SessionValue("cart") (1)
Cart viewCart(@SessionValue @Nullable Cart cart) { (2)
cart ?: new Cart()
}
@Get("/cart")
@SessionValue(ATTR_CART) (1)
internal fun viewCart(@SessionValue cart: Cart?): Cart { (2)
return cart ?: Cart()
}
1 | @SessionValue is declared on the method resulting in the return value being stored in the Session. Note that you must specify the attribute name when used on a return value |
2 | @SessionValue is used on a @Nullable parameter which results in looking up the value from the Session in a non-blocking way and supplying it if present. In the case a value is not specified to @SessionValue resulting in the parameter name being used to lookup the attribute. |
Session Events
You can register ApplicationEventListener beans to listen for Session related events located in the io.micronaut.session.event package.
The following table summarizes the events:
Type | Description |
---|---|
Fired when a Session is created |
|
Fired when a Session is deleted |
|
Fired when a Session expires |
|
Parent of both |
6.25 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 a event stream of news headlines; you may define a data class as follows:
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;
}
}
class Headline {
String title
String description
Headline() {}
Headline(String title, String description) {
this.title = title;
this.description = description;
}
}
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 reactor:Flux[] via the generate
method:
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;
});
}
}
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
})
}
}
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 reactor: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:
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.26 WebSocket Support
Micronaut features dedicated support for creating WebSocket clients and servers. The io.micronaut.websocket.annotation package includes annotations for defining both clients and servers.
6.26.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:
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));
}
}
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)) }
}
}
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 in the Micronaut Examples GitHub repository. |
For binding, method arguments to each WebSocket method can be:
-
A variable from the URI template (in the above example
topic
andusername
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 fromByteBuf
(you can register additional TypeConverter beans to support a custom type). -
A
byte[]
, aByteBuf
or a Java NIOByteBuffer
. -
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.
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:
@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));
}
@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))
}
@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. Micronaut 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, a unique @ServerWebSocket
instance is created for each WebSocket connection. This lets you retrieve the WebSocketSession from the @OnOpen
handler and assign it to a field of the @ServerWebSocket
instance.
If you define the @ServerWebSocket
as @Singleton
, extra care must be taken to synchronize local state to avoid thread safety issues.
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 the Micronaut CLI and the default ( $ mn create-websocket-server MyChat | Rendered template WebsocketServer.java to destination src/main/java/example/MyChatServer.java |
Connection Timeouts
By default, Micronaut 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):
micronaut:
server:
idle-timeout: 30m # 30 minutes
If you use Micronaut’s WebSocket client you may also wish to set the timeout on the client:
micronaut:
http:
client:
read-idle-timeout: 30m # 30 minutes
6.26.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")
RxWebSocketClient 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:
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)
}
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)
}
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:
public abstract void send(String message);
Note by returning void
this tells Micronaut that the method is a blocking send. You can instead define methods that return either futures or a Publisher:
public abstract reactor.core.publisher.Mono<String> send(String message);
The above example defines a send method that returns a reactor:Mono[].
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:
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 reactor:Flux[]) to perform non-blocking interaction via WebSocket.
|
Using the CLI
If you created your project using the Micronaut CLI and the default ( $ mn create-websocket-client MyChat | Rendered template WebsocketClient.java to destination src/main/java/example/MyChatClient.java |
6.27 HTTP/2 Support
Since Micronaut 2.x, Micronaut’s 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:
micronaut:
server:
http-version: 2.0
With this configuration, Micronaut 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:
h2
Protocol Supportmicronaut:
ssl:
enabled: true
buildSelfSigned: true
server:
http-version: 2.0
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.40.Final")
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative</artifactId>
<version>2.0.40.Final</version>
<scope>runtime</scope>
</dependency>
runtimeOnly("io.netty:netty-tcnative-boringssl-static:2.0.40.Final")
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<version>2.0.40.Final</version>
<scope>runtime</scope>
</dependency>
In addition to a dependency on the appropriate native library for your architecture. For example:
runtimeOnly "io.netty:netty-tcnative-boringssl-static:2.0.40.Final:${Os.isFamily(Os.FAMILY_MAC) ? 'osx-x86_64' : 'linux-x86_64'}"
See the documentation on Tomcat Native for more information.
HTTP/2 Clients
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:
micronaut:
http:
client:
http-version: 2.0
Or by specifying the HTTP version to use when injecting the client:
@Inject
@Client(httpVersion=HttpVersion.HTTP_2_0)
ReactorHttpClient client;
6.28 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:
Event | Description |
---|---|
Emitted when the server completes startup |
|
Emitted when the server shuts down |
|
Emitted after all ServerStartupEvent listeners have been invoked and exposes the EmbeddedServerInstance |
|
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:
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
:
@EventListener
with ServerStartupEvent
import io.micronaut.runtime.event.annotation.EventListener;
import io.micronaut.runtime.server.event.ServerStartupEvent;
import javax.inject.Singleton;
...
@Singleton
public class MyBean {
@EventListener
public void onStartup(ServerStartupEvent event) {
// logic here
...
}
}
6.29 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 application.yml
:
micronaut:
server:
maxRequestSize: 1MB
host: localhost (1)
netty:
maxHeaderSize: 500KB (2)
worker:
threads: 8 (3)
childOptions:
autoRead: true (4)
1 | By default Micronaut binds to all network interfaces. Use localhost to bind only to loopback network interface |
2 | Maximum size for headers |
3 | Number of Netty worker threads |
4 | Auto read request body |
Property | Type | Description |
---|---|---|
|
java.util.Map |
Sets the Netty child worker options. |
|
java.util.Map |
Sets the channel options. |
|
int |
Sets the maximum initial line length for the HTTP request. Default value (4096). |
|
int |
Sets the maximum size of any one header. Default value (8192). |
|
int |
Sets the maximum size of any single request chunk. Default value (8192). |
|
boolean |
Sets whether chunked transfer encoding is supported. Default value (true). |
|
boolean |
Sets whether to validate incoming headers. Default value (true). |
|
int |
Sets the initial buffer size. Default value (128). |
|
io.netty.handler.logging.LogLevel |
Sets the Netty log level. |
|
int |
Sets the minimum size of a request body must be in order to be compressed. Default value (1024). |
|
int |
Sets the compression level (0-9). Default value (6). |
|
boolean |
Sets whether to use netty’s native transport (epoll or kqueue) if available . Default value (false). |
|
java.lang.String |
Sets the fallback protocol to use when negotiating via ALPN. |
|
boolean |
Whether to send connection keep alive on internal server errors. Default value ({@value DEFAULT_KEEP_ALIVE_ON_SERVER_ERROR}). |
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:
runtime("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 Linux on x86:
runtime("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:
runtime("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:
micronaut:
netty:
event-loops:
default:
prefer-native-transport: true
Netty enables simplistic sampling resource leak detection which reports there is a leak or not, at the cost of small overhead. You can disable it or enable more advanced detection by setting property netty.resource-leak-detector-level to one of: SIMPLE (default), DISABLED , PARANOID or ADVANCED .
|
6.29.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 ).
|
Property | Type | Description |
---|---|---|
|
Sets the worker event loop configuration. |
|
|
java.lang.String |
Sets the name to use. |
|
int |
Sets the number of threads for the event loop group. |
|
java.lang.Integer |
Sets the I/O ratio. |
|
java.lang.String |
Sets the name of the executor. |
|
boolean |
Set whether to prefer the native transport if available |
|
java.time.Duration |
Set the shutdown quiet period |
|
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:
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:
Property | Type | Description |
---|---|---|
|
int |
|
|
java.lang.Integer |
|
|
boolean |
|
|
java.lang.String |
|
|
java.time.Duration |
|
|
java.time.Duration |
Blocking Operations
When dealing with blocking operations, Micronaut 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:
micronaut:
executors:
io:
type: fixed
nThreads: 75
The above configuration creates a fixed thread pool with 75 threads.
6.29.2 Configuring the Netty Pipeline
You can customize the Netty pipeline by writing a Bean Event Listener that listens for the creation of ChannelPipelineCustomizer.
Both the Netty HTTP server and client implement this interface and it lets you customize the Netty ChannelPipeline
and add additional handlers.
The ChannelPipelineCustomizer interface defines constants for the names of the various handlers Micronaut registers.
As an example the following code sample demonstrates registering the Logbook library which includes additional Netty handlers to perform request and response logging:
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 org.zalando.logbook.Logbook;
import org.zalando.logbook.netty.LogbookClientHandler;
import org.zalando.logbook.netty.LogbookServerHandler;
import jakarta.inject.Singleton;
@Requires(beans = Logbook.class)
@Singleton
public class LogbookPipelineCustomizer
implements BeanCreatedEventListener<ChannelPipelineCustomizer> { (1)
private final Logbook logbook;
public LogbookPipelineCustomizer(Logbook logbook) {
this.logbook = logbook;
}
@Override
public ChannelPipelineCustomizer onCreated(BeanCreatedEvent<ChannelPipelineCustomizer> event) {
ChannelPipelineCustomizer customizer = event.getBean();
if (customizer.isServerChannel()) { (2)
customizer.doOnConnect(pipeline -> {
pipeline.addAfter(
ChannelPipelineCustomizer.HANDLER_HTTP_SERVER_CODEC,
"logbook",
new LogbookServerHandler(logbook)
);
return pipeline;
});
} else { (3)
customizer.doOnConnect(pipeline -> {
pipeline.addAfter(
ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC,
"logbook",
new LogbookClientHandler(logbook)
);
return pipeline;
});
}
return customizer;
}
}
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.netty.channel.ChannelPipeline
import org.zalando.logbook.Logbook
import org.zalando.logbook.netty.LogbookClientHandler
import org.zalando.logbook.netty.LogbookServerHandler
import jakarta.inject.Singleton
@Requires(beans = Logbook.class)
@Singleton
class LogbookPipelineCustomizer
implements BeanCreatedEventListener<ChannelPipelineCustomizer> { (1)
private final Logbook logbook
LogbookPipelineCustomizer(Logbook logbook) {
this.logbook = logbook
}
@Override
ChannelPipelineCustomizer onCreated(BeanCreatedEvent<ChannelPipelineCustomizer> event) {
ChannelPipelineCustomizer customizer = event.bean
if (customizer.serverChannel) { (2)
customizer.doOnConnect( { ChannelPipeline pipeline ->
pipeline.addAfter(
ChannelPipelineCustomizer.HANDLER_HTTP_SERVER_CODEC,
"logbook",
new LogbookServerHandler(logbook)
)
return pipeline
})
} else { (3)
customizer.doOnConnect({ ChannelPipeline pipeline ->
pipeline.addAfter(
ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC,
"logbook",
new LogbookClientHandler(logbook)
)
return pipeline
})
}
return customizer
}
}
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.netty.channel.ChannelPipeline
import org.zalando.logbook.Logbook
import org.zalando.logbook.netty.LogbookClientHandler
import org.zalando.logbook.netty.LogbookServerHandler
import jakarta.inject.Singleton
@Requires(beans = [Logbook::class])
@Singleton
class LogbookPipelineCustomizer(private val logbook: Logbook) :
BeanCreatedEventListener<ChannelPipelineCustomizer> { (1)
override fun onCreated(event: BeanCreatedEvent<ChannelPipelineCustomizer>): ChannelPipelineCustomizer {
val customizer = event.bean
if (customizer.isServerChannel) { (2)
customizer.doOnConnect { pipeline: ChannelPipeline ->
pipeline.addAfter(
ChannelPipelineCustomizer.HANDLER_HTTP_SERVER_CODEC,
"logbook",
LogbookServerHandler(logbook)
)
pipeline
}
} else { (3)
customizer.doOnConnect { pipeline: ChannelPipeline ->
pipeline.addAfter(
ChannelPipelineCustomizer.HANDLER_HTTP_CLIENT_CODEC,
"logbook",
LogbookClientHandler(logbook)
)
pipeline
}
}
return customizer
}
}
1 | LogbookPipelineCustomizer implements ChannelPipelineCustomizer and requires the definition of a Logbook bean |
2 | If the bean being created is the server, the server handler is registered |
3 | if the bean being created is the client, the client handler is registered |
6.29.3 Configuring CORS
Micronaut supports CORS (Cross Origin Resource Sharing) out of the box. By default, CORS requests are rejected. To enable processing of CORS requests, modify your configuration. For example with application.yml
:
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.
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.
Allowed Origins
To allow any origin for a given configuration, don’t include the allowedOrigins
key in your configuration.
For multiple valid origins, set the allowedOrigins
key of the configuration to a list of strings. Each value can either be a static value (http://www.foo.com
) or a regular expression (^http(|s)://www\.google\.com$
).
Regular expressions are passed to Pattern#compile and compared to the request origin with Matcher#matches.
micronaut:
server:
cors:
enabled: true
configurations:
web:
allowedOrigins:
- http://foo.com
- ^http(|s):\/\/www\.google\.com$
Allowed Methods
To allow any request method for a given configuration, don’t include the allowedMethods
key in your configuration.
For multiple allowed methods, set the allowedMethods
key of the configuration to a list of strings.
micronaut:
server:
cors:
enabled: true
configurations:
web:
allowedMethods:
- POST
- PUT
Allowed Headers
To allow any request header for a given configuration, don’t include the allowedHeaders
key in your configuration.
For multiple allowed headers, set the allowedHeaders
key of the configuration to a list of strings.
micronaut:
server:
cors:
enabled: true
configurations:
web:
allowedHeaders:
- Content-Type
- Authorization
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 exposedHeaders
key in your configuration. None are exposed by default.
micronaut:
server:
cors:
enabled: true
configurations:
web:
exposedHeaders:
- Content-Type
- Authorization
Allow Credentials
Credentials are allowed by default for CORS requests. To disallow credentials, set the allowCredentials
option to false
.
micronaut:
server:
cors:
enabled: true
configurations:
web:
allowCredentials: false
Max Age
The default maximum age that preflight requests can be cached is 30 minutes. To change that behavior, specify a value in seconds.
micronaut:
server:
cors:
enabled: true
configurations:
web:
maxAge: 3600 # 1 hour
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
6.29.4 Securing the Server with HTTPS
Micronaut 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 with application.yml
:
micronaut:
ssl:
enabled: true
buildSelfSigned: true (1)
1 | Micronaut will create a self-signed certificate. |
By default Micronaut with HTTPS support starts on port 8443 but you can change the port with the property micronaut.ssl.port .
|
This configuration will generate a warning in the browser. |

Using a valid x509 certificate
It is also possible to configure Micronaut 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:
micronaut:
ssl:
enabled: true
keyStore:
path: classpath:server.p12 (1)
password: mypassword (2)
type: PKCS12
1 | The p12 file. It can also be referenced as file:/path/to/the/file |
2 | The password defined during the export |
With this configuration, if we start Micronaut 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.

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
:

Using Java Keystore (JKS)
Using this type of certificate is not recommended because the format is proprietary - PKCS12 format is preferred. Regardless, Micronaut 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:
micronaut:
ssl:
enabled: true
keyStore:
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.
6.29.5 Enabling HTTP and HTTPS
Micronaut supports binding both HTTP and HTTPS. To enable dual protocol support, modify your configuration. For example with application.yml
:
micronaut:
ssl:
enabled: true
build-self-signed: true (1)
server:
dual-protocol : true (2)
1 | You must configure SSL for HTTPS to work. In this example we are just using a self-signed certificate, but see Securing the Server with HTTPS for other configurations |
2 | Enabling both HTTP and HTTPS is an opt-in feature - setting the dualProtocol flag enables it. By default Micronaut 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, with application.yml
:
micronaut:
ssl:
enabled: true
build-self-signed: true
server:
dual-protocol : true
http-to-https-redirect: true (1)
1 | Enable HTTP to HTTPS redirects |
6.29.6 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 application.yml
set:
micronaut:
server:
netty:
access-logger:
enabled: true # Enables the access logger
logger-name: my-access-logger # A logger name, optional, default is `HTTP_ACCESS_LOGGER`
log-format: common # A log format, optional, default is 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:
<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:
-
common -
%h %l %u %t "%r" %s %b
for Common Log Format (CLF) -
combined -
%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"
for Combined Log Format
6.30 Server Side View Rendering
Micronaut supports Server Side View Rendering.
See the documentation for Micronaut Views for more information.
6.31 OpenAPI / Swagger Support
To configure Micronaut integration with OpenAPI/Swagger, please follow these instructions
6.32 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
Using the CLI
If you create your project using the Micronaut CLI, the |
Client communication between Microservices is a critical component of any Microservice architecture. With that in mind, Micronaut 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. |
To use the HTTP client, add the http-client
dependency to your build:
implementation("io.micronaut:micronaut-http-client")
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-client</artifactId>
</dependency>
Since the higher level API is built on the low-level HTTP client, we first introduce the low-level client.
7.1 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 reactor:Flux[] types.
7.1.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:
@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
:
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);
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
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:
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)
}
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)
}
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 reactor: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 reactor: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:
<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 application.yml
:
micronaut:
http:
client:
logger-name: mylogger
services:
otherClient:
logger-name: other.client
Then enable logging in logback.yml
:
<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:
Flux<String> response = Flux.from(client.retrieve(
GET("/hello/John")
.header("X-My-Header", "SomeValue")
));
Flux<String> response = Flux.from(client.retrieve(
GET("/hello/John")
.header("X-My-Header", "SomeValue")
))
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:
@Get("/greet/{name}")
Message greet(String name) {
return new Message("Hello " + name);
}
@Get("/greet/{name}")
Message greet(String name) {
new Message("Hello $name")
}
@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:
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;
}
}
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
}
}
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:
Flux<Map> response = Flux.from(client.retrieve(
GET("/greet/John"), Map.class
));
Flux<Map> response = Flux.from(client.retrieve(
GET("/greet/John"), Map
))
var response: Flux<Map<*, *>> = Flux.from(client.retrieve(
GET<Any>("/greet/John"), Map::class.java
))
The above examples decodes the response into a Map representing the JSON. You can use the Argument.of(..)
method to customize the type of the key and string:
response = Flux.from(client.retrieve(
GET("/greet/John"),
Argument.of(Map.class, String.class, String.class) (1)
));
response = Flux.from(client.retrieve(
GET("/greet/John"),
Argument.of(Map, String, String) (1)
))
response = Flux.from(client.retrieve(
GET<Any>("/greet/John"),
Argument.of(Map::class.java, 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:
Flux<Message> response = Flux.from(client.retrieve(
GET("/greet/John"), Message.class
));
assertEquals("Hello John", response.blockFirst().getText());
when:
Flux<Message> response = Flux.from(client.retrieve(
GET("/greet/John"), Message
))
then:
"Hello John" == response.blockFirst().getText()
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:
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());
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()
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.1.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:
Method | Description | Allows Body |
---|---|---|
Constructs an HTTP |
|
|
Constructs an HTTP |
|
|
Constructs an HTTP |
|
|
Constructs an HTTP |
|
|
Constructs an HTTP |
|
|
Constructs an HTTP |
|
|
Constructs an HTTP |
|
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:
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)
));
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)
))
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:
Flux<HttpResponse<Message>> call = Flux.from(client.exchange(
POST("/greet", new Message("Hello John")), (1)
Message.class (2)
));
Flux<HttpResponse<Message>> call = Flux.from(client.exchange(
POST("/greet", new Message("Hello John")), (1)
Message (2)
))
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:
{"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:
Flux<HttpResponse<Book>> call = Flux.from(client.exchange(
POST("/amazon/book/{title}", new Book("The Stand")),
Book.class
));
Flux<HttpResponse<Book>> call = client.exchange(
POST("/amazon/book/{title}", new Book("The Stand")),
Book
);
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:
Flux<HttpResponse<Book>> call = Flux.from(client.exchange(
POST("/amazon/book/{title}", new Book("The Stand"))
.contentType(MediaType.APPLICATION_FORM_URLENCODED),
Book.class
));
Flux<HttpResponse<Book>> call = client.exchange(
POST("/amazon/book/{title}", new Book("The Stand"))
.contentType(MediaType.APPLICATION_FORM_URLENCODED),
Book
)
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.1.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:
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)
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)
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. |
HttpRequest.POST("/multipart/upload", requestBody) (1)
.contentType(MediaType.MULTIPART_FORM_DATA_TYPE) (2)