Self contained Applications with Java 9
The fact that Java 9 has been released already half a year ago and reading this article about self contained applications convinced me to try out Java modules myself. The plan was to take an existing project and turn it into a directly executable Java application. I choose my swiss-wowbagger and here’s what happened then.
And here we go:
- Modularize artifacts under my control
- Modularize third party dependencies
- Using jlink to produce an executable file
- Not using libraries that do not work
- Cross compiling
- Conclusion
Modularize artifacts under my control
The first step was to modularize all artifacts that are under my control. This is basically done by adding a module-info.java
file to every module defining its dependencies and exports. This is not too hard, just a few points to keep in mind:
- I did not find an “official” documentation of
module-info.java
so I used several sources e.g. this. requires transitive
does not mean that a module requires another one and also the dependencies thereof. It means that consumers of the module also see this dependency.- If a module wants to access a resource of another module using the classloader, the module containing the resource has to enable this using the
opens
clause. - Use
Thread.currentThread().getContextClassLoader()
to load such resources.getClass().getClassLoader()
does not work any more.
As Java has a slow pace, I wanted to provide backwards compatibility for Java 8 users. So the jar files should contain module-info.class
but the rest of the class files should be compiled for Java 8.
My solution is to put module-info.java
into a separate directory src/main/java9
and compile this separately. The following maven profile does the trick.
<profile>
<id>java9</id>
<activation>
<file>
<exists>src/main/java9</exists>
</file>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler.version}</version>
<executions>
<execution>
<id>java9-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<compileSourceRoots>
<compileSourceRoot>src/main/java9</compileSourceRoot>
</compileSourceRoots>
<source>1.9</source>
<target>1.9</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
Modularize third party dependencies
Real world applications have dependencies to other libraries. Unfortunately, very few of them are already supporting the JPMS (Java Platform Module System). In my case, I wanted to use the jetty HTTP server which has no support for it.
To add JPMS support to a given jar file, I see no other way than:
- Unpack the jar.
- Write a
module-info.java
. - Compile it.
- Repack the original files plus
module-info.class
into a new jar.
Fortunately, there is the ModiTect maven-plugin helping with this. It can generate and add module-info.java
to existing jar files. It’s not perfect (Version 1.0.0.Alpha2) as the generated module definitions have to be adjusted sometimes, but it helps a lot in the process.
The configuration for one dependency looks like this (real projects can have dozens of dependencies!):
<plugin>
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<version>1.0.0.Alpha2</version>
<executions>
<execution>
<id>generate-module-infos</id>
<phase>generate-resources</phase>
<goals>
<goal>generate-module-info</goal>
</goals>
<configuration>
<outputDirectory>target/generated-module-infos</outputDirectory>
<modules>
<module>
<artifact>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</artifact>
</module>
</modules>
</configuration>
</execution>
<execution>
<id>add-module-infos</id>
<phase>generate-resources</phase>
<goals>
<goal>add-module-info</goal>
</goals>
<configuration>
<outputDirectory>target/modules</outputDirectory>
<overwriteExistingFiles>true</overwriteExistingFiles>
<modules>
<module>
<artifact>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</artifact>
<moduleInfoFile>
target/generated-module-infos/kotlin.stdlib/module-info.java
</moduleInfoFile>
</module>
</modules>
</configuration>
</execution>
</executions>
</plugin>
With this, I was able to add module definitions to all artifacts/jar files of the project.
Using jlink to produce an executable file
When a project is fully modularized, the jlink
tool can be used to pack it together with just the necessary parts of the JDK into a self contained executable file, feeling just like a “native” application.
There is the jlink maven-plugin which is supposed to help with this task, but all I was able to get from it was NullPointerException
s.
But jlink
can also be used directly, it just needs to find the modules to pack:
target/modules
contains the enhanced third party modules generated in the previous step.target/dependency
contains all dependencies and the main module.jlink
is clever enough to only pick up the JPMS dependencies and to ignore the others.
To populate the later one, this maven config is used:
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>copy-dependencies</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<includeScope>compile</includeScope>
<excludeTransitive>false</excludeTransitive>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>prepare jlink</id>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<copy file="target/${project.build.finalName}.jar"
todir="target/dependency" />
</target>
</configuration>
</execution>
</executions>
</plugin>
Then, I ran jlink
by
$JAVA_HOME/bin/jlink
--module-path target/modules:target/dependency:$JAVA_HOME/jmods
--add-modules swiss.wowbagger.server
--launcher wowbagger=swiss.wowbagger.server/guru.nidi.wowbagger.server.WowbaggerServer
--output target/jlinked
and got a lot of error messages.
After a rather not short time of tweaking module definitions, jlink
was happy and it produced some output. I run it and got more error messages.
After more module tweaking, I was finally able to run the executable and I saw that jetty was listening to the specified port. Yay!
But whatever I did, requests to this port were not responded. Never. I suspect that the module definitions are still not 100% correct and that this causes something in the jetty internals to go wrong. Bummer!
Not using libraries that do not work
The project’s requirements for the HTTP server are not very high, so it would have been possible to kick out jetty and just write my own little server from scratch. But for a long time, the JDK itself contains a HTTP server: com.sun.net.httpserver.HttpServer
. This is evil because it sits in a com.sun
package which is not supported and can change at any time.
But as the JDK is packed together with the application, JDK changes on a client machine do not affect the application in any way. Problem solved.
And sure enough, with this change, everything worked fine! A java application that feels like a native one!
Cross compiling
Having a “native” application is nice and has some advantages. But it has the disadvantage that it is only runnable on the operating system it was compiled for.
But it seems that cross compiling java “native” applications is no problem. Just download the JDK for your target OS and reference its jmods
directory: jlink --module-path ...:$TARGET_OS_JAVA_HOME/jmods
. At least this worked for me on MacOS to create a linux executable.
Conclusion
Java 9 Modules (JPMS) is definitely an interesting topic and the possibility to create independently executable applications too. Now, half a year after its inception, there are still very few projects supporting it. It remains to be seen if it will get widespread support or if it will stay mainly a means to just modularize the JDK itself.
I was able to produce a (semi-real world) standalone Java application. It’s up and running, powering this insulting web site. But as long as most libraries do not support JPMS, I would definitely not recommend it for real productive systems.