Whenever you pull in SnakeYAML (either directly or via Jackson), you will break your modulear builds. The reason: SnakeYAML is a named automatic module. But then, automatic modules cannot be used in jlink
images.
But this can be healed. You can rescue your builds using the moditect-maven-plugin
. It is a little hard to use, as the documentation is very technical. It also has few examples, and the documentation does not explain when to use which goal, and how to proceed. So, if you want to see a simple example, read on! 🙂
Project example
The example project is extremely simple. Just print the contents of a .yaml
file which has been read before. As the library we use SnakeYAML directly. If you want to have another example where I use SnakeYAML indirectly via Jackson, see the links at the end of the article.
Module descriptor, yaml file and Main class
To keep things simple, here are the only three source files from the source folder:
Compiling using the maven-jlink-plugin
Another relevant thing is to add the maven-jlink-plugin
to the list of executing plugins. While you can specify a qualifier in upcoming versions of the plugin, this is currently not possible. So I set the packaging to jlink
and used its default execution. Letºs inspect what would not work in a vanilla setup (and why):
<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jlink-plugin</artifactId>
<version>3.0.0</version>
<extensions>true</extensions>
<!-- the default execution is default-jlink -->
<configuration>
<launcher>start=io.github.bmarwell.snakeyaml.moditect.showcase/io.github.bmarwell.snakeyaml.moditect.showcase.Main</launcher>
<addOptions>
<addOption>-Xmx32m</addOption>
</addOptions>
<verbose>true</verbose>
</configuration>
</plugin>
</plugins>
</build>
</project>
If you execute mvn verify
, the jlink binary (or ToolProvider since 9+ if you are not using Toolchains) will complain about the automatic module "org.yaml.snakeyaml". It cannot strip down the JDK without known which modules SnakeYAML requires. So lets fix that using moditect.
Using moditect to generate a modified dependency
Moditect can do exactly this: Inject a module-info.class
(Module descriptor) into an existing dependency. It will be placed somewhere in the target
folder. Of course, it won’t do much just lying there. But we will fix that later.
For now, this is how we create a modularized copy of SnakeYAML:
<project>
<build>
<plugins>
<plugin>
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<version>1.0.0.RC1</version>
<executions>
<execution>
<id>add-module-info-to-dependencies</id>
<phase>generate-resources</phase>
<goals>
<goal>add-module-info</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/modules</outputDirectory>
<overwriteExistingFiles>true</overwriteExistingFiles>
<modules>
<module>
<artifact>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${dependency.snakeyaml.version}</version>
</artifact>
<moduleInfo>
<name>org.yaml.snakeyaml</name>
<requires>
java.logging;
transitive java.desktop;
</requires>
<exports>
org.yaml.snakeyaml;
org.yaml.snakeyaml.composer;
org.yaml.snakeyaml.constructor;
org.yaml.snakeyaml.emitter;
org.yaml.snakeyaml.env;
org.yaml.snakeyaml.error;
org.yaml.snakeyaml.events;
org.yaml.snakeyaml.extensions.compactnotation;
org.yaml.snakeyaml.external.biz.base64Coder;
org.yaml.snakeyaml.external.com.google.gdata.util.common.base;
org.yaml.snakeyaml.introspector;
org.yaml.snakeyaml.nodes;
org.yaml.snakeyaml.parser;
org.yaml.snakeyaml.reader;
org.yaml.snakeyaml.representer;
org.yaml.snakeyaml.resolver;
org.yaml.snakeyaml.scanner;
org.yaml.snakeyaml.serializer;
org.yaml.snakeyaml.tokens;
org.yaml.snakeyaml.util;
</exports>
</moduleInfo>
</module>
</modules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
That is quite a lot of code to write. In this case, we created a java module descriptor in XML, so the moditect-maven-module
can read it.
Note that we also changed the output folder to target/modules
– that is just convenience.
The phase I am using here is generate-resources
so you can test the outcome using mvn generate-sources
. But for the jlink package, it can be as late as the prepare-package
phase. In fact, I updated the repository to this phase. But for testing, this is really convenient.
Telling jlink about the updated module
If you came here and already tried to execute mvn verify
, then you will have seen jlink
still complaining about the automatic SnakeYAML module. Let’s fix that!
<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jlink-plugin</artifactId>
<version>3.0.0</version>
<extensions>true</extensions>
<!-- the default execution is default-jlink -->
<configuration>
<modulePaths>
<modulePath>${project.build.directory}/modules</modulePath>
</modulePaths>
<launcher>start=io.github.bmarwell.snakeyaml.moditect.showcase/io.github.bmarwell.snakeyaml.moditect.showcase.Main</launcher>
<addOptions>
<addOption>-Xmx32m</addOption>
</addOptions>
<verbose>true</verbose>
</configuration>
</plugin>
</plugins>
</build>
</project>
Lines 11 to 13 have been added to tell jlink about the additional module paths. They will be directly appended to the jlink[.exe]
call. JLink will be smart enough to see the duplicate dependency and choose the one with the module descriptor we just created.
Wrapping it up
See the full project here: https://github.com/bmarwell/snakeyaml-moditect-showcase.
You can now do the following:
# compile and package
$ mvn clean verify
# execute the unpacked image
$ ./target/maven-jlink/bin/start
id: [abc].
name: [😄 Γεια σας].
# here is the created image
$ ls -lhF ./target/*.zip
-rw-rw-r-- 1 user user 34M Dez 8 15:29 ./target/snakeyaml-moditect-showcase-1.0.0-SNAPSHOT.zip
Other resources
MemeforceHunt – the application where I use this technique to package a JavaFX app via the maven-jlink-plugin
. It uses SnakeYAML via Jackson (see intro).