Micronaut AOT

Build time optimizations for Micronaut

Version: 3.0.1-SNAPSHOT

1 Introduction

Unlike other Micronaut modules, Micronaut AOT is not a dependency which needs to be added to your application: it’s a framework which requires integration with your build process: it is typically aimed at being integrated into build tool plugins. If you are looking into documentation about how to configure Micronaut AOT, you should look into the corresponding build tool documentation. Micronaut AOT support is implemented by the Micronaut Gradle plugin and Micronaut Maven plugin.

Micronaut AOT is a framework which implements ahead-of-time (AOT) optimizations for Micronaut application and libraries. Those optimizations consist of computing at build time things which would normally be done at runtime. This includes, but is not limited to:

  • pre-parsing configuration files (yaml, properties, …​)

  • pre-computing bean requirements in order to reduce startup time

  • performing substitution of classes with "optimized" versions for a particular environment

Micronaut AOT can generate specific optimizations for different environments, in particular, it can make a difference between optimizations needed in JIT mode (traditional JVM applications) and native mode (application compiled with GraalVM native-image).

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Quick Start

Applicability

The goal of Micronaut AOT is to create an optimized "binary" for a particular deployment environment. It is not to make development experience faster: because build time optimizations require deep analysis of the application, it will actually make the local development slower if you use optimized binaries.

You should consider Micronaut AOT similarly to what GraalVM’s native-image tool is: a "compiler" which generates a different application, aimed at a particular runtime. That is, depending on the optimizations which are enabled, a binary optimized by AOT may, or may not, work in a specific deployment environment.

As a consequence, it is recommended to build your Micronaut AOT optimized application on the same environment as the deployment one. Please refer to the documentation of your build tool for configuration options.

The following documentation is for users who want to implement their own AOT optimizers.

Micronaut AOT projects

The Micronaut AOT project consists of 4 main modules:

  • micronaut-aot-core provides the APIs for implementing "AOT optimizers", or code generators.

  • micronaut-aot-api exposes the public API for interacting with the AOT compiler. It mostly consists of the MicronautAotOptimizer class which is responsible for loading the different AOT modules via service loading, then driving the AOT process.

  • micronaut-aot-std-optimizers implements a number of standard Micronaut AOT optimizers.

  • micronaut-aot-cli is the command line tool responsible for invoking the AOT compiler. It is recommended to integrate with Micronaut AOT via this CLI to ensure proper classloader isolation.

How it works

Optimization process

Micronaut AOT is a post-processing tool. By that, we mean that it takes the output of regular Micronaut application compilation, then performs an analysis of this and generates new classes, resources, etc. which are then used to create a new binary (jar, native binary, …​).

In a nutshell, the inputs of Micronaut AOT are:

  • the application runtime classpath

  • the Micronaut AOT runtime (including the AOT optimizers)

  • the AOT optimizer configuration (including the target runtime, e.g JIT vs native)

And its outputs are:

  • generated of source files and their compiled (.class) versions

  • generated resource files

  • a list of resources which should be removed from the final binary (for example, if a YAML file is replaced with Java configuration, a class would be generated, but then we know we don’t need the YAML file anymore in the final binary)

  • log files (to diagnose what happened during the AOT process)

The MicronautAotOptimizer class is a special case of code generator which integrates the dynamically loaded AOT optimizers, and generates an ApplicationContextConfigurer which will initialize the optimizations.

It’s then the responsibility of integrators to take those outputs to generate different binaries.

User-code loading

In order to perform optimizations, so called optimizers (or AOT modules) are used. Those modules need access to the application context, so that they can, for example, determine if a bean is going to be needed in a particular deployment environment. Or, they may need access to the configuration which is dynamically loaded from an external source (think of distributed configuration) to generate static configuration instead.

