On this page
Core
The Micronaut Framework is a modern, JVM-based, full stack Java framework designed for building modular, easily testable JVM applications with support for Java, Kotlin, and Groovy.
The Micronaut framework was originally created by a team who had also worked on the Grails framework. The Micronaut framework takes inspiration from lessons learned over the years building real-world applications from monoliths to microservices using Spring, Spring Boot and the Grails framework. The core team continues to develop and maintain the Micronaut project through the support of the Micronaut Foundation.
The Micronaut framework aims to provide all the tools necessary to build JVM applications including:
-
Dependency Injection and Inversion of Control (IoC)
-
Aspect Oriented Programming (AOP)
-
Sensible Defaults and Auto-Configuration
With the Micronaut framework you can build Message-Driven Applications, Command Line Applications, HTTP Servers and more whilst for Microservices in particular Micronaut also provides:
-
Distributed Configuration
-
Service Discovery
-
HTTP Routing
-
Client-Side Load Balancing
At the same time, the Micronaut framework aims to avoid the downsides of frameworks like Spring, Spring Boot and Grails by providing:
-
Fast startup time
-
Reduced memory footprint
-
Minimal use of reflection
-
Minimal use of proxies
-
No runtime bytecode generation
-
Easy Unit Testing
Historically, frameworks such as Spring and Grails were not designed to run in scenarios such as serverless functions, Android apps, or low memory footprint microservices. In contrast, the Micronaut framework is designed to be suitable for all of these scenarios.
This goal is achieved through the use of Java’s annotation processors, which are usable on any JVM language that supports them, as well as an HTTP Server (with several runtimes Netty, Jetty, Tomcat, Undertow…) and an HTTP Client (with several runtimes Netty, Java HTTP Client, …). To provide a similar programming model to Spring and Grails, these annotation processors precompile the necessary metadata to perform DI, define AOP proxies and configure your application to run in a low-memory environment.
Many APIs in the Micronaut framework are heavily inspired by Spring and Grails. This is by design, and helps bring developers up to speed quickly.
Micronaut Framework 5.0.x continues the work started in the first 5.0 milestones and adds a number of improvements across the core container, HTTP stack, configuration system, and developer-facing APIs.
This page is a curated overview of the most important additions on the 5.0.x line. For upgrade guidance, see Upgrading your Micronaut Application, and for incompatible changes, review Breaking Changes.
Core Themes in Micronaut Framework 5.0.x
Micronaut Framework 5 modernizes the core platform with newer JVM and language support, including a new JDK 25 baseline, an Apache Groovy 5 baseline, a Kotlin 2.3 baseline, broader support for current JDK capabilities, and deeper investment in compile-time metadata. See also Breaking Changes for version-specific migration notes.
The framework APIs now embrace nullability annotations and specifically JSpecify nullability annotations, with @NullMarked adoption across the codebase and stronger static analysis integration. The result is clearer API contracts, improved Kotlin interoperability, and better IDE feedback.
The IoC container and compile-time infrastructure also received substantial work in the 5.0 branch. Bean resolution, qualifier handling, replacement metadata, eager initialization, and runtime annotation processing were refined to reduce runtime work and improve predictability.
Container, AOP, and Runtime Improvements
The bean context was reworked in several areas during the 5.0 development cycle, including precomputed bean indexes, compile-time @Replaces handling, and broader bean context optimizations. Together, these changes continue Micronaut’s focus on startup performance and low runtime overhead.
Micronaut 5 also added support for creating AOP proxies at runtime when build-time proxy generation is not the right fit. This enables integrations such as Byte Buddy-based proxies, JDK dynamic proxies for interfaces, and test-oriented proxy scenarios such as mocks and spies. For background on Micronaut’s AOP model, see Aspect Oriented Programming.
Recent 5.0.x work also improved interoperability with Jakarta APIs by adding support for jakarta.annotation.Priority, mapping it to Micronaut ordering semantics for beans and HTTP filters. Related ordering documentation can be found in Injectable Container Types and Filter Order.
HTTP, Configuration, and Metadata
On the HTTP side, HTTP/3 support on the Netty stack was promoted to stable. See HTTP/3 Support and HTTP/3 in Clients. The 5.0 line also includes a multipart/form handling refactor that introduces a lower-level, more server-independent form API and improves resource management in higher-level binders. See Forms, Detailed Form API, and File Uploads.
Configuration support expanded significantly in 5.0.x. Micronaut now supports config imports and a PropertySourceImporter SPI, enabling configuration loading from sources such as files, classpath locations, environment variables, config trees, and custom importer implementations. See Importing Additional Configuration, Implementing a Custom PropertySourceImporter, and Externalized Configuration with PropertySources.
Configuration metadata also became more useful for tooling. Micronaut 5 can generate JSON Schema documents from @ConfigurationProperties, making it easier to drive IDE completion, validation, and external tooling from the same configuration model used by the framework. For the underlying configuration model, see Configuration Properties.
The JSON and serialization stack was updated as well, with work across JsonMapper, the update to Jackson 3, and additional configuration coverage so applications can adopt the newer JSON infrastructure more smoothly.
Resilience and Context Propagation
Micronaut Retry gained programmatic retry and circuit breaker APIs in addition to the existing annotation-driven model documented in Retry Advice. This makes it possible to define typed retry and circuit breaker policies in code and reuse them for synchronous, reactive, and asynchronous flows.
The 5.0.x line also expanded context propagation capabilities, including support for scoped values and continued alignment with modern JDK context propagation patterns. See Context Propagation.
Summary
For teams adopting Micronaut Framework 5.0.x, the biggest benefits are stronger null-safety, more flexible AOP and retry models, better HTTP and multipart capabilities, richer configuration tooling, and continued improvements to the container’s compile-time-first runtime model.
Upgrading between Micronaut Framework versions
Check Micronaut Upgrade documentation to help you upgrade your Micronaut applications.
To learn what’s new, check the GitHub Release notes for each module you’re interested in. They contain a summary of all changes broken down by type.
Breaking Changes
Review the section on Breaking Changes and update your affected application code.
The following sections walk you through a Quick Start on how to use the Micronaut framework to set up a basic "Hello World" application.
Before getting started ensure you have a Java 8 or higher JDK installed, and it is recommended that you use a suitable IDE such as IntelliJ IDEA.
The Micronaut CLI is an optional but convenient way to create Micronaut applications. The best way to install Micronaut CLI on Unix systems is with SDKMAN which greatly simplifies installing and managing multiple Micronaut versions.
To see all available installation methods, check the Micronaut Starter documentation.
Using the Micronaut CLI you can create a new Micronaut application in either Groovy, Java, or Kotlin (the default is Java).
The following command creates a new "Hello World" server application in Java with a Gradle build:
|
Note
|
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|
Tip
|
Supply --build maven to create a Maven-based build instead
|
If you don’t have the CLI installed then you can also create the same application by visiting Micronaut Launch and clicking the "Generate Project" button or by using the following curl command on Unix systems:
curl https://launch.micronaut.io/hello-world.zip -o hello-world.zip
unzip hello-world.zip
cd hello-world|
Tip
|
Add ?build=maven to the URL passed to curl to generate a Maven project.
|
The previous steps created a new Java application in a directory called hello-world featuring a Gradle build. You can run the application with ./gradlew run:
$ ./gradlew run
> Task :run
[main] INFO io.micronaut.runtime.Micronaut - Startup completed in 540ms. Server Running: http://localhost:28933If you have created a Maven-based project, use ./mvnw mn:run instead.
|
Note
|
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:
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|
Tip
|
See the guide for Creating your First Micronaut Application to learn more. |
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);
}
}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.
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.
|
Tip
|
See the guide for Using IntelliJ IDEA to Develop Micronaut Applications to learn more. |
The Micronaut framework can easily be set up within Visual Studio Code by installing the following two extensions:
These extensions can be installed by clicking on the Install button in the banner of the pages linked above, or, by searching for the extensions within VS Code.
Once they are installed they will give you access to a host of Micronaut specific features such as:
Once the extensions are installed just type code . in any Micronaut project directory and the project will be opened within VS Code.
|
Note
|
For macOS, you need to install the code command by following these instructions.
|
To use Eclipse IDE, it is recommended you import your Micronaut project into Eclipse using either Gradle BuildShip for Gradle or M2Eclipse for Maven.
|
Note
|
The Micronaut framework requires Eclipse IDE 4.9 or higher |
Eclipse and Gradle
Once you have set up Eclipse 4.9 or higher with Gradle BuildShip, first run the gradle eclipse task from the root of your project, then import the project by selecting File → Import and choosing Gradle → Existing Gradle Project and navigating to the root directory of your project (where the build.gradle file is located).
Eclipse and Maven
For Eclipse 4.9 and above with Maven you need the following Eclipse plugins:
Once these are installed, import the project by selecting File → Import and choosing Maven → Existing Maven Project and navigating to the root directory of your project (where the pom.xml file is located).
Then enable annotation processing by opening Eclipse → Preferences and navigating to Maven → Annotation Processing and selecting the option Automatically configure JDT APT.
Apache NetBeans can open Maven and Gradle projects out of the box.
Make sure that the Java Web and EE feature is enabled at Tools → Plugins → Installed,
in order to have additional support for Micronaut, like code completion for configuration
and data elements.
As mentioned previously, the Micronaut framework includes both an HTTP server and an HTTP client. A low-level HTTP client is provided which you can use to test the HelloController created in the previous section.
In addition to a low-level client, the Micronaut framework features a declarative, compile-time HTTP client, powered by the Client annotation.
To create a client, create an interface annotated with @Client, for example:
To test the HelloClient, retrieve it from the ApplicationContext associated with the server:
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.
To deploy a Micronaut application you create an executable JAR file by running ./gradlew assemble or ./mvnw package.
The constructed JAR file can then be executed with java -jar. For example:
$ java -jar build/libs/hello-world-0.1-all.jarif building with Gradle, or
$ java -jar target/hello-world.jarif 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 dockerPushFor 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=dockerDeployment Guides
See the following guides to learn more about deploying Micronaut Applications:
Dependency Injection (often referred to as Inversion of Control) is a pattern that allows your code to remain independent of configuration and allows the definition of loosely coupled applications. Reducing coupling increases the ability to test your application by ensuring code is not tied directly to an external systems (like a database).
Unlike other frameworks which rely on runtime reflection and proxies, the Micronaut framework uses compile time data to implement dependency injection.
This is a similar approach taken by tools such as Google Dagger, which is designed primarily with Android in mind. Micronaut, on the other hand, is designed for building server-side microservices and provides many of the same tools and utilities as other frameworks but without using reflection or caching excessive amounts of reflection metadata.
The goals of the Micronaut IoC container are summarized as:
-
Use reflection as a last resort
-
Avoid runtime-generated proxies
-
Optimize start-up time
-
Reduce memory footprint
-
Provide clear, understandable error handling
Note that the IoC part of the Micronaut framework can be used completely independently of Micronaut for whatever application type you wish to build.
To do so, configure your build to include the micronaut-inject-java dependency as an annotation processor.
If you are creating custom compile-time annotations or annotation mappers, see Annotation Metadata for additional requirements around AnnotationMapper, NamedAnnotationMapper, and TypedAnnotationMapper.
The easiest way to do this is with Micronaut’s Gradle or Maven plugins. For example with Gradle:
The entry point for IoC is then the ApplicationContext interface, which includes a run method. The following example demonstrates using it:
ApplicationContext|
Note
|
The example uses Java try-with-resources syntax to ensure the ApplicationContext is cleanly shutdown when the application exits. |
A bean is an object whose lifecycle is managed by the Micronaut IoC container. That lifecycle may include creation, execution, and destruction. Micronaut implements the JSR-330 (jakarta.inject) - Dependency Injection for Java specification, hence to use Micronaut you simply use the annotations provided by jakarta.inject.
The following is a simple example:
public interface Engine { //
int getCylinders();
String start();
}@Singleton//
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;
}
}To perform dependency injection, run the BeanContext using the run() method and lookup a bean using getBean(Class), as per the following example:
final ApplicationContext context = ApplicationContext.run(Map.of("spec.name", "VehicleIntroSpec"));
Vehicle vehicle = context.getBean(Vehicle.class);
System.out.println(vehicle.start());The Micronaut framework automatically discovers dependency injection metadata on the classpath and wires the beans together according to injection points you define.
At this point, you may be wondering how Micronaut framework performs the above dependency injection without requiring reflection.
The key is a set of AST transformations (for Groovy) and annotation processors (for Java) that generate classes that implement the BeanDefinition interface.
Micronaut framework uses the ASM bytecode library to generate classes, and because Micronaut knows ahead of time the injection points, there is no need to scan all methods, fields, constructors, etc. at runtime like other frameworks such as Spring do.
Also, since reflection is not used when constructing the bean, the JVM can inline and optimize the code far better, resulting in better runtime performance and reduced memory consumption. This is particularly important for non-singleton scopes where application performance depends on bean creation performance.
In addition, with Micronaut framework your application startup time and memory consumption are not affected by the size of your codebase in the same way as with a framework that uses reflection. Reflection-based IoC frameworks load and cache reflection data for every single field, method, and constructor in your code. Thus, as your code grows in size so do your memory requirements, whilst with Micronaut this is not the case.
To help you easily understand what Micronaut is doing at startup and when a particular bean is created Micronaut includes a dependency injection tracing feature which can be activated in a number of different ways including via the ApplicationContextBuilder API.
The simplest way to activate injection trace mode is using an environment variable. For example if you are running your application locally you can do:
MICRONAUT_INJECT_TRACE=.+ ./gradlew runOr for Maven:
MICRONAUT_INJECT_TRACE=.+ ./mvnw mn:runTrace mode will output useful information such as:
-
The Configuration profile of the application
-
The applicable configuration and where it came from
-
The beans that are created, where they were created and how long was taken to create the bean.
The BeanContext is a container object for all your bean definitions (it also implements BeanDefinitionRegistry).
It is also the point of initialization for Micronaut. Generally speaking however, you don’t interact directly with the BeanContext API and can simply use jakarta.inject annotations and the annotations in the io.micronaut.context.annotation package for your dependency injection needs.
The Micronaut framework supports the following types of dependency injection:
-
Constructor injection (with an access level of public, protected, or default - and in case of multiple constructors, you can specify the one to be chosen, annotating it with
@Inject) -
Field injection
-
JavaBean property injection
-
Method parameter injection
|
Note
|
Classes or particular fields, methods can be excluded by adding an annotation @Vetoed |
|
Tip
|
See the guide for Micronaut Dependency Injection Types to learn more. |
Constructor injection is when dependencies are injected into the constructor for a type.
Constructor injection is the preferred and recommended injection type because Constructor injection:
-
Allows immutable types
-
Doesn’t require an additional annotation
-
Is less likely to result in a
NullPointerException -
More clearly expresses the dependencies of a particular type in one place.
The example in the next section uses constructor injection. Note that if you have multiple constructors you can disambiguate which constructor to invoke with the jakarta.inject.Inject annotation or the @Creator annotation:
In the above example retrieving the Vehicle type from the BeanContext will result in calling the Vehicle(Engine engine) constructor which will in turn resolve the Engine using the getDefault() method since it is annotated with @Creator.
|
Note
|
If no @Inject or @Creator is specified Micronaut will try to locate the first public constructor in the class otherwise a compilation error will occur.
|
If there are multiple possible candidates for a particular constructor argument a qualifier can be specified (such as jakarta.inject.Named) to disambiguate the injection. If it is not possible to disambiguate then the result will be a NonUniqueBeanException. See the Qualifiers section for more information.
|
Warning
|
If you use @Inject on a private constructor then the type will be instantiated via the Java reflection API which is not recommended.
|
You can inject non-final Java fields by annotating the field with jakarta.inject.Inject, for example:
-
Note that for Kotlin instead of an optional type (a type ending with
?) you can uselateinit var
Trying to inject a field that is declared final will result in a compilation error. Field injection should be seen as inferior to constructor injection as explained in the previous sections since it can result in the code being less-structured, harder to read and harder to test.
|
Warning
|
If the field is private scope or inaccessible then the field will be injected using the Java reflection API which is not recommended.
|
You can inject methods by annotating the method with jakarta.inject.Inject. For each argument of the method Micronaut will attempt to resolve the method argument as a bean. If any of the methods are not resolvable a NoSuchBeanException will be thrown.
|
Warning
|
If the method is private scope or inaccessible then the method will be injected using the Java reflection API which is not recommended.
|
Method injection can be useful if you need post construction initializers, however in general should be avoided in favour of constructor injection where possible.
Occasionally it is desirable for injection to be optional (ie. not fail with a NoSuchBeanException if there is no candidate bean available).
For example if you are trying to build an extensible system where a default implementation is provided by the type but consumers of your API can provide a bean that, if available, will be injected.
One way to make injection optional is to annotate the injected type with org.jspecify.annotations.Nullable which will result in null being injected by the framework if the bean is unavailable:
nullUsing org.jspecify.annotations.Nullable has the following considerations:
-
Can be used with any of the injection types (constructor, method or field injection)
-
Somewhere the code has to handle what happens if
nullis injected, for constructors this is easy since the constructor can handle thenull, but for fields and methods a@PostConstructmethod would need to be implemented to handlenullifnullis not desirable. -
Finally,
@Nullablecannot be used on primitive types likeint,longetc. when using configuration injection. To handle primitives you need to specify@Bindable(defaultValue="..")and provide a default value.
In addition to being able to inject beans, Micronaut framework natively supports injecting the following types:
| Type | Description | Example |
|---|---|---|
An |
|
|
An |
|
|
An |
|
|
A lazy |
|
|
A native array of beans of a given type |
|
|
A |
|
|
A |
|
|
Note
|
There are 3 different provider types supported, however the BeanProvider is the one we suggest to use.
|
|
Note
|
When injecting a In this example, the injected member variable |
|
Tip
|
A prototype bean will have one instance created per place the bean is injected. When a prototype bean is injected as a provider, each call to get() creates a new instance.
|
Collection Ordering
When injecting a collection of beans, they are not ordered by default. Implement the Ordered interface to inject an ordered collection. If the requested bean type does not implement Ordered, Micronaut framework searches for the @Order annotation on beans.
The @Order annotation is especially useful for ordering beans created by factories where the bean type is a class in a third-party library. In this example, both LowRateLimit and HighRateLimit implement the RateLimit interface.
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);
}
}When a collection of RateLimit beans are requested from the context, they are returned in ascending order based on the value in the annotation. Micronaut maps jakarta.annotation.Priority to @Order at build time, so @Priority values are treated the same as @Order and follow the same ascending numeric precedence (lower numbers mean higher precedence).
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. Micronaut also maps jakarta.annotation.Priority to @Order at build time, so the same mapped @Priority numeric semantics determine which bean has the highest precedence for single-bean injection.
|
Note
|
The Ordered interface is not taken into account when selecting a single instance as this would require instantiating the bean to resolve the order. |
If you have multiple possible implementations for a given interface to inject, you need to use a qualifier.
Once again Micronaut framework leverages JSR-330 and the Qualifier and Named annotations to support this use case.
Qualifying By Name
To qualify by name, use the Named annotation. For example, consider the following classes:
public interface Engine { //
int getCylinders();
String start();
}@Singleton
public class V6Engine implements Engine { //
@Override
public String start() {
return "Starting V6";
}
@Override
public int getCylinders() {
return 6;
}
}@Singleton
public class V8Engine implements Engine { //
@Override
public String start() {
return "Starting V8";
}
@Override
public int getCylinders() {
return 8;
}
}Micronaut framework is capable of injecting V8Engine in the previous example, because:
@Named qualifier value (v8) + type being injected simple name (Engine) == (case-insensitive) == The simple name of a bean of type Engine (V8Engine)
You can also declare @Named at the class level of a bean to explicitly define the name of the bean.
Qualifying By Annotation
In addition to being able to qualify by name, you can build your own qualifiers using the Qualifier annotation. For example, consider the following annotation:
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Retention(RUNTIME)
public @interface V8 {
}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;
}Qualifying By Annotation Members
Since Micronaut framework 3.0, annotation qualifiers can also use annotation members to resolve the correct bean to inject. For example, consider the following annotation:
You can then use the @Cylinders annotation on any bean and the members that are not annotated with @NonBinding are considered during dependency resolution:
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;
}Qualifying by Generic Type Arguments
Since Micronaut framework 3.0, it is possible to select which bean to inject based on the generic type arguments of the class or interface. Consider the following example:
public interface CylinderProvider {
int getCylinders();
}The CylinderProvider interface provides the number of cylinders.
You can define implementations of the Engine interface with different generic type arguments. For example for a V6 engine:
public class V6 implements CylinderProvider {
@Override
public int getCylinders() {
return 6;
}
}The above defines a V6 class that implements the CylinderProvider interface.
And a V8 engine:
public class V8 implements CylinderProvider {
@Override
public int getCylinders() {
return 8;
}
}The above defines a V8 class that implements the CylinderProvider interface.
You can then use the generic arguments when defining the injection point and Micronaut framework will pick the correct bean to inject based on the specific generic type arguments:
@Inject
public Vehicle(Engine<V8> engine) {
this.engine = engine;
}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();
}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";
}
}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";
}
}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.
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.
|
Tip
|
See the guide for Micronaut Patterns - Composite to learn more. |
Injecting Any Bean
If you are not particular about which bean gets injected then you can use the @Any qualifier which will inject the first available bean, for example:
@Inject @Any
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:
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:
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 { //
@Override
public String start() {
return "Starting V8";
}
@Override
public int getCylinders() {
return 8;
}
}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:
The following test demonstrates the behaviour of typed using programmatic lookup and the BeanContext API:
Micronaut framework features an extensible bean scoping mechanism based on JSR-330. The following default scopes are supported:
| 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. |
|
|
|
|
|
|
|
Note
|
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.
|
Note
|
If your bean fails to initialize in parallel, the application will be automatically shut down. |
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:
When you use Micronaut framework in environments such as Serverless Functions, you will not have an Application class, and instead you extend a Micronaut-provided class. In those cases, Micronaut provides methods which you can override to enhance the ApplicationContextBuilder
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:
The Refreshable scope is a custom scope that allows a bean’s state to be refreshed via:
-
/refreshendpoint. -
Publication of a RefreshEvent.
The following example illustrates @Refreshable scope behavior.
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());Scopes can be defined on meta annotations that you can then apply to your classes. Consider the following example meta annotation:
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:
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.
|
Note
|
The factory has the default scope singleton and will be destroyed with the context. If you want to dispose the factory after it produces a bean, use @Prototype scope. |
The return types of methods annotated with a bean scope annotation are the bean types. This is best illustrated by an example:
@Singleton
class CrankShaft {
}class V8Engine implements Engine {
private final int cylinders = 8;
private final CrankShaft crankShaft;
public V8Engine(CrankShaft crankShaft) {
this.crankShaft = crankShaft;
}
@Override
public String start() {
return "Starting V8";
}
}@Factory
class EngineFactory {
@Singleton
Engine v8Engine(CrankShaft crankShaft) {
return new V8Engine(crankShaft);
}
}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.
|
Note
|
If you take this approach you should not invoke other bean methods internally within the class. Instead, inject the types via parameters. |
|
Tip
|
To allow the resulting bean to participate in the application context shutdown process, annotate the method with @Bean and set the preDestroy argument to the name of the method to be called to close the bean.
|
Beans from Fields
With Micronaut framework 3.0 or above it is also possible to produce beans from fields by declaring the @Bean annotation on a field.
Whilst generally this approach should be discouraged in favour for factory methods, which provide more flexibility it does simplify testing code. For example with bean fields you can easily produce mocks in your test code:
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.
|
Note
|
If the bean method/field includes a scope or a qualifier any scope or qualifiers from the type will be omitted. |
|
Note
|
Qualifiers from the factory instance aren’t inherited to the beans. |
Primitive Beans and Arrays
Since Micronaut framework 3.1 it is possible to define and inject primitive types and array types from factories.
For example:
Primitive beans can be injected like any other bean:
import jakarta.inject.Named;
import jakarta.inject.Singleton;
@Singleton
public class V8Engine {
private final int cylinders;
public V8Engine(@Named("V8") int cylinders) { //
this.cylinders = cylinders;
}
public int getCylinders() {
return cylinders;
}
}Note that primitive beans and primitive array beans have the following limitations:
-
AOP advice cannot be applied to primitives or wrapper types
-
Due to the above custom scopes that proxy are not supported
-
The
@Bean(preDestroy=..)member is not supported
Programmatically Disabling Beans
Factory methods can throw DisabledBeanException to conditionally disable beans. Using @Requires should always be the preferred method to conditionally create beans; throwing an exception in a factory method should only be done if using @Requires is not possible.
For example:
public interface Engine {
Integer getCylinders();
}@EachProperty("engines")
public class EngineConfiguration implements Toggleable {
private boolean enabled = true;
private Integer cylinders;
@NotNull
public Integer getCylinders() {
return cylinders;
}
public void setCylinders(Integer cylinders) {
this.cylinders = cylinders;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}@Factory
public class EngineFactory {
@EachBean(EngineConfiguration.class)
public Engine buildEngine(EngineConfiguration engineConfiguration) {
if (engineConfiguration.isEnabled()) {
return engineConfiguration::getCylinders;
} else {
throw new DisabledBeanException("Engine configuration disabled");
}
}
}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;
}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();
}
}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:
|
Note
|
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.
|
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;
}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.
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 {
}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 |
|
Requires bean to be present in case no beanProperty specified |
|
Requires the specified property of bean to be present |
|
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.
Referencing bean properties in @Requires.
You can also reference other beans properties in @Requires to conditionally load beans. Similar to property requirements, you can specify required value or set the value bean property should not be equal to using notEquals annotation member. For the bean property to be checked, the bean of type specified in bean annotation member should be present within context, otherwise conditional bean will not be loaded.
Specified bean property is accessed through respective getter method whose presence and availability will be checked at compilation time.
Note that bean property is considered to be present in case it’s value is not null. Keep in mind that primitive properties are initialized with default values such as false for boolean and 0 for int, so they are considered to be set even if no value is explicitly specified for them.
Debugging Conditional Beans
If you have multiple conditions and complex requirements it may become difficult to understand why a particular bean has not been loaded.
To help resolve issues with conditional beans you can enable debug logging for the io.micronaut.context.condition package which will log the reasons why beans were not loaded.
<logger name="io.micronaut.context.condition" level="DEBUG"/>Consult the logging chapter for details howto setup logging.
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;
}You can define a class in src/test/java that replaces this class just for your tests:
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");
}
}|
Warning
|
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");
}
}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");
}
}The BookFactory#novel() method will not be replaced because the TextBook class is defined in the annotation.
Default Implementation
When exposing an API, you may want to define an implementation of the interface that is used as the default when injecting a particular interface. For this you can use the @DefaultImplementation annotation.
It may also be desirable to not expose the default implementation of an interface as part of the public API by making it package private in Java.
Doing so prevents users from being able to replace the implementation because they will not be able to reference the class.
The @DefaultImplementation annotation allows the framework to establish the implementation to replace if a user creates a bean that declares @Replaces(YourInterface.class).
For example consider:
A public API contract
import io.micronaut.context.annotation.DefaultImplementation;
@DefaultImplementation(DefaultResponseStrategy.class)
public interface ResponseStrategy {
}The default implementation
import jakarta.inject.Singleton;
@Singleton
class DefaultResponseStrategy implements ResponseStrategy {
}The custom implementation
import io.micronaut.context.annotation.Replaces;
import jakarta.inject.Singleton;
@Singleton
@Replaces(ResponseStrategy.class)
public class CustomResponseStrategy implements ResponseStrategy {
}In the above example, the CustomResponseStrategy replaces the DefaultResponseStrategy because the DefaultImplementation annotation points to the DefaultResponseStrategy.
A bean @Configuration is a grouping of multiple bean definitions within a package.
The @Configuration annotation is applied at the package level and informs the Micronaut framework that the beans defined with the package form a logical grouping.
The @Configuration annotation is typically applied to package-info classes. For example:
@Configuration
package my.package
import io.micronaut.context.annotation.ConfigurationWhere 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.packageIn the above example, all bean definitions within the annotated package are only loaded and made available if a javax.sql.DataSource bean is present. This lets you implement conditional autoconfiguration of bean definitions.
|
Note
|
Java and Kotlin also support this functionality via package-info.java. Kotlin does not support a package-info.kt as of version 1.3.
|
When The Bean Is Constructed
To invoke a method when the bean is constructed, use the jakarta.annotation.PostConstruct annotation:
To manage when a bean is constructed, see the section on bean scopes.
When The Bean Is Destroyed
To invoke a method when the bean is destroyed, use the jakarta.annotation.PreDestroy annotation:
For factory beans, the preDestroy value in the Bean annotation tells Micronaut framework which method to invoke.
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import jakarta.inject.Singleton;
@Factory
public class ConnectionFactory {
@Bean(preDestroy = "stop") //
@Singleton
public Connection connection() {
return new Connection();
}
}-
The
preDestroyvalue is set on the annotation
|
Note
|
Simply implementing the Closeable or AutoCloseable interface is not enough for a bean to be closed with the context. One of the above methods must be used.
|
Dependent Beans
Dependent beans are the beans used in the construction of your bean.
If the dependent bean’s scope is @Prototype or unknown, it will be destroyed along with your instance.
In some deployments, it is desirable to "gracefully" shut down an application, that is, to stop accepting new work but to finish in-progress tasks. In the Micronaut framework, a graceful shutdown means the following:
-
No new HTTP connections will be accepted
-
Existing connections will serve no new requests, but in-progress requests will still be served
-
Scheduled tasks will stop running, but in-progress tasks will finish uninterrupted
If the micronaut.lifecycle.graceful-shutdown.enabled config property is set to true, a graceful shutdown is
triggered automatically when the context stops (ApplicationContext.stop()). There is also a programmatic
GracefulShutdownManager API if you want more control over the shutdown process.
Graceful shutdown status can be read using the health management endpoint. This also returns the number of still-active tasks (e.g. connections or running scheduled tasks).
If you want to add graceful shutdown support to your own beans, implement
GracefulShutdownCapable. You will implement a shutdownGracefully method that triggers shutdown
and returns a future that should complete once the graceful shutdown is complete (e.g. all clients have closed their
connection). You can also optionally implement reportActiveTasks to give a number of active tasks for the health
endpoint.
The Micronaut framework supports a general event system through the context. The ApplicationEventPublisher API publishes events and the ApplicationEventListener API is used to listen to events. The event system is not limited to events that Micronaut publishes and supports custom events created by users. Context Events require Micronaut Context dependency:
implementation("io.micronaut:micronaut-context")micronaut-context is a transitive dependency of micronaut-http. If you use a Micronaut HTTP runtime, your project already includes the Micronaut-context dependency.
Publishing Events
The ApplicationEventPublisher API supports events of any type, although all events that the Micronaut framework publishes extend ApplicationEvent.
To publish an event, use dependency injection to obtain an instance of ApplicationEventPublisher where the generic type is the type of event and invoke the publishEvent method with your event object.
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());
}
}|
Warning
|
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.
ApplicationEventListenerimport io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.docs.context.events.SampleEvent;
import jakarta.inject.Singleton;
@Singleton
public class SampleEventListener implements ApplicationEventListener<SampleEvent> {
private int invocationCounter = 0;
@Override
public void onApplicationEvent(SampleEvent event) {
invocationCounter++;
}
public int getInvocationCounter() {
return invocationCounter;
}
}import io.micronaut.context.ApplicationContext;
import io.micronaut.docs.context.events.SampleEventEmitterBean;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class SampleEventListenerSpec {
@Test
void testEventListenerIsNotified() {
try (ApplicationContext context = ApplicationContext.run()) {
SampleEventEmitterBean emitter = context.getBean(SampleEventEmitterBean.class);
SampleEventListener listener = context.getBean(SampleEventListener.class);
assertEquals(0, listener.getInvocationCounter());
emitter.publishSampleEvent();
assertEquals(1, listener.getInvocationCounter());
}
}
}|
Note
|
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:
@EventListenerimport 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;
}
}If your listener performs work that might take a while, use the @Async annotation to run the operation on a separate thread:
@EventListenerimport io.micronaut.docs.context.events.SampleEvent;
import io.micronaut.runtime.event.annotation.EventListener;
import io.micronaut.scheduling.annotation.Async;
@Singleton
public class SampleEventListener {
private AtomicInteger invocationCounter = new AtomicInteger(0);
@EventListener
@Async
public void onSampleEvent(SampleEvent event) {
invocationCounter.getAndIncrement();
}
public int getInvocationCounter() {
return invocationCounter.get();
}
}import io.micronaut.context.ApplicationContext;
import io.micronaut.docs.context.events.SampleEventEmitterBean;
import org.junit.jupiter.api.Test;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
class SampleEventListenerSpec {
@Test
void testEventListenerIsNotified() {
try (ApplicationContext context = ApplicationContext.run()) {
SampleEventEmitterBean emitter = context.getBean(SampleEventEmitterBean.class);
SampleEventListener listener = context.getBean(SampleEventListener.class);
assertEquals(0, listener.getInvocationCounter());
emitter.publishSampleEvent();
await().atMost(5, SECONDS).until(listener::getInvocationCounter, equalTo(1));
}
}
}The event listener by default runs on the scheduled executor. You can configure this thread pool as required in your configuration file (e.g application.yml):
micronaut.executors.scheduled.type=scheduled
micronaut.executors.scheduled.core-pool-size=30You 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
@PostConstructevent hooks. -
BeanCreatedEventListener - allows modifying or replacing a bean after the bean is fully initialized and all
@PostConstructhooks 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; //
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); //
}
@Singleton
public Engine v8Engine() {
return engine;//
}
public void setRodLength(double rodLength) {
this.rodLength = rodLength;
}
}The BeanCreatedEventListener interface is more typically used to decorate or enhance a fully initialized bean, for example by creating a proxy.
|
Important
|
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. |
Since Micronaut framework 1.1, a compile-time replacement for the JDK’s Introspector class has been included.
The BeanIntrospector and BeanIntrospection interfaces allow looking up bean introspections to instantiate and read/write bean properties without using reflection or caching reflective metadata, which consume excessive memory for large beans.
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")|
Note
|
For Kotlin, add the micronaut-inject-java dependency in kapt scope, and for Groovy add micronaut-inject-groovy in compileOnly scope.
|
runtimeOnly("io.micronaut:micronaut-core")Once your build is configured you have a few ways to generate introspection data.
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;
}
}Once introspection data has been produced at compile time, retrieve it via the BeanIntrospection API:
It’s possible to apply the introspection to all the classes in one package. Simply create package-info file and annotate the package with @Introspected.
@Introspected
@AccessorsStyle(readPrefixes = "", writePrefixes = "")
package io.micronaut.docs.ioc.introspection.pck.foobar;
import io.micronaut.core.annotation.AccessorsStyle;
import io.micronaut.core.annotation.Introspected;|
Warning
|
The package should contain only classes that can be introspected. |
|
Note
|
Only classes located directly within the package are processed; subpackages are ignored. |
It is possible to use the @AccessorsStyle annotation with @Introspected:
Now it is possible to retrieve the compile time generated introspection using the BeanIntrospection API:
val introspection = BeanIntrospection.getIntrospection(Person::class.java)
val person = introspection.instantiate("John", 42)
Assertions.assertEquals("John", person.name())
Assertions.assertEquals(42, person.age())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:
|
Note
|
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.
|
|
Important
|
Introspections on fields are not possible in Kotlin because it is not possible to declare fields directly. |
The @Property annotation can be used on an introspected field or method to make that member an explicit bean property. This is useful when the member does not follow normal JavaBean getter or setter naming rules, or when the property should expose extra metadata such as an external serialized name.
import io.micronaut.core.annotation.Introspected;
@Introspected
class Book {
private String title;
private String author = "Ursula Le Guin";
@Introspected.Property("book_title")
public String title() {
return title;
}
@Introspected.Property("book_title")
public void title(String title) {
this.title = title;
}
@Introspected.Property(
value = "author_name",
accessKind = Introspected.Property.Access.READ
)
public String author() {
return author;
}
}In the example above, title() and title(String) are included as read/write bean property accessors even though they are not JavaBean getTitle and setTitle methods.
The author() method is included as a read-only property because its accessKind only contains Introspected.Property.Access.READ.
The value member is a shorthand for name.
The name member represents an external property name and is available through the property annotation metadata.
It does not change the Micronaut bean property name used with BeanIntrospection lookup methods.
If both value and name are declared, they must contain the same value.
BeanIntrospection<Book> introspection = BeanIntrospection.getIntrospection(Book.class);
BeanProperty<Book, String> property = introspection.getRequiredProperty("title", String.class);
Optional<String> externalName = property.stringValue(Introspected.Property.class, "name");The accessKind member controls whether the bean property can be read, written, or both:
-
Introspected.Property.Access.READallows the property to be read. -
Introspected.Property.Access.WRITEallows the property to be written. -
The default is both
READandWRITE.
If both a getter and setter exist, Micronaut can still expose a read/write property. Use accessKind when a field or method must restrict one side of access, for example read-only or write-only properties.
The access declaration applies to the whole bean property. Multiple @Property declarations for the same bean property must declare the same accessKind; conflicting declarations are rejected during compilation.
When @Property is declared on a field that belongs to the same bean property as getter or setter methods, Micronaut uses the normal bean accessor precedence: getter methods are used for reads and setter methods are used for writes when they are present.
The annotated field still contributes the mapped property metadata and can provide field access when a corresponding getter or setter is absent.
Set ignoreOtherAccessors to true when the annotated member must be used for its access direction even if another field, getter, or setter exists for the same bean property.
For example, an annotated field with the default read/write accessKind will be used for both reads and writes instead of same-name getter and setter methods.
Jackson Annotations
When Jackson annotations are present on an introspected type, Micronaut maps the following annotations to @Property at compile time:
-
com.fasterxml.jackson.annotation.JsonProperty -
com.fasterxml.jackson.annotation.JsonGetter -
com.fasterxml.jackson.annotation.JsonSetter
This means Jackson-style property names are visible through Micronaut bean property annotation metadata, and Jackson-annotated methods that do not follow JavaBean naming rules can still be recognized as bean properties.
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonSetter;
import io.micronaut.core.annotation.Introspected;
@Introspected
class User {
private String displayName;
@JsonGetter("display_name")
public String displayName() {
return displayName;
}
@JsonSetter("display_name")
public void displayName(String displayName) {
this.displayName = displayName;
}
}The displayName methods are treated as a bean property, and the external name display_name is available from the mapped @Property metadata.
For JsonGetter and JsonSetter, read and write availability is determined by the available bean property reader and writer.
Jackson annotations do not set ignoreOtherAccessors, so Micronaut keeps the same accessor precedence as Jackson Databind: getters are preferred for reads and setters are preferred for writes when they exist.
For JsonProperty, Micronaut also maps Jackson’s access member:
-
JsonProperty.Access.READ_ONLYmaps toIntrospected.Property.Access.READ. -
JsonProperty.Access.WRITE_ONLYmaps toIntrospected.Property.Access.WRITE. -
JsonProperty.Access.AUTOandJsonProperty.Access.READ_WRITEmap to both read and write access.
Because accessKind is property-level, multiple Jackson annotations that map to the same bean property must agree on the mapped access.
For classes with multiple constructors, apply the @Creator annotation to the constructor to use.
|
Note
|
This class has no default constructor, so calls to instantiate without arguments throw an InstantiationException. |
The @Creator annotation can be applied to static methods that create class instances.
|
Tip
|
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. |
If a type can only be constructed via the builder pattern then you can use the builder member of the @Introspected annotation to generate a dynamic builder. For example given this class:
@ReflectiveAccess
@Introspected(builder = @Introspected.IntrospectionBuilder(
builderClass = Person.Builder.class
))
public class Person {
private final String name;
private final int age;
private Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public static Builder builder() {
return new Builder();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
if (age != person.age) return false;
return Objects.equals(name, person.name);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
public static final class Builder {
private String name;
private int age;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Person build() {
Objects.requireNonNull(name);
if (age < 1) {
throw new IllegalArgumentException("Age must be a positive number");
}
return new Person(name, age);
}
}
}You can use the builder() method of the BeanIntrospection API to construct the instance:
BeanIntrospection<Person> introspection = BeanIntrospection.getIntrospection(Person.class);
BeanIntrospection.Builder<Person> builder = introspection.builder();
Person person = builder
.with("age", 25)
.with("name", "Fred")
.build();|
Tip
|
The builder() method also works regardless if the type uses a builder and can be used as a general abstraction for object construction. Note however that there is a slight performance overhead vs direct instantiation via the instantiate() method, hence the hasBuilder() method can be checked if optimized code paths are needed.
|
|
Note
|
Introspection Builder does not work with Groovy @Builder AST.
|
It is possible to introspect enums as well. Add the annotation to the enum, and it can be constructed through the standard valueOf method.
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 {
}In the above example the PersonConfiguration class generates introspections for the Person class.
|
Note
|
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.
|
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.
|
Note
|
The AnnotationMapper must be on the annotation processor classpath.
|
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.
You can annotate a Kotlin Data Class with @Introspected:
@Introspected
data class UserDataClass(val name: String)and instantiate it with the BeanIntrospection API:
|
Warning
|
Kotlin Inline Value Classes are not supported yet by the BeanIntrospection API. |
Since 4.1.x the @Mapper annotation can be used on any abstract method to automatically create a mapping between one type and another. Since 4.8.x the annotation can also be used for merging beans.
Inspired by similar functionality in libraries like Map Struct, a Mapper uses the Bean Introspection and Expressions features, built into the Micronaut Framework, which are already reflection free.
|
Note
|
For Mapping, base and target types need to be introspected. |
@Mapper Example
Given the following types:
@Introspected
public record ContactForm(String firstName, String lastName) {
}@Introspected
public record ContactEntity(Long id, String firstName, String lastName) {
}You can write an interface to define a mapping between both types by simply annotating a method with @Mapper.
import io.micronaut.context.annotation.Mapper;
public interface ContactMappers {
@Mapper
ContactEntity toEntity(ContactForm contactForm);
}The Micronaut compiler generates an implementation the previous an interface at compilation-time.
You can then inject a bean of type ContactMappers and easily map from one type to another.
ContactMappers contactMappers = context.getBean(ContactMappers.class);
ContactEntity contactEntity = contactMappers.toEntity(new ContactForm("John", "Snow"));
assertEquals("John", contactEntity.firstName());
assertEquals("Snow", contactEntity.lastName());@Mapping Example
Each abstract method can define a single @Mapper annotation or one or many @Mapping annotations to define how properties map onto the target type.
For example, given the following type:
import io.micronaut.core.annotation.Introspected;
@Introspected
public record Product(
String name,
double price,
String manufacturer) {
}It is common to want to alter this type’s representation in HTTP responses. For example, consider this response type:
import io.micronaut.core.annotation.Introspected;
@Introspected
public record ProductDTO(String name, String price, String distributor) {
}Here the price property is of a different type and an extra property exists called distributor. You could write manual logic to deal the mapping and these differences, or you could define a mapping:
import io.micronaut.context.annotation.Mapper.Mapping;
import jakarta.inject.Singleton;
@Singleton
public interface ProductMappers {
@Mapping(
to = "price",
from = "#{product.price * 2}",
format = "$#.00"
)
@Mapping(
to = "distributor",
from = "#{this.getDistributor()}"
)
ProductDTO toProductDTO(Product product);
default String getDistributor() {
return "Great Product Company";
}
}The from member can be used to define either a property name on the source type or an expression that reads values from the method argument and transforms them in whatever way you choose, including invoking other methods of the instance.
|
Note
|
A @Mapping definition is only needed if you need to apply a transformation for the mapping to be successful. Other properties will be automatically mapped and converted.
|
You can retrieve from the context or inject a bean of type ProductMappers. Then, you can use the toProductDTO method to map from the Product type to the ProductDTO type:
ProductMappers productMappers = context.getBean(ProductMappers.class);
ProductDTO productDTO = productMappers.toProductDTO(new Product(
"MacBook",
910.50,
"Apple"
));
assertEquals("MacBook", productDTO.name());
assertEquals("$1821.00", productDTO.price());
assertEquals("Great Product Company", productDTO.distributor());Given the following types:
@Introspected
record ChristmasPresent(
String packagingColor,
String type,
Float weight,
String greetingCard
) {
}
@Introspected
record PresentPackaging(
Float weight,
String color
) {
}
@Introspected
record Present(
Float weight,
String type
) {
}You can write an interface and method to merge the types by simply specifying two or more arguments to the method and annotating the method with @Mapper. Optionally, define custom mapping rules using @Mapping.
NOTE: Missing tag `imports` in `test-suite/src/test/java/io/micronaut/docs/ioc/mappers/ChristmasMappers.java`.You can then inject the type ChristmasMappers and easily merge the types.
ChristmasMappers mappers = context.getBean(ChristmasMappers.class);
ChristmasPresent result = mappers.merge(
new PresentPackaging(1f, "red"),
new Present(10f, "teddy bear")
);
assertEquals(11f, result.weight());
assertEquals("red", result.packagingColor());
assertEquals("teddy bear", result.type());
assertEquals("Merry christmas", result.greetingCard());And much more is possible! See more mapping examples in the following snippet.
See the Micronaut Validation documentation.
The methods provided by Java’s AnnotatedElement API in general don’t provide the ability to introspect annotations without loading the annotations themselves. Nor do they provide any ability to introspect annotation stereotypes (often called meta-annotations; an annotation stereotype is where an annotation is annotated with another annotation, essentially inheriting its behaviour).
To solve this problem many frameworks produce runtime metadata or perform expensive reflection to analyze the annotations of a class.
The Micronaut framework instead produces this annotation metadata at compile time, avoiding expensive reflection and saving memory.
The BeanContext API can be used to obtain a reference to a BeanDefinition which implements the AnnotationMetadata interface.
For example the following code obtains all bean definitions annotated with a particular stereotype:
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
The Micronaut framework will respect the rules defined in Java’s AnnotatedElement API with regard to annotation inheritance:
-
Annotations meta-annotated with Inherited will be available via the
getAnnotation*methods of the AnnotationMetadata API whilst those directly declared are available via thegetDeclaredAnnotation*methods. -
Annotations not meta-annotated with Inherited will not be included in the metadata
The Micronaut framework differs from the AnnotatedElement API in that it extends these rules to methods and method parameters such that:
-
Any annotations annotated with AnnotatedElement and present on a method of interface or super class
Athat is overridden by child interface or classBwill 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
Athat is overridden by child interface or classBwill be inherited into the AnnotationMetadata retrievable via the Argument interface from thegetArgumentsmethod 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:
With this meta-annotation in place you can add the annotation to a super class:
@SqlRepository
public 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;
}
}|
Note
|
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:
With these aliases in place, whether you define @Client("foo") or @Client(id="core-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.AnnotationMapperfile referencing the implementation class -
Add the JAR file containing the implementation to the
annotationProcessorclasspath (kaptfor Kotlin)
|
Note
|
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.
|
Note
|
The example above implements the NamedAnnotationMapper interface which allows annotations to be mixed with runtime code. To operate against a concrete annotation type, use TypedAnnotationMapper instead. However, TypedAnnotationMapper requires both the mapper and the annotation class itself to be available on the annotation processor classpath of the consuming project. If the annotation and mapper are defined in regular application code and not packaged as an annotation-processor-visible dependency, use NamedAnnotationMapper instead. |
You can use the @Import annotation to import beans from external, already compiled libraries that use JSR-330 annotations.
|
Note
|
Bean import is currently only supported in the Java language as other languages have limitations on classpath scanning during source code processing. |
For example, to import the JSR-330 TCK into an application, add a dependency on the TCK:
implementation("jakarta.inject:jakarta.inject-tck:2.0.1")Then define the @Import annotation on your Application class:
|
Note
|
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
|
As an alternative to the @Import annotation the @ClassImport annotation allows to process already compiled classes as if they were ordinary non-compiled classes. Internally all the type visitors will be run, allowing to create necessary metadata.
|
Note
|
Class import is currently only supported in the Java language as other languages have limitations on classpath scanning during source code processing. |
For example, to import the JSR-330 TCK into an application, add a dependency on the TCK:
implementation("jakarta.inject:jakarta.inject-tck:2.0.1")Then define the @ClassImport annotation on your Application class:
In the same way, it’s possible to import classes required for Micronaut Serialization or Micronaut Validation.
package example;
import io.micronaut.context.annotation.ClassImport;
import io.micronaut.serde.annotation.Serdeable;
@ClassImport(
packages = "my.external.library",
annotate = Serdeable.class)
public class Application {
}|
Note
|
At this moment, Micronaut doesn’t support reimporting classes already processed by the Micronaut annotation processor. |
There are scenarios where a class cannot be accessed to add or remove annotations for the annotation processor
The most used scenario is to modify the annotations when the class is imported with @ClassImport or a scenario when classes are generated and cannot be modified.
It’s possible to define a mixin class by annotating it with @Mixin and specifying which class does it reference.
All the annotations of the mixin will be copied to the original class and all the annotations of the constructor with matching parameters, fields of the same name and methods of the same name with matching parameters will be copied.
In this example we have a simple bean class included in an external library for which we want to apply Micronaut Serialization:
package my.external.library;
class MyBean {
String name;
}To add serialization annotations we can create a mixin that is referencing the original class:
|
Note
|
Mixins currently supported only for the Java language. |
Following the example from Importing Classes from Libraries of importing Jakarta Inject TCK, most of the beans from the TCK have the correct Jakarta Inject annotations except one bean which is not annotated at all, to fix that we can create a mixin to fix that:
package example;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.ClassImport;
import io.micronaut.context.annotation.Mixin;
import org.atinject.tck.auto.FuelTank;
@Mixin(FuelTank.class)
@Bean
class FuelTankMixin {
}
@ClassImport(packages = {"org.atinject.tck.auto", "org.atinject.tck.auto.accessories"})
class BeanImportTest {
}The mixin supports copying only specific annotations
by defining includeAnnotations, the set of annotations or packages that should be copied. Alternatively there is excludeAnnotation that will copy only annotations not excluded.
Each mixin point (constructor, method, field, parameter) can have specific rules of copying using @Filter.
The annotation @Filter also support removing existing annotations of the original class.
In this example all the Jakarta Validation are removed from the original method referenced by the mixin:
|
Note
|
Mixins only modify the Micronaut annotations metadata model. Original classes are not modified in any way. |
In Java, you can use annotations showing whether a variable can or cannot be null. Such annotations aren’t part of the standard library.
|
Note
|
Since Micronaut Framework 5, we recommend you use JSpecify Annotations instead of the Micronaut Nullability Annotations for better [Kotlin interoperability](https://kotlinlang.org/docs/whatsnew21.html#change-of-jspecify-nullability-mismatch-diagnostics-severity-to-strict) and [IDE](https://www.jetbrains.com/idea/whatsnew/#page__content-jspecify-support)/Tooling support. Indeed, since 5.0 Micronaut’s APIs use JSpecify annotations. |
Micronaut’s Nullability Annotations
Micronaut framework provides first-class nullability annotations:
-
@NonNull — the annotated element must never be null.
-
@Nullable — the annotated element may be null.
-
@NullMarked — sets a default non-null policy within the annotated scope (package, type, or method), unless overridden.
These annotations are designed for use on parameters, return values, fields, and type-use positions (for example, generic type arguments).
|
Note
|
Micronaut will default to the non-null policy in most of the places if not defined explicitly as nullable |
Why does the Micronaut framework add its own set of nullability annotations instead of using one of the existing nullability annotations libraries?
Throughout the history of the framework, we used other nullability annotation libraries. However, licensing issues made us change nullability annotations several times. To avoid having to change nullability annotations in the future, we added our own set of nullability annotations in Micronaut framework 2.4
Are Micronaut Nullability annotations recognized by Kotlin?
Kotlin does not recognize Micronaut framework’s nullability annotations. However, Micronaut supports other nullability annotations via AnnotationMapper.
|
Note
|
Micronaut framework supports other known nullability annotations from: Android, FindBugs, Javax, Eclipse, JetBrains, JSpecify |
To better support Kotlin, we recommended to use JSpecify or any other Kotlin recognizable nullability annotations.
Micronaut supports JSpecify annotations as an alternative to its own. Internally they are simply remapped to the Micronaut ones.
-
org.jspecify.annotations.Nullable— the annotated value may be null. -
org.jspecify.annotations.NonNull— expresses a not-null contract for the annotated element. -
org.jspecify.annotations.NullMarked— sets a default non-null policy within the annotated scope (package, type, or method), unless overridden.
|
Warning
|
There is a difference how the annotations should be put on an array field. Micronaut supports @io.micronaut.core.annotation.Nullable String[] myField but for JSpecify the correct syntax is String @org.jspecify.annotations.Nullable [] myField, the opposite will only mark the array component as nullable.
|
|
Note
|
You may mix JSpecify with Micronaut’s nullability annotations; however, prefer a single, consistent approach within a module or package to keep intent clear and reduce ambiguity. |
|
Tip
|
Adopt @NullMarked on a package or type to make non-null the default, then annotate only the exceptional cases with @Nullable.
|
The following class uses JSpecify to declare non-null by default with @NullMarked, and annotates only the few nullable cases:
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NonNull;
import jakarta.inject.Singleton;
@Singleton
@NullMarked
final class AccountService {
// Non-null by default due to @NullMarked
String greet(String name) {
return "Hello, " + name;
}
// Nullable return and parameter explicitly marked
@Nullable
String findNickname(@Nullable String userId) {
if (userId == null) {
return null;
}
// Lookup may return null if not found
return null;
}
}Micronaut framework has integrations with Spring in several forms. See the Micronaut Spring Documentation for more information.
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:5.1.15"
compileOnly "io.micronaut:micronaut-inject-java:5.1.15"
...
}If you use lint as part of your build you may also need to disable the invalid packages check since Android includes a hard-coded check that regards the jakarta.inject package as invalid unless you use Dagger:
android {
...
lintOptions {
lintOptions { warning 'InvalidPackage' }
}
}You can find more information on configuring annotations processors in the Android documentation.
|
Note
|
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:
Micronaut features a flexible configuration mechanism that allows reading configuration from a variety of sources into a unified model that can be bound to Java types annotated with @ConfigurationProperties.
Configuration can by default be provided in Java properties files or JSON with the ability to add support for more formats (such as YAML or Groovy configuration) by adding additional third-party libraries to your classpath. The convention is to search for a file named application.properties or application.json with support for other formats requiring additional dependencies as described by the following table:
| Format | File | Dependency Required |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
In addition, Micronaut framework allows overriding any property via system properties or environment variables.
Each source of configuration is modeled with the PropertySource interface and the mechanism is extensible, allowing the implementation of additional PropertySourceLoader implementations.
Micronaut also supports in-file configuration imports via micronaut.config.import, which allows one configuration source to load additional sources recursively.
See Property Sources for protocol-specific syntax (file, classpath, env, configtree) and optional import behavior.
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"));The active environment names allow loading different configuration files depending on the environment, and also using the @Requires annotation to conditionally load beans or bean @Configuration packages.
In addition, the Micronaut framework attempts to detect the current environments. For example within a Spock or JUnit test the TEST environment is automatically active.
Additional active environments can be specified using the micronaut.environments system property or the MICRONAUT_ENVIRONMENTS environment variable. These are specified as a comma-separated list. For example:
$ java -Dmicronaut.environments=foo,bar -jar myapp.jarThe above activates environments called foo and bar.
It is also possible to enable the detection of the Cloud environment the application is deployed to (this feature is disabled by default since Micronaut framework 4). See the section on Cloud Configuration for more information.
The Micronaut framework loads property sources based on the environments specified, and if the same property key exists in multiple property sources specific to an environment, the environment order determines which value to use.
The Micronaut framework uses the following hierarchy for environment processing (lowest to highest priority):
-
Deduced environments
-
Environments from the
micronaut.environmentssystem property -
Environments from the
MICRONAUT_ENVIRONMENTSenvironment variable -
Environments specified explicitly through the application context builder
NoteThis also applies to @MicronautTest(environments = …)
Automatic detection of environments can be disabled by setting the micronaut.env.deduction system property or the MICRONAUT_ENV_DEDUCTION environment variable to false. This prevents the Micronaut framework from detecting current environments, while still using any environments that are specifically provided as shown above.
$ java -Dmicronaut.env.deduction=false -jar myapp.jarAlternatively, you can disable environment deduction using the ApplicationContextBuilder deduceEnvironment method when setting up your application.
@Test
void testDisableEnvironmentDeductionViaBuilder() {
ApplicationContext ctx = ApplicationContext.builder()
.deduceEnvironment(false)
.properties(Collections.singletonMap("micronaut.server.port", -1))
.start();
assertFalse(ctx.getEnvironment().getActiveNames().contains(Environment.TEST));
ctx.close();
}The Micronaut framework supports the concept of one or many default environments.
A default environment is one that is only applied if no other environments are explicitly specified or deduced.
Environments can be explicitly specified either through the application context builder Micronaut.build().environments(…), through the micronaut.environments system property, or the MICRONAUT_ENVIRONMENTS environment variable.
Environments can be deduced to automatically apply the environment appropriate for cloud deployments.
If an environment is found through any of the above means, the default environment will not be applied.
To set the default environments, include a public static class that implements ApplicationContextConfigurer and is annotated with ContextConfigurer:
public class Application {
@ContextConfigurer
public static class DefaultEnvironmentConfigurer implements ApplicationContextConfigurer {
@Override
public void configure(@NonNull ApplicationContextBuilder builder) {
builder.defaultEnvironments(defaultEnvironment);
}
}
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
}|
Note
|
Previously, we recommended using Micronaut.defaultEnvironments("dev") however this does not allow the Ahead of Time (AOT) compiler to detect the default environments.
|
Since Micronaut framework 2.3 a banner is shown when the application starts. It is enabled by default, and it also shows the Micronaut version.
$ ./gradlew run
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
Micronaut (5.1.15)
17:07:22.997 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 611ms. Server Running: http://localhost:8080To 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:
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"));The PropertySource.of method can be used to create a PropertySource from a map of values.
Alternatively one can register a PropertySourceLoader by creating a META-INF/services/io.micronaut.context.env.PropertySourceLoader file containing a reference to the class name of the PropertySourceLoader.
Included PropertySource Loaders
Micronaut framework by default contains PropertySourceLoader implementations that load properties from the given locations and priority:
-
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 an absolute path (without any prefix),
-
the classpath with a
classpath:prefix.
-
-
Environment-specific properties from
application-{environment}.{extension} -
Application-specific properties from
application.{extension}
|
Note
|
'micronaut.config.files' will be ignored in bootstrap.yml or application.yml. |
Importing Additional Configuration
You can import additional configuration directly from a configuration file using micronaut.config.import.
The value can be a single string, a list, or indexed entries (micronaut.config.import[0], micronaut.config.import[1], …).
micronaut.config.import[0]=file:///etc/myapp/shared
micronaut.config.import[1]=classpath://overrides.yml
micronaut.config.import[2]=optional:env://MY_APP_INLINE_CONFIGImports are resolved recursively and support the following protocols:
-
file://– load from the file system -
classpath://– load exactly one matching classpath resource (fails if duplicates are found) -
classpath*://– load and merge all matching classpath resources in classpath discovery order -
env://– load key/value properties from an environment variable value, using the variable-name suffix (for example.yml) or?extension=yml/?extension=jsonto select non-properties formats -
configtree://– load a directory tree where file paths map to property keys
Prefix any import with optional: to skip it when the target is missing.
Without optional:, an ConfigurationException is thrown if the import cannot be loaded.
For file://, classpath://, and classpath*:// imports, if no extension is provided, Micronaut probes known configuration extensions (.properties, .json, .yml, and any additional enabled loaders).
For env:// imports, Micronaut defaults to .properties parsing. To import YAML or JSON from an environment variable, either include the format in the variable reference such as env://MY_APP_INLINE_CONFIG.yml, or specify it explicitly with env://MY_APP_INLINE_CONFIG?extension=yml.
When multiple resources with the same classpath name are present:
-
classpath://throws a ConfigurationException and reports all matching locations. -
classpath*://loads all matches in classpath order and merges them in that same order.
|
Important
|
file:///tmp/bar.properties (with three slashes) resolves to the absolute path /tmp/bar.properties.
|
Implementing a Custom PropertySourceImporter
You can add support for a custom import protocol by implementing PropertySourceImporter and registering it with Java ServiceLoader. Each importer declares its provider with getProvider(), converts the parsed ConnectionString into a typed declaration via newImportDeclaration(..), and then reads configuration from importPropertySource(..).
public final class DemoPropertySourceImporter implements PropertySourceImporter<DemoPropertySourceImporter.DemoImport> {
@Override
public String getProvider() {
return "demo";
}
@Override
public DemoImport newImportDeclaration(ConnectionString connectionString) {
return new DemoImport(connectionString.getPath());
}
@Override
public DemoImport newImportDeclaration(ConvertibleValues<Object> values) {
return new DemoImport(values.get("path", String.class).orElse("defaults"));
}
@Override
public Optional<PropertySource> importPropertySource(ImportContext<DemoImport> context) {
if (!"defaults".equals(context.importDeclaration().path())) {
return Optional.empty();
}
return Optional.of(PropertySource.of(
"demo:defaults",
Map.of("demo.message", "hello-from-demo-importer")
));
}
public record DemoImport(String path) {
}
}@Test
void importsDemoDefaults() {
try (ApplicationContext context = ApplicationContext.run()) {
DemoPropertySourceImporter importer = new DemoPropertySourceImporter();
DemoPropertySourceImporter.DemoImport declaration = importer.newImportDeclaration(ConnectionString.parse("demo://defaults"));
PropertySourceImporter.ImportContext<DemoPropertySourceImporter.DemoImport> importContext = new PropertySourceImporter.ImportContext<>() {
@Override
public Environment environment() {
return context.getEnvironment();
}
@Override
public ConnectionString connectionString() {
return ConnectionString.parse("demo://defaults");
}
@Override
public DemoPropertySourceImporter.DemoImport importDeclaration() {
return declaration;
}
@Override
public PropertySource.Origin parentOrigin() {
return PropertySource.Origin.of("classpath:application.yml");
}
@Override
public Optional<PropertySource> importPropertySource(ResourceLoader resourceLoader,
String resourcePath,
String sourceName,
PropertySource.Origin origin) {
return Optional.empty();
}
@Override
public Optional<PropertySource> importPropertySource(String content,
String sourceName,
String extension,
PropertySource.Origin origin) {
return Optional.empty();
}
@Override
public Optional<PropertySource> importClasspathPropertySource(String resourcePath,
String sourceName,
PropertySource.Origin origin,
boolean allowMultiple) {
return Optional.empty();
}
};
Optional<PropertySource> propertySource = importer.importPropertySource(importContext);
assertTrue(propertySource.isPresent());
assertEquals("hello-from-demo-importer", propertySource.get().get("demo.message"));
}
}Register your implementation in META-INF/services/io.micronaut.context.env.PropertySourceImporter:
io.micronaut.docs.config.importer.DemoPropertySourceImporterAfter registration, the importer is selected when micronaut.config.import uses your protocol, for example demo://defaults.
If your importer loads remote configuration, RetryablePropertySourceImporter in micronaut-discovery-core provides a reusable base class that standardizes retry behavior across both connection-string and map-based imports. It parses the same retry properties from either form and applies them with Micronaut’s programmatic retry support.
Standard retry properties supported by RetryablePropertySourceImporter are:
-
retry-attempts– maximum number of attempts -
retry-count– alias forretry-attempts -
retry-delay– delay between attempts -
retry-max-delay– maximum overall retry delay -
retry-multiplier– delay multiplier -
retry-jitter– retry jitter factor from0.0to1.0
|
Tip
|
.properties, .json, .yml are supported out of the box. For Groovy users .groovy is supported as well.
|
Duplicate Configuration Resources
If a configuration file (for example application.properties or application.yml) is present more than once on the classpath, Micronaut can be configured to:
-
fail fast with a clear error describing the conflicting locations,
-
take the first match (with optional warning), or
-
merge all matching resources.
The behavior can be customized using the ApplicationContextBuilder (including Micronaut).
Micronaut.build(args)
.configurationLoadingStrategy(ResourceLoadStrategy.builder()
.type(ResourceLoadStrategyType.FIRST_MATCH)
.warnOnDuplicates(true))
.start();To merge duplicates, set the strategy type to MERGE_ALL:
ApplicationContext ctx = ApplicationContext.builder()
.configurationLoadingStrategy(ResourceLoadStrategy.builder()
.type(ResourceLoadStrategyType.MERGE_ALL))
.start();When using MERGE_ALL, you can optionally specify a merge order based on artifact (JAR) name patterns:
ApplicationContext ctx = ApplicationContext.builder()
.configurationLoadingStrategy(ResourceLoadStrategy.builder()
.type(ResourceLoadStrategyType.MERGE_ALL)
.mergeOrder("lib-.*\\.jar", "app-.*\\.jar"))
.start();|
Note
|
mergeOrder is only supported when the strategy type is MERGE_ALL.
When resources are merged, later resources override earlier ones when the same property key is present.
|
Note that if you want full control of where your application loads configuration from you can disable the default PropertySourceLoader implementations listed above by calling the enableDefaultPropertySources(false) method of the ApplicationContextBuilder interface when starting your application.
In this case only explicit PropertySource instances that you add via the propertySources(..) method of the ApplicationContextBuilder interface will be used.
Supplying Configuration via Command Line
Configuration can be supplied at the command line using Gradle or our Maven plugin. For example:
$ ./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
}
}Secrets and Sensitive Configuration
It is important to note that it is not recommended to store sensitive configuration such as passwords and tokens within configuration files that can potentially be checked into source control systems.
It is good practise to instead externalize sensitive configuration completely from the application code using preferably an external secret manager system (there are many options here, many provided by Cloud providers) or environment variables that are set during the deployment of the application. You can also use property placeholders (see the following section), to customize names of the environment variables to use and supply default values:
datasources.default.url=${JDBC_URL:`jdbc:mysql://localhost:3306/db`}
datasources.default.username=${JDBC_USER:root}
datasources.default.password=${JDBC_PASSWORD:}
datasources.default.dialect=MYSQL
datasources.default.driverClassName=${JDBC_DRIVER:com.mysql.cj.jdbc.Driver}To securely externalize configuration consider using a secrets manager system supported by the Micronaut framework such as:
Property Value Placeholders
As mentioned in the previous section, the Micronaut framework includes a property placeholder syntax to reference configuration properties both within configuration values and with any Micronaut annotation. See @Value and the section on Configuration Injection.
|
Tip
|
Programmatic usage is also possible via the PropertyPlaceholderResolver interface. |
The basic syntax is to wrap a reference to a property in ${…}. For example:
myapp.endpoint=http://${micronaut.server.host}:${micronaut.server.port}/fooThe 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}/fooThe 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`}/fooThe above example looks for a server.address property and defaults to http://localhost:8080. This default value is escaped with backticks since it has a : character.
Property Value Binding
Note that these property references should be in kebab case (lowercase and hyphen-separated) when placing references in code or in placeholder values. For example, use micronaut.server.default-charset and not micronaut.server.defaultCharset.
The Micronaut framework still allows specifying the latter in configuration, but normalizes the properties into kebab case form to optimize memory consumption and reduce complexity when resolving properties. The following table summarizes how properties are normalized from different sources:
| 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.
|
Important
|
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.
|
|
Important
|
Because of the way characters are treated in environment variables, it is not possible to target a poperty with in the name. Per the above, properties should be in kebab case to maintain the ability to target the property with environment variables.
|
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();
}
}|
Note
|
The configuration above does not have any impact on property placeholders. It is still possible to reference an environment variable in a placeholder regardless of whether environment configuration is disabled, or even if the specific property is explicitly excluded. |
Using Random Properties
You can use random values by using the following properties. These can be used in configuration files as variables like the following.
micronaut.application.name=myapplication
micronaut.application.instance.id=${random.shortuuid}| Property | Value |
|---|---|
random.port |
An available random port number |
random.int |
Random int |
random.integer |
Random int |
random.long |
Random long |
random.float |
Random float |
random.shortuuid |
Random UUID of only 10 chars in length (Note: As this isn’t full UUID, collision COULD occur) |
random.uuid |
Random UUID with dashes |
random.uuid2 |
Random UUID without dashes |
The random.int, random.integer, random.long and random.float properties supports a range suffix whose syntax is one of as follows:
-
(max)where max is an exclusive value -
[min,max]where min being inclusive and max being exclusive values.
instance.id=${random.int[5,10]}
instance.count=${random.int(5)}|
Note
|
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.
|
Important
|
It is recommended to use this feature sparingly to ensure fast startup. |
You can inject configuration values into beans using the @Value annotation.
Using the @Value Annotation
Consider the following example:
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:
|
Note
|
Because it is not possible to define a default value with @Property, if the value doesn’t exist or cannot be converted to the required type, bean instantiation will fail.
|
The above instead injects the value of the my.engine.cylinders property resolved from application configuration. If the property cannot be found in configuration, an exception is thrown. As with other types of injection, the injection point can also be annotated with @Nullable to make the injection optional.
You can also use this feature to resolve sub maps. For example, consider the following configuration:
datasources.default.name=mydb
jpa.default.properties.hibernate.hbm2ddl.auto=update
jpa.default.properties.hibernate.show_sql=trueTo 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.
|
Tip
|
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. |
Since 4.0, Micronaut framework supports embedding evaluated expressions in annotation values using #{…} syntax which
allows to achieve even more flexibility while configuring your application.
@Value("#{ T(Math).random() }")
double injectedValue;Expressions can be defined whenever an annotation member accepts a string or an array of strings.
|
Note
|
Expressions are currently not supported for "type use" annotations (that declare ElementType.TYPE_USE).
|
@Singleton
@Requires(env = {"dev", "#{ 'test' }"})
public class EvaluatedExpressionInArray {}You can also embed one or more expressions in a string template in a similar manner to embedding properties with the ${…} syntax.
@Value("http://#{'hostname'}/#{'path'}")
String url;Evaluated Expressions are validated and compiled at build time which guarantees type safety at runtime.
Once an application is running expressions are evaluated on demand as part of annotation metadata resolution. The usage of expressions does not impact performance as evaluation process is completely reflection free.
Note that, for security reasons expressions cannot be dynamically compiled at runtime from potentially untrusted input. All expressions are compiled and checked statically during the compilation process of the application with errors reported as compilation failures.
In general, expressions can be treated as statement written using a programming language with reduced set of available features. Even though the complexity of expression is only limited by the list of supported syntax constructs, it is in general not recommended to place complex logic inside an expression as there are usually better ways to achieve the same result.
Using Expressions in Micronaut framework
Expressions can be used anywhere throughout the Micronaut framework and associated modules, but as an example, you can use them to implement simple scheduled job control, for example:
|
Tip
|
You can also use expressions to perform conditional routing using the @RouteCondition annotation. |
Evaluated Expression Language Reference
The Evaluated Expressions syntax supports the following functionality:
-
Literal Values
-
Math Operators
-
Comparison Operators
-
Logical Operators
-
Ternary Operator
-
Type References
-
Method Invocation
-
Property Access
-
Retrieving Beans from Bean Context
-
Retrieving Environment Properties
Literal Values
The following types of literal values are supported:
-
null -
boolean values (
true,false) -
strings, which need to be surrounded with single quotation mark (
') -
numeric values (
int,long,float,double)
Integer and Long values can also be specified in hexadecimal or octal notation. Float and Double values can also be specified in exponential notation. All numeric values can be negative as well.
#{ null }
#{ true }
#{ 'string value' }
#{ 10 }
#{ 0xFFL }
#{ 10L }
#{ .123f }
#{ 1E+1d }
#{ 123D }Math Operators
The supported mathematical operators are `, `-`, `*`, `/`, `%`, `^`. Math operators can only be applied to numeric
values (except ` which can be used for string concatenation as well). Mathematical operations are performed in order
enforced by standard operator precedence. You can also change evaluation order by using brackets ().
/ and % operators can be aliased by div and mod keywords respectively.
#{ 1 + 2 } // 3
#{ 'a' + 'b' + 'c' } // 'abc'
#{ 7 - 3 } // 4
#{ 7 * 3 } // 21
#{ 7 * ( 3 + 1) } // 28
#{ 15 / 3 } // 5
#{ 15 div 3 } // 5
#{ 15 % 3 } // 0
#{ 15 mod 3 } // 0
// Unlike in Java, ^ operator means exponentiation
#{ 3 ^ 2 } // 9Comparison Operators
The following comparison operators are supported: ==, !=, >, <, >=, <=, matches
Comparison operations are performed in order enforced by standard operator precedence.
You can also change evaluation order by using brackets ().
Equality check is supported for both primitive types and objects. It is performed using Object.equals() method.
>, <, >=, <= operations can be applied to numeric types or types that implement java.lang.Comparable
interface.
matches keyword can be used to determine whether a string matches provided regular expression which has to
be specified as string literal. The regular expression itself will be checked for validity at compilation time.
#{ 1 + 2 == 3 } // true
#{ 'abc' != 'abc' } // false
#{ 7 > 3 } // true
#{ 7 < 3 } // false
#{ 7 >= 7 } // true
#{ 7 <= 8 } // false
#{ 'AbC' matches '[A-Za-z*' } // Compilation failure
#{ 'AbC' matches '[A-Za-z]*' } // true
#{ 'AbC' matches '[a-z]*' } // falseLogical Operators
The following logical operators are supported:
-
&&(can be aliased withand) -
||(can be aliased withor), -
!(can be aliaded withnot) -
empty/not empty(works with strings, collections, arrays, and maps)
Logical operations are performed in order enforced by standard operator precedence.
You can also change evaluation order by using brackets ().
#{ true && false } // false
#{ true and true } // true
#{ true || false } // true
#{ false or false } // false
#{ !false } // true
#{ !!true } // true
#{ empty '' } // true
#{ not empty '' } // falseTernary Operator
A standard ternary operator is supported to allow specifying if-then-else conditional logic in expression
condition ? thenBranch : elseBranchwhere condition evaluation should provide boolean value, and the complexity of then and else branches is not
limited.
#{ 15 > 10 ? 'a' : 'b' } // 'a'
#{ 15 >= 16 ? 'a' : 'b' } // 'b'Dot and Safe Navigation Operator
The dot operator can be used to access methods and properties of a value within an expression. For example:
#{ collection.size() > 0 }
#{ foo.bar.name == "Fred" }You can also use the safe dereference operator ?. to navigate paths in a null safe way:
#{ foo?.bar?.name == "Fred" }|
Tip
|
When used, the safe dereference operator will also automatically unwrap Java’s Optional type.
|
Type References
A predefined syntax construct T(…) can be used to reference a class. The value inside brackets should be fully
qualified class name (including the package name). The only exception is java.lang.* classes which can be referenced
directly by only specifying the simple class name. Primitive types can not be referenced.
Type References are evaluated in different ways depending on the context.
Simple type reference
A simple type reference is resolved as a Class<?> object.
#{ T(java.lang.String) } // String.classSame rule applies if type reference is specified as a method argument.
Type check with instanceof
A Type Reference can be used as the right-hand side part of the instanceof operator
#{ 'abc' instanceof T(String) } // truewhich is equivalent to the following Java code and will be evaluated as a boolean value:
"abc" instanceof StringStatic method invocation
Type Reference can be used to invoke a static method of a class
#{ T(Math).random() }Expression Evaluation Context
By default, the only methods you can invoke inside Evaluated Expressions are static methods using type references.
The available methods can be extended by extended the evaluation context. There are two ways to extend the evaluation context. The first involves registering new context class via a custom TypeElementVisitor.
|
Note
|
The TypeElementVisitor has to be on the annotation processor classpath, therefore needs to be defined in a separate module that can be included on this classpath. |
Once a class is registered within evaluation context the methods and properties of the class are available for referencing in evaluated expressions.
Consider the following example:
import jakarta.inject.Singleton;
import java.util.Random;
@Singleton
public class CustomEvaluationContext {
private Random random = random = new Random();
public int generateRandom(int min, int max) {
return random.nextInt(max - min) + min;
}
}|
Note
|
The class should be resolvable as a bean can use jakarta.inject annotations to inject other types if necessary. In addition, for performance reasons all evaluation context classes are effectively singleton regardless of the defined scope.
|
Registering this class can be achieved with a custom implementation of ExpressionEvaluationContextRegistrar that is registered via service loader as a TypeElementVisitor (create a new META-INF/services/io.micronaut.inject.visitor.TypeElementVisitor file referencing the new class) and placed on the annotation processor classpath:
import io.micronaut.expressions.context.ExpressionEvaluationContextRegistrar;
public class ContextRegistrar implements ExpressionEvaluationContextRegistrar {
@Override
public String getContextClassName() {
return "io.micronaut.docs.expressions.CustomEvaluationContext";
}
}Method generateRandom(int, int) can now be used within Evaluated Expression in the following way:
package io.micronaut.docs.expressions;
import io.micronaut.context.annotation.Value;
import jakarta.inject.Singleton;
@Singleton
public class ContextConsumer {
@Value("#{ generateRandom(1, 10) }")
public int randomField;
}At runtime, the bean will be retrieved from application context and respective method will be invoked.
If a matching method is not found within evaluation context at compilation time, the compilation will fail. A compilation error will also occur if multiple suitable methods are found in the evaluation context, keep that in mind if you provide multiple ExpressionEvaluationContextRegistrar that a conflict can occur as these types are effectively global.
The methods will be considered ambiguous (leading to compilation failure) when their names are the same and list of provided arguments matches multiple methods parameters.
Using a ExpressionEvaluationContextRegistrar makes its methods and properties available for evaluated expressions within any annotation in a global manner.
However, you can also specify evaluation context scoped to concrete annotation or annotation member using @AnnotationExpressionContext.
Again context classes need to be explicitly defined as beans to make them available for retrieval from application context at runtime.
Method Invocation
You can invoke both static methods using type references, methods from evaluation context and methods on objects, which means method chaining is supported.
import io.micronaut.context.annotation.Value;
import jakarta.inject.Singleton;
@Singleton
class CustomEvaluationContext {
public String stringValue() {
return "stringValue";
}
}
@Singleton
class ContextConsumer {
@Value("#{ #stringValue().length() }")
public int stringLength;
}Varargs methods invocation is supported as well. Note that if last parameter of a method is an array, you can still invoke it providing list of arguments separated by comma without explicitly wrapping it into array. So in this case it will be treated in same way as if last method argument was explicitly specified as varargs parameter.
import io.micronaut.context.annotation.Value;
import jakarta.inject.Singleton;
@Singleton
class CustomEvaluationContext {
public int countIntegers(int... values) {
return values.length;
}
public int countStrings(String[] values) {
return values.length;
}
}
@Singleton
class ContextConsumer {
@Value("#{ #countIntegers(1, 2, 3) }")
public int totalIntegers;
@Value("#{ #countStrings('a', 'b', 'c') }")
public int totalStrings;
}Property Access
JavaBean properties can be accessed simply be referencing their names from evaluation context prefixed with #. Bean
properties can also be chained with dot in the same way as methods.
import io.micronaut.context.annotation.Value;
import jakarta.inject.Singleton;
@Singleton
class CustomEvaluationContext {
public String getName() {
return "Bob";
}
public int getAge() {
return 25;
}
}
@Singleton
class ContextConsumer {
@Value("#{ 'Name is ' + #name + ', age is ' + #age }")
public String value;
}Retrieving Beans from Bean Context
A predefined syntax construct ctx[…] can be used to retrieve beans from bean
context. The argument inside square brackets has to be a fully qualified class name (note that T(…) wrapper is
optional and can be omitted for simplicity).
#{ ctx[T(io.micronaut.example.ContextBean)] }
#{ ctx[io.micronaut.example.ContextBean] }Retrieving Environment Properties
A syntax construct env[…] can be used to retrieve environment properties by name.
The expression inside square brackets has to resolve to string value, otherwise compilation will fail. If property
value will be absent at runtime, the expression will return null
#{ env['test.property'] }You can create type-safe configuration by creating classes that are annotated with @ConfigurationProperties.
The Micronaut framework will produce a reflection-free @ConfigurationProperties bean and will also at compile time calculate the property paths to evaluate, greatly improving the speed and efficiency of loading @ConfigurationProperties.
For example:
Once you have prepared a type-safe configuration it can be injected into your beans like any other bean:
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");
map.put("spec.name", "VehiclePropertiesSpec");
ApplicationContext applicationContext = ApplicationContext.run(map, "test");
Vehicle vehicle = applicationContext.getBean(Vehicle.class);
System.out.println(vehicle.start());The above example prints: "Ford Engine Starting V8 [rodLength=6.0]"
You can directly reference configuration properties in @Requires annotation to conditionally load beans using the following syntax: @Requires(bean=Config.class, beanProperty="property", value="true")
Note for more complex configurations you can structure @ConfigurationProperties beans through inheritance.
For example creating a subclass of EngineConfig with @ConfigurationProperties('bar') will resolve all properties under the path my.engine.bar.
|
Note
|
YAML Reserved Words When using YAML configuration files, certain words are reserved by the YAML specification and will be automatically converted to boolean values. These include:
If you use these words as unquoted property keys in YAML, they will be parsed as booleans instead of strings: To use these words as property names, you must quote the keys: This is a limitation of YAML/SnakeYAML parsing and not specific to Micronaut. For more information, see the YAML 1.2 Specification. |
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).
Change accessors style
Since 3.3, the Micronaut framework supports defining different accessors prefixes for getters and setter other than the default get and set defined for Java Beans. Annotate your POJO or @ConfigurationProperties class with the @AccessorsStyle annotation.
This is useful when you write the getters and setters in a fluent way. For example:
@AccessorsStyleNow you can inject EngineConfig and use it with engineConfig.manufacturer() and engineConfig.cylinders() to retrieve the values from configuration.
Property Type Conversion
The Micronaut framework uses the ConversionService bean to convert values when resolving properties. You can register additional converters for types not supported by Micronaut by defining beans that implement the TypeConverter interface.
The Micronaut framework features some built-in conversions that are useful, which are detailed below.
Duration Conversion
Durations can be specified by appending the unit with a number. Supported units are s, ms, m etc. The following table summarizes examples:
| Configuration Value | Resulting Value |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
For example to configure the default HTTP client read timeout:
micronaut.http.client.read-timeout=15sList / 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[0]=1
my.app.integers[1]=2
my.app.urls[0]=http://foo.com
my.app.urls[1]=http://bar.comFor 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:
@ReadableBytespublic 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 the Micronaut framework where to look for configuration values. By default, builder methods use the configuration prefix specified in a class-level @ConfigurationProperties annotation.
For example:
|
Note
|
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());
}
}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("spec.name", "VehicleBuilderSpec");
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());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).
@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;
}
}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);
map.put( "spec.name", "VehicleMapFormatSpec");
ApplicationContext applicationContext = ApplicationContext.run(map, "test");
Vehicle vehicle = applicationContext.getBean(Vehicle.class);
System.out.println(vehicle.start());The above example prints: "Engine Starting V8 [sensors=2]"
|
Tip
|
See the guide for @Configuration and @ConfigurationBuilder to learn more. |
The Micronaut framework includes an extensible type conversion mechanism. To add additional type converters you register beans of type TypeConverter.
The following example shows how to use one of the built-in converters (Map to an Object) or create your own.
Consider the following ConfigurationProperties:
@ConfigurationProperties(MyConfigurationProperties.PREFIX)
public class MyConfigurationProperties {
public static final String PREFIX = "myapp";
protected LocalDate updatedAt;
public LocalDate getUpdatedAt() {
return updatedAt;
}
}The type MyConfigurationProperties has a property named updatedAt of type LocalDate.
To bind this property from a map via configuration:
This won’t work by default, since there is no built-in conversion from Map to LocalDate. To resolve this, define a custom TypeConverter:
|
Note
|
It’s possible to add a custom type converter into ConversionService.SHARED by registering it in a TypeConverterRegistrar via the service loader.
|
The @ConfigurationProperties annotation is great for a single configuration class, but sometimes you want multiple instances, each with its own distinct configuration. That is where EachProperty comes in.
The @EachProperty annotation creates a ConfigurationProperties bean for each sub-property within the given name. As an example consider the following class:
|
Note
|
Micronaut configuration uses kebap case, not lower camel case. For example, using @EachProperty("my-bean") works, but @EachProperty("myBean") fails.
|
The above DataSourceConfiguration defines a url property to configure one or more data sources. The URLs themselves can be configured using any of the PropertySource instances evaluated to Micronaut:
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")
));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:
List-Based Binding
The default behavior of @EachProperty is to bind from a map style of configuration, where the key is the named qualifier of the bean and the value is the data to bind from. For cases where map style configuration doesn’t make sense, it is possible to inform the Micronaut framework that the class is bound from a list. Simply set the list member on the annotation to true.
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:
|
Note
|
@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:
Since 1.3, Micronaut framework supports the definition of immutable configuration. Immutable configuration with an interface requires the Micronaut Context dependency.
implementation("io.micronaut:micronaut-context")micronaut-context is a transitive dependency of micronaut-http. If you use a Micronaut HTTP runtime, your project already includes the Micronaut-context dependency.
There are two ways to define immutable configuration. The preferred way is to define an interface annotated with @ConfigurationProperties. For example:
In this case the Micronaut framework provides a compile-time implementation that delegates all getters to call the getProperty(..) method of the Environment interface.
This has the advantage that if the application configuration is refreshed (for example by invoking the /refresh endpoint), the injected interface automatically sees the new values.
|
Note
|
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:
The @ConfigurationInject annotation provides a hint to the Micronaut framework to prioritize binding values from configuration instead of injecting beans.
|
Note
|
Using this approach, to make the configuration refreshable, add the @Refreshable annotation to the class as well. This allows the bean to be re-created in the case of a runtime configuration refresh event. |
There are a few exceptions to this rule. Micronaut framework will not perform configuration binding for a parameter if any of these conditions is met:
-
The parameter is annotated with
@Value(explicit binding) -
The parameter is annotated with
@Property(explicit binding) -
The parameter is annotated with
@Parameter(parameterized bean handling) -
The parameter is annotated with
@Inject(generic bean injection) -
The type of the parameter is annotated with a bean scope (such as
@Singleton)
Once you have prepared a type-safe configuration it can be injected into your beans like any other bean:
Configuration values can then be supplied when running the application. For example:
ApplicationContext applicationContext = ApplicationContext.run(Map.of(
"spec.name", "VehicleImmutableSpec",
"my.engine.cylinders", "8",
"my.engine.crank-shaft.rod-length", "7.0"
));
Vehicle vehicle = applicationContext.getBean(Vehicle.class);
System.out.println(vehicle.start());The above example prints: "Ford Engine Starting V8 [rodLength=7B.0]"
Using Java Record Classes to define immutable configuration
For Java language applications, it’s also possible to use Java Record Classes for immutable configuration with @ConfigurationProperties. For example:
|
Note
|
From a performance perspective Java records are better than interfaces. |
Customizing accessors
As already explained in Change accessors style, it is also possible to customize the accessors when creating immutable configuration properties:
Most application configuration is stored in your configuration file (e.g application.yml), environment-specific files like application-{environment}.{extension}, environment and system properties, etc.
These configure the application context.
But during application startup, before the application context is created, a "bootstrap" context can be created to store configuration necessary to retrieve additional configuration for the main context. Typically, that additional configuration is in some remote source.
The bootstrap context is enabled depending on the following conditions. The conditions are checked in the following order:
-
If The BOOTSTRAP_CONTEXT_PROPERTY system property is set, that value determines if the bootstrap context is enabled.
-
If The application context builder option bootstrapEnvironment is set, that value determines if the bootstrap context is enabled.
-
If a BootstrapPropertySourceLocator bean is present the bootstrap context is enabled. Normally this comes from the
micronaut-discovery-clientdependency. If you provide a custom ConfigurationClient for distributed configuration, make sure that dependency is present so the bootstrap/distributed configuration infrastructure is loaded.
Configuration properties that must be present before application context configuration properties are resolved, for example when using distributed configuration, are stored in a bootstrap configuration file. Once it is determined the bootstrap context is enabled (as described above), the bootstrap configuration files are read using the same rules as regular application configuration.
See the property source documentation for the details. The only difference is the prefix (bootstrap instead of application).
The file name prefix bootstrap is configurable with a system property micronaut.bootstrap.name.
|
Note
|
The bootstrap context configuration is carried over to the main context automatically, so it is not necessary for configuration properties to be duplicated in the main context. In addition, the bootstrap context configuration has a higher precedence than the main context, meaning if a configuration property appears in both contexts, then the value will be taken from the bootstrap context first. |
That means if a configuration property is needed in both places, it should go into the bootstrap context configuration.
See the distributed configuration section of the documentation for the list of integrations with common distributed configuration solutions.
Bootstrap Context Beans
In order for a bean to be resolvable in the bootstrap context it must be annotated with @BootstrapContextCompatible. If any given bean is not annotated then it will not be able to be resolved in the bootstrap context. Typically, any bean that is participating in the process of retrieving distributed configuration needs to be annotated.
For example, if you implement custom distributed configuration that is resolved during bootstrap, any supporting beans it depends on must be bootstrap-compatible, and the application must include the discovery client infrastructure, typically by adding the io.micronaut.discovery:micronaut-discovery-client dependency.
Micronaut framework provides basic support for JMX.
For more information, see the documentation for the micronaut-jmx project.
Aspect-Oriented Programming (AOP) has historically had many incarnations and some very complicated implementations. Generally AOP can be thought of as a way to define cross-cutting concerns (logging, transactions, tracing, etc.) separate from application code in the form of aspects that define advice.
There are typically two forms of advice:
-
Around Advice - decorates a method or class
-
Introduction Advice - introduces new behaviour to a class.
In modern Java applications, declaring advice typically takes the form of an annotation. The most well-known annotation advice in the Java world is probably @Transactional, which demarcates transaction boundaries in Spring and Grails applications.
The disadvantage of traditional approaches to AOP is the heavy reliance on runtime proxy creation and reflection, which slows application performance, makes debugging harder and increases memory consumption.
Micronaut framework tries to address these concerns by providing a simple compile-time AOP API that does not use reflection.
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:
The next step to defining Around advice is to implement a MethodInterceptor. For example the following interceptor disallows parameters with null values:
|
Note
|
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);
}
}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:
NOTE: Missing tag `test` in `test-suite/src/test/java/io/micronaut/docs/aop/around/AroundSpec.java`.|
Note
|
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 aReentrantReadWriteLockand allows swapping the target instance at runtime. -
lazy(defaults tofalse) - By default the Micronaut framework eagerly initializes the proxy target when the proxy is created. If set totruethe 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();
}
}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();
}
}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.
Introduction advice is distinct from Around advice in that it involves providing an implementation instead of decorating.
Examples of introduction advice includes Spring Data which implements persistence logic for you.
Micronaut Client annotation is another example of introduction advice where the Micronaut framework implements HTTP client interfaces for you at compile time.
The way you implement Introduction advice is very similar to how you implement Around advice.
You start by defining an annotation that powers the introduction advice. As an example, say you want to implement advice to return a stubbed value for every method in an interface (a common requirement in testing frameworks). Consider the following @Stub annotation:
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:
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();
}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());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.
There are cases where you want to introduce a new bean based on the presence of an annotation on a method. An example of this is the @EventListener annotation which produces an implementation of ApplicationEventListener for each annotated method that invokes the annotated method.
For example the following snippet runs the logic contained within the method when the ApplicationContext starts up:
import io.micronaut.context.event.StartupEvent;
import io.micronaut.runtime.event.annotation.EventListener;
...
@EventListener
void onStartup(StartupEvent event) {
// startup logic here
}The presence of the @EventListener annotation causes the Micronaut framework to create a new class that implements ApplicationEventListener and invokes the onStartup method defined in the bean above.
The actual implementation of the @EventListener is trivial; it simply uses the @Adapter annotation to specify which SAM (single abstract method) type it adapts:
|
Note
|
The Micronaut framework also automatically aligns the generic types for the SAM interface if they are specified. |
Using this mechanism you can define custom annotations that use the @Adapter annotation and a SAM interface to automatically implement beans for you at compile time.
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
@PostConstructinvocation -
Interception of a bean’s
@PreDestroyinvocation
The Micronaut framework supports these 3 use cases by allowing the definition of additional @InterceptorBinding meta-annotations.
Consider the following annotation definition:
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:
Now you can define ConstructorInterceptor beans for constructor interception and MethodInterceptor beans for @PostConstruct or @PreDestroy interception.
The following factory defines a ConstructorInterceptor that intercepts construction of Product instances and registers them with a hypothetical ProductService validating the product name first:
Defining 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:
Validation advice is one of the most common advice types you are likely to want to use in your application.
Validation advice is built on Bean Validation JSR 380, a specification of the Java API for bean validation which ensures that the properties of a bean meet specific criteria, using jakarta.validation annotations such as @NotNull, @Min, and @Max.
The Micronaut framework provides native support for the jakarta.validation annotations with the micronaut-validation dependency:
annotationProcessor("io.micronaut.validation:micronaut-validation-processor")implementation("io.micronaut.validation:micronaut-validation")Or full JSR 380 compliance with the micronaut-hibernate-validator dependency:
implementation("io.micronaut.beanvalidation:micronaut-hibernate-validator")See the section on Bean Validation for more information on how to apply validation rules to your bean classes.
Like Spring and Grails, the Micronaut framework provides caching annotations in the io.micronaut.cache package.
The CacheManager interface allows different cache implementations to be plugged in as necessary.
The SyncCache interface provides a synchronous API for caching, whilst the AsyncCache API allows non-blocking operation.
Cache Annotations
The following cache annotations are supported:
-
@Cacheable - Indicates a method is cacheable in the specified cache
-
@CachePut - Indicates that the return value of a method invocation should be cached. Unlike
@Cacheablethe 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:
micronaut.caches.my-cache.maximum-size=20The above example configures a cache called "my-cache" with a maximum size of 20.
|
Note
|
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, the Micronaut framework registers an instance of DefaultDynamicCacheManager that creates Caffeine caches with default values.
Other Cache Implementations
Check the Micronaut Cache project for more information.
In distributed systems and microservice environments, failure is something you have to plan for, and it is common to want to attempt to retry an operation if it fails. If first you don’t succeed try again!
With this in mind, the Micronaut framework includes a Retryable annotation.
Retry Dependency
|
Note
|
Since Micronaut Framework 4.0 to use the Retry functionality you need to add the following dependency: |
implementation("io.micronaut:micronaut-retry")Simple Retry
The simplest form of retry is just to add the @Retryable annotation to a type or method. The default behaviour of @Retryable is to retry three times with a linear delay of one second between each retry. (first attempt with 1s delay, second attempt with 2s delay, third attempt with 3s delay).
For example:
@Retryable
public List<Book> listBooks() {
// ...With the above example if the listBooks() method throws an Exception, it is retried until the maximum number of attempts is reached.
The multiplier value of the @Retryable annotation can be used to configure a multiplier used to calculate the delay between retries, allowing exponential retry support.
To customize retry behaviour, set the attempts and delay members, For example to configure five attempts with a two seconds delay:
@Retryable(attempts = "5",
delay = "2s")
public Book findBook(String title) {
// ...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) {
// ...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() {
// ...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() {
// ...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.
Programmatic Retry
For cases where annotations are not desirable, Micronaut Retry also provides programmatic retry creation through typed policies and injected factory beans.
Create a policy once and derive reusable operations from the injected factory:
RetryPolicy retryPolicy = RetryPolicy.builder()
.maxAttempts(5)
.delay(Duration.ofMillis(5))
.build();
CircuitBreakerPolicy circuitBreakerPolicy = CircuitBreakerPolicy.builder()
.maxAttempts(3)
.delay(Duration.ofMillis(5))
.resetTimeout(Duration.ofMillis(100))
.build();The programmatic API uses typed values such as Duration and int instead of annotation strings.
For synchronous work, execute the supplier directly through RetryOperations:
public List<Book> listBooks() {
return retryOperations.execute(() -> {
if (syncCounter.incrementAndGet() < 3) {
throw new IllegalStateException("Temporary failure");
}
return Collections.singletonList(new Book("The Stand"));
});
}Reactive retry is also supported. Pass a supplier so each retry attempt creates a fresh Publisher:
public Publisher<Book> streamBooks() {
return retryOperations.executePublisher(() -> Flux.defer(() -> {
if (reactiveCounter.incrementAndGet() < 3) {
return Flux.error(new IllegalStateException("Temporary failure"));
}
return Flux.just(new Book("The Stand"));
}));
}For asynchronous work, pass a supplier that creates a new CompletionStage for each attempt:
public CompletionStage<Book> findBook(String title) {
return retryOperations.executeCompletionStage(() -> CompletableFuture.supplyAsync(() -> {
if (asyncCounter.incrementAndGet() < 3) {
throw new IllegalStateException("Temporary failure");
}
return new Book(title);
}));
}Programmatic Circuit Breaker
Programmatic circuit breakers are created in the same way, using a typed CircuitBreakerPolicy and an injected CircuitBreakerOperationsFactory. The resulting CircuitBreakerOperations instance owns the shared breaker state.
public Book findBookWithCircuitBreaker(String title) {
return circuitBreakerOperations.execute(() -> {
if (circuitCounter.incrementAndGet() < 4) {
throw new IllegalStateException("Circuit failure");
}
return new Book(title);
});
}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:
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.
Like Spring and Grails, the Micronaut framework features a Scheduled annotation for scheduling background tasks.
|
Tip
|
See the guide for Schedule Periodic Tasks inside your Micronaut Applications to learn more. |
Using the @Scheduled Annotation
The Scheduled annotation can be added to any method of a bean, and you should set one of the fixedRate, fixedDelay, or cron members. Scheduling requires the Micronaut Context dependency:
implementation("io.micronaut:micronaut-context")micronaut-context is a transitive dependency of micronaut-http. If you use a Micronaut HTTP runtime, your project already includes the Micronaut-context dependency.
|
Note
|
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()");
}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()");
}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()");
}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()");
}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;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()");
}The above example allows the task execution frequency to be configured with the property my.task.rate, and the initial delay to be configured with the property my.task.delay.
Configuring the Scheduled Task Thread Pool
Tasks executed by @Scheduled are run by default on a ScheduledExecutorService configured to have twice the number of threads as available processors.
You can configure this thread pool in your configuration file (e.g application.yml):
micronaut.executors.scheduled.type=scheduled
micronaut.executors.scheduled.core-pool-size=30Handling Exceptions
By default, the Micronaut framework includes a DefaultTaskExceptionHandler bean that implements the TaskExceptionHandler interface and simply logs the exception if an error occurs invoking a scheduled task.
If you have custom requirements you can replace this bean with your own implementation (for example to send an email or shutdown the context to fail fast). To do so, write your own TaskExceptionHandler and annotate it with @Replaces(DefaultTaskExceptionHandler.class).
Although the Micronaut framework’s design is based on a compile-time approach and does not rely on Spring dependency injection, there is still a lot of value in the Spring ecosystem that does not depend directly on the Spring container.
You may wish to use existing Spring projects within the Micronaut framework and configure beans to be used within the Micronaut framework.
You may also wish to leverage existing AOP advice from Spring. One example of this is Spring’s support for declarative transactions with @Transactional.
The Micronaut framework provides support for Spring-based transaction management without requiring Spring itself. Simply add the spring module to your application dependencies:
implementation("io.micronaut.spring:micronaut-spring")This also requires adding the spring-annotation module dependency as an annotation processor:
annotationProcessor("io.micronaut.spring:micronaut-spring-annotation")|
Note
|
If you use Micronaut’s Hibernate support you already get this dependency and a HibernateTransactionManager is configured for you.
|
This is done by intercepting method calls annotated with Spring’s @Transactional with TransactionInterceptor.
The benefit here is you can use Micronaut’s compile-time, reflection-free AOP to declare programmatic Spring transactions. For example: