Java modules en jlink

Vanaf Java 9 is er een modulair systeem geintroduceerd om met packages om te gaan. In feite is een module een groep packages die verband houden met elkaar. Bij de release van java 9 kwam tevens de tool jlink. Jlink is een tool die een aangepaste Java-runtime-image genereert die alleen de platform modules bevat die vereist zijn voor een bepaalde toepassing. Zo’n runtime-image werkt precies zoals de JRE, maar bevat alleen de modules die we hebben gekozen en de afhankelijkheden die ze nodig hebben om te functioneren. Iemand zou dan een applicatie simpel op de computer kunnen uitpakken en draaien zonder hiervoor zelf een jdk te moeten installeren. Omdat de gehele JRE zo klein wordt is dit ook goed voor gebruik voor een container-technologie als Docker of Kubernetes.

Modules hebben een naam die overeenkomt met de folder waarin ze staan, hier binnen krijg je de oude package structuur voor de packages die er gebruikt worden. Binnen de module folder staat een module-info.java bestand welke de module beschrijft en packages exposed aan de buitenwereld. Door het onderstaande commando uit te voeren is te zien welke modules java kent.

java --list-modules
        
java.base@15.0.2
java.compiler@15.0.2
java.datatransfer@15.0.2
java.desktop@15.0.2
java.instrument@15.0.2
java.logging@15.0.2
java.management@15.0.2
java.management.rmi@15.0.2
java.naming@15.0.2
java.net.http@15.0.2
java.prefs@15.0.2
etc…

In volgende stuk gaan we aan de slag met het zelf maken van een tweetal modules, het compileren en uitvoeren er van.

Voorbeeld code modules:

Om te beginnen maken we de volgende folder structuur binnen een zelf gekozen hoofd folder:

\---the-modules
    +---modules.messenger
    |   \---com
    |       \---test
    |           \---modules
    |               \---messenger
    \---modules.user
        \---com
            \---test
                \---modules
                    \---user

Begin eerst met het aanmaken van de DemoModuleMessenger onder folder modules.messenger/com/test/modules/messenger, in eerste instantie mag deze er zo uit zien:

package com.test.modules.messenger;

public class DemoModuleMessenger {
    
    public static void showDemoMessage() {
        System.out.println("Showing DemoModule message");
    }
}

Voeg vervolgens de module-info.java file toe aan de root van de module folder in dit geval modules.messenger:

module modules.messenger {
    exports com.test.modules.messenger;
}

Hiermee zeggen we, deze module heeft de naam modules.messenger en deze exporteerd de package com.test.modules.messenger, dit houd dus in dat ook al zijn er andere packages in deze module dan worden die niet aan de buiten wereld getoond.

We kunnen nu een module maken die gebruik maakt van de klasse DemoModuleMessenger, deze module noemen we nu even modules.user. Onder deze folder modules.user/com/test/modules/user maken we de klasse DemoModuleUser aan:

package com.test.modules.user;

import com.test.modules.messenger.DemoModuleMessenger;

public class DemoModuleUser {
    public static void main(String[] arg) {
        DemoModuleMessenger.showDemoMessage();
    }
}

Vervolgens maken we ook wederom voor deze module een module-info.java bestand aan in de folder modules.user:

module modules.user {
    requires modules.messenger;
}

We hebben nu 2 simpele modules, maar hoe krijgen we deze aan de praat?

Eerst moet alles gecompileerd worden, dat kan door het volgende commando uit te voeren vanuit de root folder:

javac -d outputDir --module-source-path the-modules -m modules.user,modules.messenger

En om de applicatie te starten kan dit commando worden uitgevoerd vanuit de root folder:

java --module-path out -m modules.user/com.test.modules.user.DemoModuleUser

Dit is dan het resultaat:

Showing DemoModule message

Nu gaan we kijken naar het jlink commando. De –module-path is verplicht om aan te geven waar de modules te vinden zijn met –add-modules waarbij de module namen worden aangegeven die in de JRE moeten komen en –output is verplicht om aan te geven waar de JRE moet komen te staan. Met de –launcher optie wordt er een bat file aangemaakt die te vinden is in de bin folder waarmee de applicatie gestart kan worden.

jlink --launcher customjrelauncher=modules.user/com.test.modules.user.DemoModuleUser --module-path the-modules;out --add-modules modules.user,modules.messenger --output customjre

In het voorbeeld hierboven wordt er een customjre folder aangemaakt met daarin alles om de applicatie aan de praat te kunnen krijgen, in dit geval is er onder de folder bin het bat bestand customjrelauncher aangemaakt. Om alles nog net iets kleiner te krijgen kunnen er nog wat parameters toegevoegd worden aan jlink:

--strip-debug
--strip-native-commands (LET wel op deze haalt de java.exe weg, de launcher maakt daar gebruik van)
--compress 2
--no-header-files
--no-man-pages

Bij mij werd dit lokaal 23.6MB wat best netjes is als je bedenkt dat je JDK normaal gesproken iets van 300+ MB is.

Sinds java 16 (geïntroduceerd in java 14) is er nog een andere optie om ook een executable te maken voor de applicatie met het commando jpackage, jpackage maakt op de achtergrond gebruik van jlink, daarom is hetgeen hierboven ook te gebruiken bij het maken van een executable op een later moment. Overigens doen we hier in de voorbeelden alleen maar een system.out.println, dit is niet te zien via een executable, daarvoor kan beter een grotere applicatie worden gemaakt die ook daadwerkelijk een scherm kan tonen aan de gebruiker.

Uitbreiding met ServiceLoader voorbeeld:

Door gebruik te maken van de nieuwe ServiceLoader is het mogelijk om vrij eenvoudig door de geïmplementeerd klasses heen te lopen. Hiermee wordt het mogelijk om in ons voorbeeld de functie uit de interface klasse aan te roepen en de geimplemteerde functies van de verschillende klasses te zien.

Hieronder weer een voorbeeld, we maken om te beginnen een simpele interface klasse zoals deze hieronder:

package com.test.modules.messenger;

public interface DemoInterface {
    void showDemoInterfaceMessage();
}

Hiervoor maken we vervolgens een implementatie klasse die er een berichtje in zet:

package com.test.modules.messenger;

public class DemoInterfaceImpl implements DemoInterface {
    
    @Override
    public void showDemoInterfaceMessage() {
        System.out.println("Showing DemoInterfaceImpl message");
    }
}

En om de interface nogmaals te gebruiken kunnen we de bestaande messenger uitbreiden:

package com.test.modules.messenger;

public class DemoModuleMessenger implements DemoInterface {
public static void showDemoMessage() {
System.out.println("Showing DemoModule message");
}

    @Override
    public void showDemoInterfaceMessage() {
        System.out.println("Showing DemoModule interface message");
    }
}

Aan de bestaande module-info.java voegen we her volgende toe: provides DemoInterface with DemoInterfaceImpl, DemoModuleMessenger wat dit doet is voor andere modules het mogelijk maken om de interface met alle aangegeven implementaties te gebruiken.

import com.test.modules.messenger.DemoInterface;
import com.test.modules.messenger.DemoModuleMessenger;
import com.test.modules.messenger.DemoInterfaceImpl;

module modules.messenger {
    exports com.test.modules.messenger;
    provides DemoInterface with DemoInterfaceImpl, DemoModuleMessenger;
}

In de DemoModuleUser voegen we nu de ServiceLoader toe:

package com.test.modules.user;

import com.test.modules.messenger.DemoInterface;
import com.test.modules.messenger.DemoModuleMessenger;

import java.util.ServiceLoader;

public class DemoModuleUser {
    
    public static void main(String[] arg) {
        DemoModuleMessenger.showDemoMessage();

        Iterable<DemoInterface> services = ServiceLoader.load(DemoInterface.class);
        services.forEach(DemoInterface::showDemoInterfaceMessage);
    }
}

Het enige dat we nog wel moeten aangeven in de module-info.java van de modules.user module is: uses DemoInterface

import com.test.modules.messenger.DemoInterface;

module modules.user {
    requires modules.messenger;
    uses DemoInterface;
}

Zodra we nu opnieuw de commando’s uitvoeren voor het compileren en draaien van de applicatie is dit de uitkomst:

Showing DemoModule message
Showing DemoInterfaceImpl message
Showing DemoModule interface message

Conclusie:

Java modules geeft het gevoel dat er goed is nagedacht over het bundelen van packages. Zelf een module maken met eigen packages is vrij eenvoudig te bereiken. Met jlink is het handig om een kleinere bundel te maken van de java spullen die nodig zijn om de applicatie aan de praat te krijgen zonder dat iemand hiervoor java geinstalleerd hoef te hebben.

André Maassen

André Maassen is sinds 2016 in dienst bij Profit4Cloud als senior java developer. André zit al ruim 15 jaar in het vak en is sinds 2009 een gecertificeerde java developer.