Therefore, the AOT compiler needs to be executed in the same classloader as the application code itself. This is why there are 2 different classpaths for the AOT compiler:

  • the application classpath corresponds to the application runtime classpath. It is, technically speaking, the result of previous compilation plus all transitive dependencies needed by the application (or library).

  • the AOT classpath, which is the classpath of the AOT compiler and the AOT optimizers.

The role of the application context configurers

Given the application classpath, the AOT compiler will instantiate an ApplicationContext, which will have all the context configurers (classes implementing the ApplicationContextConfigurer interface and annotated with @ContextConfigurer) applied automatically. This is how the AOT compiler will "know" about particular application context customizations which can be done by a user.

Therefore, it is critical to understand that the AOT compiler cannot figure out arbitrary application context customizations. For example, in the following code:

class Application {
    public static void main(String...args) {
        Micronaut.build()
            .deduceEnvironment(false)
            .mainClass(Application.class)
            .start();
    }
}

there’s a deduceEnvironment() call which is opaque to the AOT compiler: it cannot know that the application is configured that way (for this it would actually have to start the application and perform runtime interception which would be too expensive or impossible).

Therefore, all customizations need to be done using a different pattern:

class Application {
    @ContextConfigurer
    public static class MyConfigurer implements ApplicationContextConfigurer {
        @Override
        public void configure(ApplicationContextBuilder context) {
            context.deduceEnvironment(false);
        }
    }

    public static void main(String... args) {
        Micronaut.run(Application.class, args);
    }
}

Because @ContextConfigurer makes sure that any application context created will see the customizer applied, the application context created by the AOT compiler for its internal use will see the customizations.

Implementing an AOT optimizer

Current capabilities

Now that we understand how the AOT optimization environment is bootstrapped, we can start implementing an AOT optimizer.

An optimizer can do one or more of the following:

  • generate static initializers which will automatically be loaded thanks to the ApplicationContextConfigurer mechanism

  • generate new source files

  • generate new resource files

  • perform substitutions of one class with another

  • filter out resources

New capabilities will be included as part of AOT development.

Code generators

At the core of AOT optimizations is a code generator. A code generator needs to implement the AOTCodeGenerator interface and be annotated with @AOTModule.

The implementation class should be compiled from the regular Java source root of an AOT optimizer module, for example src/main/java. Register the generator with Java’s ServiceLoader mechanism by adding its fully qualified class name to src/main/resources/META-INF/services/io.micronaut.aot.core.AOTCodeGenerator. Build tool integrations load optimizer modules from the AOT optimizer classpath; for example, Gradle users can add the optimizer artifact or project dependency to the aotPlugins configuration.

The AOTModule annotation is responsible for giving metadata about the code generators, including:

  • an id is used to identify the code generator, and enable/disable it via configuration

  • a number of options (@Option) which are used to describe the parameters that the code generator takes (those are provided via configuration)

  • possibly dependencies to other code generators (for example, some code generators may only work properly if they execute after another one)

  • the target runtimes it applies to

Code generators contribute code via the AOTContext interface, which allows:

  • getting the name of the package of generated classes

  • registering generated code (source files, …​)

  • getting access to the ApplicationContext

  • sharing state

  • getting access to target runtime

For example, a simple code generator which generates a resource file may be declared as:

@AOTModule(
    id = MyResourceGenerator.ID,
    options = {
        @Option(key = "greeter.message", sampleValue = "Hello, world!", description = "The message to write")
    }
)
public class MyResourceGenerator implements AOTCodeGenerator {
    public static final String ID = "my.resource.generator";

