13.4.1 Microservices as GraalVM native images
Getting Started with Micronaut and Graal
To get started creating a Microservice that can be compiled into a native image, use the graal-native-image
feature when creating the application with the CLI:
Creating a Graal Native Microservice
$ mn create-app hello-world --features graal-native-image
The graal-native-image
feature adds three important items:
The
svm
andgraal
dependencies to yourbuild.gradle
(orpom.xml
if--build maven
is used).A
Dockerfile
which can be used to construct the native image using Docker and a scriptdocker-build.sh
to run it.A
native-image.properties
configuration filesrc/main/resources/META-INF/native-image
to simplify building the image.
The native-image.properties
file that is generated looks something like:
Args = -H:IncludeResources=logback.xml|application.yml|bootstrap.yml \ (1)
-H:Name=example \ (2)
-H:Class=example.Application (3)
1 | The -H:IncludeResources argument allows you to tweak which static resources to include. |
2 | The -H:Name argument specifies the name of the native image that will be generated. |
3 | The -H:Class argument specifies the entry point of your application (the class that defines a static void main method. |
Building a Native Image Using Docker
To build your native image using Docker simply run:
$ ./gradlew assemble # or ./mvnw package if using Maven
$ docker build . -t hello-world
$ docker run -p 8080:8080 hello-world
Or use the provided script:
$ ./gradlew assemble # or ./mvnw package if using Maven
$ ./docker-build.sh
The provided Dockerfile
is a multi-stage dockerfile which builds the project in two steps:
A GraalVM official image will build the native image.
A typical dockerfile structure will build the final image which is smaller due to layering.
Building a Native Image Without Using Docker
To build your native image without using Docker you need to install GraalVM SDK via the Getting Started instructions or using SDKman:
Installing GraalVM 20.1.0 with SDKman
$ sdk install java 20.1.0.r8-grl # or 20.1.0.r11-grl if you want to use JDK 11
$ sdk use java 20.1.0.r8-grl
The native-image
tool was extracted from the base GraalVM distribution. Currently it is available as an early adopter plugin. To install it, run:
Installing native-image
tool
$ gu install native-image
Creating native image with Gradle
$ ./gradlew assemble
$ native-image --no-server -cp build/libs/hello-world-0.1-all.jar
Creating native image with Maven
$ ./mvnw package
$ native-image --no-server -cp target/hello-world-0.1-all.jar
Run native image
$ ./hello-world
Understanding Micronaut and Graal
Micronaut itself does not rely on reflection or dynamic classloading so works automatically with GraalVM native, however certain third party libraries used by Micronaut may require additional input about uses of reflection.
Micronaut includes an annotation processor that helps to handle generating the reflection-config.json
metadata that is automatically picked up by the native-image
tool:
annotationProcessor("io.micronaut:micronaut-graal")
<annotationProcessorPaths>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-graal</artifactId>
</path>
</annotationProcessorPaths>
This processor will generate a reflection-config.json
file to a file to the META-INF/native-image
directory in your build classes directory (target/classes
with Maven and typically build/classes/java/main
with Gradle) and a native-image.properties
file to read this configuration for all classes annotated with either @Introspected or @TypeHint.
For example the following class:
package example;
import io.micronaut.core.annotation.*;
@Introspected
class Test {
...
}
The above example will result in the public methods and declared constructors of example.Test
being included in reflection-config.json
.
If you have more advanced requirements and only wish to include certain fields or methods, you can use @ReflectiveAccess instead which can be present on any constructor, field or method to include only the specific field, constructor or method.
If you wish to provide your own reflect.json you can add one to src/main/graal/reflect.json and it will be automatically picked up. |
Adding Additional Classes for Reflective Access
To inform Micronaut of additional classes that should be included in the generated reflect.json
file at compilation time you can either annotate a class with @Introspected or @TypeHint.
The former will generate a compile time introspection as well as allowing reflective access and the latter will only allow reflective access and is typically used on a module or Application
class to include classes that are needed reflectively. For example, the following is taken from Micronaut’s Jackson module:
@TypeHint(
value = { (1)
PropertyNamingStrategy.UpperCamelCaseStrategy.class,
ArrayList.class,
LinkedHashMap.class,
HashSet.class
},
accessType = TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS (2)
)
1 | The value member is used to specify which classes require reflection. |
2 | The accessType member specifies if only classloading access is needed or whether full reflection on all public members is needed. |
Generating Native Images
GraalVM’s native-image
command is used to generate native images. You can use this command manually to generate your native image. An example can be seen below.
The native-image
command
native-image --no-server \ (1)
--class-path build/libs/hello-world-0.1-all.jar (2)
1 | Do not start a background server to generate the native image |
2 | The class-path argument is used to refer to the Micronaut shaded JAR |
Once the image has been built you can run the application using the native image name:
Running the Native Application
$ ./hello-world
15:15:15.153 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 14ms. Server Running: http://localhost:8080
As you can see the advantage of having a native image is startup completes in milliseconds and memory consumption does not include the overhead of the JVM (a native Micronaut application runs with just 20mb of memory).