To main content

Use SnakeYAML in a modular jlink distribution

Published by Benjamin Marwell on

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).