    @Override
    public void generate(AOTContext context) {
        context.registerGeneratedResource("/hello.txt", file -> {
            try (PrintWriter writer = new PrintWriter(file)) {
                String message = context.getConfiguration()
                    .mandatoryValue("greeter.message");
                writer.println(message);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }
}

Then in a configuration file, the code generator would be configured this way:

my.resource.generator.enabled=true
greeter.message=Hello, world!
Different code generators may share the same option values: it is legal, but often simply required (for example if there’s a different implementation of a specific optimization based on the target runtime).

4 Standard Optimizers

Available Optimizers

Micronaut AOT includes standard optimizers that can be enabled or disabled in the Micronaut AOT configuration. Each optimizer is identified by its module id. To enable an optimizer, set <module id>.enabled=true.

These properties belong in the Micronaut AOT configuration used by the build plugin, not in the regular Micronaut application configuration files.
These are not configuration properties to add in your regular Micronaut configuration files,but properties to be added to the Micronaut AOT configuration, via your build plugin.Please refer to the appropriate Maven or Gradle plugin for more details.

Module cached.environment

This module is available in JIT and native modes.

Table 1. Configuration Properties for cached.environment

Property

Description

Example value

cached.environment.enabled

Enables the Caches environment property values: environment properties will be deemed immutable after application startup. optimization

true

Module deduce.environment

This module is available in JIT and native modes.

Table 2. Configuration Properties for deduce.environment

Property

Description

Example value

deduce.environment.enabled

Enables the Deduces the environment at build time instead of runtime optimization

true

Module known.missing.types

This module is available in JIT and native modes.

Table 3. Configuration Properties for known.missing.types

Property

Description

Example value

known.missing.types.enabled

Enables the Checks of existence of some types at build time instead of runtime optimization

true

known.missing.types.list

A list of types that the AOT analyzer needs to check for existence (comma separated)

io.reactivex.Observable,​reactor.core.publisher.Flux,​kotlinx.coroutines.flow.Flow,​io.reactivex.rxjava3.core.Flowable,​io.reactivex.rxjava3.core.Observable,​io.reactivex.Single,​reactor.core.publisher.Mono,​io.reactivex.Maybe,​io.reactivex.rxjava3.core.Single,​io.reactivex.rxjava3.core.Maybe,​io.reactivex.Completable,​io.reactivex.rxjava3.core.Completable,​io.methvin.watchservice.MacOSXListeningWatchService,​io.micronaut.core.async.publisher.CompletableFuturePublisher,​io.micronaut.core.async.publisher.Publishers.JustPublisher,​io.micronaut.core.async.subscriber.Completable

Module logback.xml.to.java

This module is available in JIT and native modes.

Table 4. Configuration Properties for logback.xml.to.java

Property

Description

Example value

logback.xml.to.java.enabled

Enables the Replaces logback.xml with a pure Java configuration (Experimental) optimization

true

Module netty.properties

This module is available in JIT and native modes.

Table 5. Configuration Properties for netty.properties

Property

Description

Example value

netty.properties.enabled

Enables the Defines some Netty system properties when starting the application which optimize startup times. optimization

true

netty.machine.id

The machine id used by Netty. By default, generates a random value at runtime. Set it to a fixed MAC address to override, or use the value 'netty' to disable the optimization and get it at runtime.

random

netty.process.id

The process id to use for Netty. Defaults to a random PID at runtime. Set it to a fixed value (not recommended) or use the value 'netty' to disable the optimization and get it at runtime.

random

Module precompute.environment.properties

This module is available in JIT and native modes.

Table 6. Configuration Properties for precompute.environment.properties

Property

Description

Example value

precompute.environment.properties.enabled

Enables the Precomputes Micronaut configuration property keys from the current environment variables optimization

true

Module property-source-loader.generate

This module is available in JIT and native modes.

Table 7. Configuration Properties for property-source-loader.generate

Property

Description

Example value

property-source-loader.generate.enabled

Enables the Converts configuration files supplied by property source loaders to Java configuration optimization

true

property-source-loader.types

The PropertySourceLoader classnames to use for generating property sources

io.micronaut.context.env.PropertiesPropertySourceLoader,​ io.micronaut.context.env.yaml.YamlPropertySourceLoader

property-source-loader.base-order

The base order to use for the generated property sources. Positive value will be added to base order for specific environments

-1073741824

property-source-loader.resource-names

The resource names to generate property sources for. By default, it is 'application,bootstrap'.

application,​bootstrap

property-source-loader.service-loader-exclude

Whether the property source loaders specified by types should be excluded in service loading

true

yaml.to.java.config

Deprecated option to enable the yaml property source generation. Use property-source-loader.types=io.micronaut.context.env.yaml.YamlPropertySourceLoader instead

false

Module scan.reactive.types

This module is available in JIT and native modes.

Table 8. Configuration Properties for scan.reactive.types

Property

Description

Example value

scan.reactive.types.enabled

Enables the Scans reactive types at build time instead of runtime optimization

true

Module sealed.property.source

This module is available in JIT and native modes.

Table 9. Configuration Properties for sealed.property.source

Property

Description

Example value

sealed.property.source.enabled

Enables the Precomputes property sources at build time optimization

true

Module graalvm.config

This module is available in native mode.

Table 10. Configuration Properties for graalvm.config

Property

Description

Example value

graalvm.config.enabled

Enables the Generates GraalVM configuration files required to load the AOT optimizations optimization

true

service.types

The list of service types to be scanned (comma separated)

io.micronaut.Service1,​io.micronaut.Service2

5 Diagnostics Report

Micronaut AOT can write a structured diagnostics report for build tools and CI systems. The report is disabled by default and does not change generated sources, generated resources, or log files unless it is enabled.

Enable the report with the following configuration property:

micronaut.aot.report.enabled=true

When the CLI is used for an AOT run, the same behavior can be enabled with:

micronaut-aot --classpath app.jar --runtime jit --package example.aot --config aot.properties --output build/aot --report

The default output file is:

<output.directory>/reports/micronaut-aot-report.json

Use micronaut.aot.report.output or --report-output to select a different report directory. Micronaut AOT writes fixed file names inside that directory.

The default report format is JSON. The default value is:

micronaut.aot.report.format=json

Set micronaut.aot.report.format=html or micronaut.aot.report.format=json,html to write an HTML report. The CLI accepts the same values with --report-format. The fixed output file names are:

  • micronaut-aot-report.json

  • micronaut-aot-report.html

Unsupported formats fail the AOT run with a configuration error.

The JSON report uses a versioned schema. Version 1 includes:

  • schemaVersion

  • generatedAt

  • micronautVersion

  • runtime

  • packageName

  • activeEnvironments

  • optimizers, including optimizer id, implementation class, description, enabled state, disabled reason, option keys, dependency ids, and supported runtimes

  • generatedSources

  • generatedResources

  • filteredResources

  • excludedServiceImplementations

  • buildTimeInitClasses

  • diagnostics

Example:

{
  "schemaVersion": 1,
  "runtime": "JIT",
  "packageName": "example.aot",
  "optimizers": [
    {
      "id": "netty.properties",
      "enabled": true,
      "options": [
        {
          "key": "netty.machine.id",
          "configured": true,
          "redacted": true
        }
      ]
    }
  ],
  "generatedSources": [
    {
      "className": "example.aot.AOTApplicationContextConfigurer",
      "path": "example/aot/AOTApplicationContextConfigurer.java"
    }
  ],
  "generatedResources": [
    "META-INF/services/io.micronaut.context.ApplicationContextConfigurer"
  ]
}

Configuration values are not written to the report. Optimizer options only show the option key, whether the key was configured, and whether the value was redacted. This prevents common secrets such as passwords, tokens, credentials, and datasource values from being copied into diagnostics artifacts by default.

Build-tool integrations can enable the report property for an AOT invocation and publish the printed report path or the JSON file as a build artifact. When HTML is enabled, integrations can also publish micronaut-aot-report.html as a human-readable build artifact. Existing log files remain available for detailed diagnostics.

6 Repository

You can find the source code of this project in this repository: