(J)Link your Java application before putting it into Docker! 1/3
Java Platform Module System (JPMS) is out since few months now, but does not have a lot of adoption yet. Main reasons are linked to the fact that it does not integrate that smoothly with Java ecosystem yet:
- It does not resolve dependencies (compared to Gradle or Maven) but requires to manually define them in another descriptor,
- It requires your whole stack to be module friendly (to have the JPMS descriptor). Otherwise, it is not usable to link your application (which is quite impacting if you use any library),
- It prevents some old practise, like having overlapping packages between modules (all libraries and code haven't migrate yet),
- It does not support any versioning (like OSGi),
- It is not production/test code friendly as your test code will have impacts on your JPMS descriptor with current state of the libraries (including JUnit 5),
- You lose the portability of Java as you become partially native,
- You couple the JVM and your code together, which can break support contract companies can pay for the JVM itself,
- The bundled binary includes part of the JVM and thus, must comply to the GPL2+CE license...which is something your company/project can work with or not.
All these drawbacks don't help. However, if you're developping a final application and/or delivering it as a Docker image, then you can be interested in decreasing the size of your deliverable a lot by linking your application. To give you an idea, an application depending on java.base and java.logging modules will be less than 42M once linked and put in a functional Docker. It is really less than the same version with a JRE base image. As a reference, OpenJDK Alpine image for JRE 8 - which is already way lighter than other images - is 85M. So it is half of it. For big application, it can not be that impacting. But for utilities and tools it is really nice, and without having to hack the base image to ensure it is minimal!
This post (part 1/3) opens a serie of three posts (coming soon) enabling you to get a custom minimal Java (11) ditribution in a Docker/OCI image ready to deploy. It does not aim to fully cover how JLink works in details, all you can do with it, or even the pitfalls you can meet if you build a library having to run on Java 8 -> 12. This only aims to introduce JPMS, to let you get the minimal knowledge about what it is used for, and let you create a small application to follow the next two blog posts that will enable you to link your application to create a custom Java distribution (part 2/3) and to package it into docker (part 3/3)....even without Docker daemon.
Getting started with JLink
JLink is the new JVM tool that JPMS brings. If you added your java bin folder to your PATH then you can access it by typing jlink. In the next two blogposts, we will mainly want to keep an industrialized build - using Apache Maven - so I will not detail the direct command option, but I recommend you to have a quick look to understand what you can do - and maybe what you would lack since some options you could expect are not yet available.
High level, jlink will go through the modules you have in your module path (a bit like the classpath but for modules) and build the full application graph to then create a JVM distribution with all these modules.
To ensure it works, make sure to describe this dependency graph. This is done through a new descriptor called module-info.java and located at the root of you module. In other words, we will place it in src/main/java/module-info.java.
The minimal content is the definition of the module name. By convention it is better to use the package name and a name linked to the module. But the strong requirement is to match java identifier rules (you can check out jdk.internal.module.Checks#requireModuleName for the implementation detail).
A module can define:
- Its name as we saw,
- Its version: normally you wouldn't use it yet, but it is accessible in ASM ModuleVisitor API and ModuleDescriptor JVM API. You can use it to validate some compatibility of your modules with others at runtime.
- Its modifiers: was the module explicit (what you should do), implicit (for compatibility with not yet JPMS libraries), synthetic (generated) or mandated (implicit),
- Its requirements: defines the required modules (dependencies),
- Its exports: defines which packages are exported and optionally how (you can limit the export to some other packages)
- What it opens: the packages for which the reflection can be used,
- What it uses: a service (as Service Provider Interface - SPI) dependency,
- What it provides: a service (SPI) declaration. This replaces the old META-INF/services mecanism,
- Its main class if any (note: in general, it can't be set in module-info.java directly).
In other words, you can define all your application dependencies through this module-info file but the minimal flavor will look like:
module com.github.rmannibucau.demo {
}
Side note: that java.base is implicitly required as it is the core of the JVM, and you can't write anything without it.
If you want to depend on Java Util Logging (JUL) you can require this module as well:
module com.github.rmannibucau.demo {
requires java.logging;
}
If you want to let other code do reflection on your classes, you can open your packages - testing frameworks will need it:
module com.github.rmannibucau.demo {
requires java.logging;
opens com.github.rmannibucau.demo.model;
opens com.github.rmannibucau.demo.service;
opens com.github.rmannibucau.demo.front;
}
And so on...
Nothing more to get a minimal JLink friendly module!
With this minimal descriptor added to any Java project, you can build a JPMS module and test it - I used Junit 5 to test that code but any other testing framework should have the same requirement.
Indeed, there are finer notions in JPMS but with these ones you are able to get started to build a native jimage!
Next blogpost (part 2/3) will take a module built with this last module-info containing a main class and thanks to Maven, will build a native Java Image (custom Java distribution) we will then put into Docker in our third and last part of this serie. So stay tuned ...
From the same author:
In the same category: