Skip to content

Latest commit

 

History

History
281 lines (241 loc) · 11.2 KB

File metadata and controls

281 lines (241 loc) · 11.2 KB

Micronaut Layered Native Image Demo

This example shows how to build a simple Micronaut REST application using the GraalVM Native Image Layers feature.

Prerequisites

  • SDKMAN! (for installing Micronaut CLI)
  • Linux x64
  • Latest GraalVM 25.1 EA build (with Native Image support)

Native Image Layers is an experimental feature. For the best experience use the latest GraalVM Early Access Build.

Environment Setup

Point your JAVA_HOME to the GraalVM distribution.

export JAVA_HOME=/path/to/graalvm/ea/build

Install mn, the Micronaut CLI tool:

sdk install micronaut 4.9.4
sdk use micronaut 4.9.4

Create the Micronaut Application

Start by generating a basic application using the Micronaut CLI. For more details, see the Micronaut guide.

mn create-app example.micronaut.micronaut-hello-rest-maven-layered --build=maven --lang=java --features=graalvm

This creates a new Micronaut project with the following structure:

micronaut-hello-rest-maven-layered/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── example/micronaut/Application.java
│   │   └── resources/
│   │       └── application.properties
│   └── test/
└── mvnw (Maven wrapper)

For executing the subsequent commands, enter the project directory:

cd micronaut-hello-rest-maven-layered

Add a Custom Controller

Add a custom controller to src/main/java/example/micronaut/HelloController.java:

package example.micronaut;

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

@Controller("/hello")
public class HelloController {
    @Get
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello from GraalVM Native Image!";
    }
}

Standalone Application

First, build a standalone executable for this simple application. For this, extend the pom.xml with a custom profile and configure the native build using the GraalVM Native Image Maven plugin:

<profile>
    <id>standalone</id>
    <build>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <version>${native.plugin.version}</version>
                <configuration>
                    <imageName>standalone-app</imageName>
                    <mainClass>example.micronaut.Application</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</profile>

Using this profile you can now generate the executable:

./mvnw clean package -Dpackaging=native-image -Pstandalone

This will generate an executable file that you can run:

./target/standalone-app
 __  __ _                                  _
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
12:20:53.437 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 6ms. Server Running: http://localhost:8080

Test your custom endpoint:

curl localhost:8080/hello

The expected output is:

Hello from GraalVM Native Image!

Layered Application with Micronaut in the Base layer

Configure the Base Layer

Next, create a base layer that contains both java.base and the Micronaut framework. For this, add a second custom profile:

<profile>
    <id>base-layer</id>
    <build>
    <directory>${project.basedir}/base-layer-target</directory>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <version>${native.plugin.version}</version>
                <configuration>
                    <imageName>libmicronautbaselayer</imageName>
                    <mainClass>.</mainClass>
                    <buildArgs>
                        <buildArg>-H:+UnlockExperimentalVMOptions</buildArg>
                        <buildArg>-H:LayerCreate=base-layer.nil,module=java.base,package=io.micronaut.*,package=io.netty.*,package=jakarta.*,package=com.fasterxml.jackson.*,package=org.slf4j.*,package=reactor.*,package=org.reactivestreams.*</buildArg>
                        <buildArg>-H:ApplicationLayerOnlySingletons=io.micronaut.core.io.service.ServiceScanner$StaticServiceDefinitions</buildArg>
                        <buildArg>-H:ApplicationLayerInitializedClasses=io.micronaut.inject.annotation.AnnotationMetadataSupport</buildArg>
                        <buildArg>-H:ApplicationLayerInitializedClasses=io.micronaut.core.io.service.MicronautMetaServiceLoaderUtils</buildArg>
                        <buildArg>-H:-UnlockExperimentalVMOptions</buildArg>
                    </buildArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
</profile>

The -H:LayerCreate= option is used to specify what should be included in the base layer: java.base, io.micronaut, io.netty and a few more other packages that a Micronaut application usually depends on. For more details, consult the Native Image Layers documentation.

Additionally, two more options are used, specific for layered builds:

  • -H:ApplicationLayerOnlySingletons=, which specifies that a singleton object should be installed in the application layer only,
  • -H:ApplicationLayerInitializedClasses=, which registers a class as being initialized in the app layer.

These are necessary for correctly building the Micronaut framework in a layered set-up.

Now you can build the base layer:

./mvnw clean install -Dpackaging=native-image -Pbase-layer

This will create the base-layer.nil which is a build time dependency for the application build. It will also create the libmicronautbaselayer.so shared library which is a run time dependency for the application layer. Note also that you use install instead of package to ensure that the base layer JAR is installed in the .m2 cache as it will be needed by the application build later.

Configure The Application Layer

To configure the application layer, add an additional profile:

<profile>
    <id>app-layer</id>
    <build>
        <directory>${project.basedir}/app-layer-target</directory>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <version>${native.plugin.version}</version>
                <configuration>
                    <imageName>layered-app</imageName>
                    <mainClass>example.micronaut.Application</mainClass>
                    <buildArgs>
                        <buildArg>-H:+UnlockExperimentalVMOptions</buildArg>
                        <buildArg>-H:LayerUse=base-layer-target/base-layer.nil</buildArg>
                        <buildArg>-H:LinkerRPath=$ORIGIN</buildArg>
                        <buildArg>-H:-UnlockExperimentalVMOptions</buildArg>
                    </buildArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
</profile>

Now you can build a layered native image which depends on the base layer that you created earlier:

./mvnw clean package -Dpackaging=native-image -Papp-layer

This will generate the layered executable file in ./app-layer-target/layered-app and will copy libmicronautbaselayer.so next to it.

Then you can execute the layered application:

./app-layer-target/layered-app
 __  __ _                                  _
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
12:24:21.341 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 6ms. Server Running: http://localhost:8080

Test it with:

curl localhost:8080/hello

The expected output is:

Hello from GraalVM Native Image!

Layered Application with Micronaut in the application layer

Configure the Base Layer

Next, create a base layer that contains java.base. For this, use the following build in the pom.xml file:

<build>
    <plugins>
        <plugin>
            <groupId>org.graalvm.buildtools</groupId>
            <artifactId>native-maven-plugin</artifactId>
            <version>${native.maven.plugin.version}</version>
            <executions>
                <execution>
                    <goals>
                        <goal>compile-no-fork</goal>
                    </goals>
                    <phase>package</phase>
                </execution>
            </executions>
            <configuration>
                <imageName>libjavabaselayer</imageName>
                <classesDirectory>null</classesDirectory>
                <buildArgs>
                    <buildArg>-H:+UnlockExperimentalVMOptions</buildArg>
                    <buildArg>-H:LayerCreate=base-layer.nil,digest-ignore,module=java.base,module=jdk.unsupported,module=java.sql,module=jdk.jfr,package=sun.management.spi</buildArg>
                    <buildArg>-H:+UseSharedLayerGraphs</buildArg>
                    <buildArg>-H:-UnlockExperimentalVMOptions</buildArg>
                </buildArgs>
            </configuration>
        </plugin>
    </plugins>
</build>

The -H:LayerCreate= option is used to specify what should be included in the base layer: java.base and a few more other packages that a Micronaut application usually depends on. For more details, consult the Native Image Layers documentation.

Now you can build the base layer:

../mvnw clean install

This will create the base-layer.nil file which is a build time dependency for the application build. It will also create the libjavabaselayer.so shared library which is a run-time dependency for the application layer. Note also that you use install instead of package to ensure that the base layer JAR is installed in the .m2 cache as it will be needed by the application build later.

The <classesDirectory>null</classesDirectory> configuration excludes the main JAR from the image by specifying a directory that does not exist.

Configure The Application Layer

The application layer configuration is the same as the previous example. The only difference is the shared library and the base-layer.nil file used.

Learn More