$ mvn clean package -DskipTests
native-image-agent
when porting a library to QuarkusI pondered a bit how GraalVM’s native-image-agent
can be leveraged when writing Quarkus extensions.
Even if the configuration produced by it wonβt be 100% complete and relevant, it may give you a good initial impression
where to look and which kinds of problems you need to solve.
Native compilation of Java using GraalVM’s native-image
tool typically requires a lot of
configuration.
You need to list classes/methods/fields that require reflection, you need to register proxies,
you need to declare which classes have to be initialized at runtime instead of build time, etc.
If you are on Quarkus, you usually do not need to care for these nitty-gritty details
because Quarkus does it for you under the hood and mvn package -Dnative
is all you need to know.
But say, that you are not so lucky to have a ready to use Quarkus extension for some library and you want to write one. Folks who are not able or not wanting to use Quarkus are actually in quite a similar situation.
native-image
So how can the native-image
configuration be put together? Generally speaking there are three options:
You are a GraalVM guru and you know your complete class path so well
that you can write your native-image
configuration from top of your head.
Well, this is probably not your case, because you are reading this blog post π
You first try to compile your application with no configuration at all. If the compilation fails, you try to understand the error message and fix what’s necessary. If the message does not make any sense to you, you paste it to google and you hope to find a solution.
Once the compilation succeeds, you start testing the native executable.
It may fail with some ClassNotFoundException
if you forgot to register some class for reflection
or with a NullPointerException
if you forgot to embed some resources to the native image.
It may also fail with some cryptic exception that you have no clue about.
You google again and you either find a solution that works for you
or you see only reports of frustration similar to your own.
native-image-agent
The official recommendation of GraalVM is to use their native-image-agent
.
You are supposed to put your application (with the agent attached) under a load
that hits all execution paths that will be active also in production.
Based on the runtime data, the agent outputs a configuration in a format acceptable for native-image
.
This sounds very promising, but of course the main caveat is that if you are unable to simulate the production load properly during the test runs, then the produced configuration will not be complete. Your native application may thus fail in production.
Besides that, the agent records only the usage of some features (namely JNI, Reflection, Proxies and class path resources) while it does not help with configuring others, like delayed class initialization. So you still may have to assemble some parts of the configuration manually.
native-image
vs. Quarkusnative-image-agent
was designed for use on plain GraalVM, without Quarkus.
How can we benefit from it when writing Quarkus extensions?
The main idea here is to attach it to the JVM running the application against which we run integration tests.
When porting a library or framework to Quarkus, I always start with writing some tests.
Ideally, they should be integration tests.
It means that there should be a real Quarkus application in src/main/java
and the tests should communicate with it only via network protocols, like HTTP.
The tests should cover all important use cases of the ported library.
Check Quarkus testing guide to learn how to write integration tests for Quarkus applications. |
If I am extremely lucky, the tests just pass in native mode and the library can be considered working in native without any native image configuration.
But that’s very rare. The tests usually pass in JVM mode and they fail in native mode
because some piece of native-image
configuration is missing.
native-image-agent
attachedIf you do not know the internals of the ported library well, native-image-agent
may come in very handy.
Even if the configuration produced by it won’t be 100% complete and relevat,
it may give you a good initial impression where to look and which kinds of problems you need to solve.
How can you attach native-image-agent
to your tests?
Let me explain first how
Quarkus JVM tests
annotated with @QuarkusTest
work.
When running the tests from Maven via mvn test
there are two JVMs taking part in the game:
The main Maven JVM
A separate JVM started by surefire to run the tests
Should native-image-agent
be attached to the second one?
While it is certainly the better of the two options, it is still not perfect.
The problem is that this JVM is used not only to run the application under test (living under src/main
),
but also the test code living under src/test
.
Clearly, we do not intend to compile the tests to native. We only want to compile the application.
So, how can we split the second JVM into a test JVM and an application JVM?
I have found the following hammer-and-nails procedure (please let me know if you know a better one):
Remove the @QuarkusTest
annotation from all your test classes.
Why? Because otherwise Quarkus JUnit extension would start the application in the test JVM
and that’s exactly what we’d like to avoid.
Package your application skipping the tests
$ mvn clean package -DskipTests
Start your application manually using GraalVM JRE (in which native-image-agent
is available)
with the agent attached and test
Quarkus profile activated:
$ export JAVA_HOME=/path/to/graalvm-ce-java11-...
$ $JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=target/graal-config" -Dquarkus.profile=test -jar target/*-runner.jar
...
2021-01-31 15:58:31,675 INFO [io.quarkus] (main) Profile test activated.
2021-01-31 15:58:31,676 INFO [io.quarkus] (main) Installed features: [..., cdi, resteasy]
Run the tests in another terminal:
$ mvn test # or alternatively mvn surefire:test might suffice too
...
[INFO] BUILD SUCCESS
When all the tests have passed, terminate the application running in the first terminal by pressing CTRL+C
.
After that, the configuration for native-image
should be stored in target/graal-config
:
$ tree target/graal-config
target/graal-config
βββ proxy-config.json
βββ reflect-config.json
βββ resource-config.json
βββ ...
Oliver Weiler proposed another approach:
You can use the Surefire JVM, but ignore all test-related classes by supplying an
access-filter.json
file to thenative-image-agent
. See Access Filters docs.
BuildItems
Once the native-image-agent
has generated the configuration, you can study the content of the files
and consider whether and how you need to translate the individual items to Quarkus BuildItems
.
Check Quarkus for extension authors to learn Quarkus extensions basics. |
You typically do not need to care for all configuration items
because some of them are already covered by some core Quarkus extension,
such as quarkus-netty
, quarkus-jackson
, quarkus-vertx
, etc.
For the ones that are apparently related to the library you are porting, the mapping goes like the following:
proxy-config.json
β NativeImageProxyDefinitionBuildItem
reflect-config.json
β ReflectiveClassBuildItem
resource-config.json
β NativeImageResourceBuildItem
or NativeImageResourceBundleBuildItem
Check Quarkus |
Example: If you have something like the following in your reflect-config.json
[
{
"name":"com.azure.core.util.DateTimeRfc1123",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true
},
{
"name":"com.azure.storage.blob.implementation.models.BlobsGetPropertiesResponse",
"allDeclaredConstructors":true
}
]
this is how you would translate it to a Quarkus @BuildStep
:
class AzureBuildSteps {
@BuildStep
void reflectiveClasses(BuildProducer<ReflectiveClassBuildItem> reflectiveClasses) {
reflectiveClasses.produce(
new ReflectiveClassBuildItem(
true, // allDeclaredMethods required
true, // allDeclaredFields required
"com.azure.core.util.DateTimeRfc1123"));
reflectiveClasses.produce(
new ReflectiveClassBuildItem(
false, // allDeclaredMethods not required
false, // allDeclaredFields not required
"com.azure.storage.blob.implementation.models.BlobsGetPropertiesResponse"));
}
}
Not necessarily.
As I have mentioned above, native-image-agent
does not handle all aspects of the configuration.
Running the tests in native mode may reveal more issues that may require additional BuildItem
s
or even GraalVM substitutions.
That’s already beyond the scope of this blog post where I primarily wanted to show
how native-image-agent
can be leveraged when writing Quarkus extensions.
Thanks for reading, stay tuned for more posts about Quarkus, GraalVM, Apache Camel and mvnd
!