Komt er eindelijk een einde aan de conflicten tussen libraries in Java? Een groot project bevat vaak veel dependencies met verschillende versies van libraries, zoals Apache Commons of log4j. Zelfs met hulpmiddelen (zoals de Maven enforcer plugin) gaat er veel tijd zitten in het op orde houden hiervan. Een gestandaardiseerd modulesysteem voor Java, waarin je verschillende versies van een library kunt opnemen, kan dit oplossen. Een modulair opgezet platform heeft sowieso meer voordelen.
Een module is een eenheid van software, die een bepaalde functionaliteit uitvoert en onafhankelijk en inwisselbaar is. De implementatie van een bepaalde library, zoals logging, zit verborgen achter een interface en kun je zonder conflicten (inwisselbaar en onafhankelijk) naast andere modules gebruiken. Dit verbetert de mogelijkheden voor encapsulatie en polymorfisme en daarmee de kwaliteit en onderhoudbaarheid van softwareprojecten.
Hiervoor is Jigsaw bedacht. Er zijn echter ingrijpende wijzigingen noodzakelijk in het Java-platform. Het is dan ook niet verwonderlijk dat Jigsaw is uitgesteld van JDK8 tot de release van JDK9 in 2016. Jigsaw moet kunnen omgaan met naamconflicten tussen classes en packages. Jigsaw wordt een integraal onderdeel van het Java-platform voor zowel sourcecode als runtime.
Een tweede doelstelling van Jigsaw is om de JDK modulair op te zetten, zodat je alleen de componenten van de JDK hoeft te gebruiken, die nodig zijn voor een project. Bijkomend voordeel is dat je niet de gehele JDK hoeft te laden.
Mark Reinhold (Chief Architect bij Oracle voor het Java-platform) vat de voordelen van Jigsaw als volgt samen: “A standard module system for the Java Platform will ease the construction, maintenance, and distribution of large applications, at last allowing developers to escape the “JAR hell” of the brittle and error-prone class-path mechanism”.
Naast verlossing van de jar- of dependency ‘hell’ zijn er andere voordelen:
- Betere schaalbaarheid voor Java SE en de JDK naar kleine devices;
- Verbeterde security en onderhoudbaarheid van Java SE en de JDK in het bijzonder;
- Verhoogde performance;
- Vereenvoudiging van ontwikkeling en beheer van libraries en grotere Java- en Java EE-applicaties.
Deze verbeterpunten komen overeen met de strategie, die Oracle heeft met het ‘Internet of Things’ en Embedded Java. Java 9 is beter toegerust voor uiteenlopende platforms, zoals wearables en de enterprise. Ook security krijgt terecht meer aandacht. Dat is belangrijk als er meer ‘things’ op Java gaan werken, zoals bijvoorbeeld je auto. Hierover heeft Oracle een interessante whitepaper op haar site gezet.
Hoe ziet een Jigsaw-module eruit?
Een Jigsaw-module bestaat uit een aantal packages met classes en een bestand dat de module definieert: module.info.java. Jigsaw zorgt ervoor dat de module wordt geladen en uitgevoerd.
Om classes die in een module staan te laden, wordt in plaats van het classpath een modulepath gebruikt. Op het modulepath worden modules geplaatst, die het systeem kunnen laden. Op de projectpagina’s van Jigsaw wordt dit omschreven als de ‘best practice’ voor het onderverdelen van sources in modules. De redenering hierachter is als volgt.
De meest voor de hand liggende optie zou zijn om module.info in de root package op het classpath te plaatsen:
src/classes/com/foo/HelloWorld.java
/com/bar/Baz.java
/module-info.java
Dit heeft als nadeel, dat je moeilijk kunt bepalen wat precies de afbakening van een module is en dat het moeilijk wordt om ‘Baz.java’ (in dit voorbeeld) in een andere module te plaatsen.
Een andere mogelijkheid is om de module afhankelijk van het classpath te laden.
src/classes/com/foo/HelloWorld.java
/module-info.java
/com/bar/Baz.java
/module-info.java
Hier zitten echter ook nadelen aan. De modulenaam ‘overloads’ van de package hiërarchie en het is lastig om de afbakening van een module te bepalen. Je kunt er ook voor kiezen om modules in verschillende source directories te zetten.
src/classes1/com/foo/HelloWorld.java
/module-info.java
src/classes2/com/bar/Baz.java
/module-info.java
De classes kunnen worden ingelezen, maar als de compiler de output naar één directory schrijft, dan kan het zijn dat de modules worden overschreven. Dit kun je oplossen door de module.info.class in de output directory bij de classes van de module te plaatsen.
build/classes/com/foo/HelloWorld.class
/module-info.class
/com/bar/Baz.class
/module-info.class
Deze optie is echter niet gewenst, omdat de sources anders zijn opgebouwd dan de classes. De input en de output zijn niet isomorf. In plaats daarvan kun je de sources op een modulepath zetten.
src/modules/com.foo.app/com/foo/HelloWorld.java
/module-info.java
src/modules/com.bar.app/com/bar/Baz.java
/module-info.java
En in een corresponderende output directory zetten.
build/gensrc/com.foo.app/com/foo/…
build/gensrc/com.bar.app/com/bar/…
De voordelen hiervan zijn:
- De compiler kan meerdere modules compileren;
- Classes kunnen verplaatst worden tussen modules;
- De module-content is eenvoudig te bepalen;
- Multi-module packages zijn mogelijk.
Een modulepath kan ook meerdere versies ondersteunen:
src/modules/com.foo.app-1.0/com/foo/HelloWorld.java
/module-info.java
src/modules/com.bar.app-2.0/com/bar/Baz.java
/module-info.java
Voor elke module is er een pad waar het bestand module.info.java en de sources staan. De compiler kan aan de hand van het modulepath modules compileren en builden en de opbouw van de sources consistent houden met de te builden classes in de output directories.
Naast het modulepath is de moduledefinitie interessant om nader te bekijken. Een module-informatiebestand ziet er als volgt uit:
module nl.module.a @ 1.0 {
requires nl.module.b @ >= 2.0 for compilation, reflection;
requires optional nl.module.c @ 1.3;
requires local nl.module.d @ 1.3;
exports nl.module.samples;
permits nl.module.c.Main;
provides nl.module.service @ 2.0;
class nl.module.HelloWorld;
view nl.module.a.internal view {
permits nl.module.b.Main;
}
provides service nl.module.Service with nl.module.internal.ServiceImpl
requires service nl.module.E.external.TestService
}
In het bestand staan de naam van de module, de versie, scope en afhankelijkheden. Met required wordt aangegeven welke andere modules en/of services nodig zijn. Met een versie query: ‘>=@2.0’ wordt aangegeven dat tenminste versie 2.0 nodig is voor de taken die hierachter worden genoemd.
Met optional geef je aan of de module optioneel is. In Java-code kan worden bepaald of de module aanwezig is:
Runtime of het mogelijk is om te bepalen of een required module beschikbaar is.
this.class.getModule().requireModulePresent(“nl.module.c “);
Normaal gesproken is een module zowel compile-time en runtime beschikbaar. In uitzonderlijke gevallen staat de optie scope het toe daarop uitzonderingen te maken. In de scope kun je opgeven op welk niveau de module beschikbaar is: compilation, reflection of execution. Voor het geval een module bijvoorbeeld wel runtime, maar niet compile-time aanwezig hoeft te zijn. Services hebben geen scope. Het zijn bindingen tussen een interface en een implementatie, die zowel runtime als compile-time beschikbaar moeten zijn.
Een module kan local of public zijn. Een local module wordt geladen als een ‘mixin’ met dezelfde classloader. Dat wil zeggen dat de module wordt geladen als onderdeel van de module met dezelfde classloader, die de module gebruikt. De classes van de local module zijn daardoor beschikbaar.
Met permits wordt aangegeven welke andere modules een afhankelijkheid op mogen nemen op deze module. Met het provides keyword wordt een alias voor de module gemaakt of een service ontsloten. Een module kan packages exporteren met het keyword export. Met class geef je aan wat de main-class van de module is.
Een module kan meerdere views beschrijven. In een view wordt een alternatieve beschrijving gegeven van de module in termen van provides, services, exports en een entry class.
Aan de JDK zijn een aantal handige tools toegevoegd zoals ‘jdeps’, waarmee je statische dependencies kunt opvragen.
jdeps | (al beschikbaar in Java 8) een utility, die de afhankelijkheden tussen classes weergeeft. |
jpkg | een utility waarmee je een package van de module kunt maken in een .jmod file. |
jmod | een tool waarmee je module libraries kunt beheren. |
Vanuit module.info.java kan niet worden verwezen naar remote repositories. In een lokale repository kun je wel een verwijzing naar een remote repository opnemen (met behulp van jmod), zodat je externe modules kunt ophalen.
Waarom heeft Oracle niet gekozen voor bestaande oplossingen, zoals OSGI?
Oracle had een bestaande oplossing kunnen kiezen, zoals OSGI. Dat scheelt veel werk en bovendien is het voor veel ontwikkelaars bekende materie. Er zijn echter redenen om dit niet te doen. OSGI is niet geschikt om het JRE/Single Classloader-mechanisme op te lossen, omdat het gebruik maakt van het Java SE-platform. Dit is te zien in de afbeelding met de gelaagde architectuur van OSGI. Boven de onderste laag staat de Java JVM en de bovenliggende lagen (die het modulesysteem van OSGI implementeren) zijn daarvan afhankelijk.
afbeelding 1
Jigsaw lost twee problemen van het classloader-proces op: tightly coupled modules en statische dependencies, die OSGI minder goed oplost. OSGI ondersteunt weliswaar split packages (dezelfde package in verschillende bundles) en meerdere classloaders, maar deze lossen niet de modularisatie van sources op. Ook kan OSGI niet worden gebruikt om de JDK modulair op te zetten. Mark Reinhold zegt over OSGI op de Jigsaw-projectpagina’s het volgende:
“The OSGi module layer is not operative at compile time; it only addresses modularity during packaging, deployment, and execution. As it stands, moreover, it’s useful for library and application modules, but since it’s built strictly on top of the Java SE Platform, it can’t be used to modularize the Platform itself.” (http://mreinhold.org/blog/late-for-the-train-qa)
Voorbereiding in JDK8
Om het pad te effenen voor Jigsaw zijn in JDK8 al de nodige wijzigingen doorgevoerd, die de overgang naar een modulesysteem eenvoudiger maken.
Er zijn diverse uitdagingen te overwinnen om modularisatie te realiseren: de JDK kent cyclische afhankelijkheden, het classloader-mechanisme moet vervangen worden en bestaande applicaties moeten blijven werken.
De modularisatie van de JDK API en implementatie is complex vanwege de vele onderlinge afhankelijkheden. Des te moeilijker is het om het ook backward compatible te realiseren. De voorbereidingen, die zijn getroffen in JDK8, hebben betrekking op class loading, commandline tools, deprecated SE en JDK-libraries en een aantal bestanden in $JAVA_HOME.
De grootste wijziging is de herstructurering van de JDK, want de JDK7 is een monoliet die je niet kunt opdelen in modules.
In afbeelding 2 zijn de afhankelijkheden binnen JDK7 weergegeven. De afbeelding laat de onderlinge verwevenheid van packages zien. De base-module is bijvoorbeeld afhankelijk van Logging, die weer afhankelijk is van JMX, die een afhankelijkheid heeft op JavaBeans, JNDI, RMI en CORBA. JNDI is afhankelijk van java.applet.Applet en JavaBeans van onder andere AWT en Swing. Gebruik van de Logging API introduceert een indirecte afhankelijkheid naar bijna het gehele platform.
afbeelding 2
In JDK8 is dit opgelost door de afhankelijkheden anders te organiseren, waardoor niet de gehele SE nodig is en je genoeg hebt aan de base-module. Ook is het hierdoor mogelijk applicaties te maken met alleen de modules die nodig zijn.
De base-module omvat alleen de core-libraries, zoals java.io, java.net, java.nio en security. Afhankelijkheden op Logging, AWT, JNDI, etcetera, zijn uit de base-module gehaald. Logging heeft niet langer JMX nodig en JNDI niet java.applet.Applet. JavaBeans heeft geen afhankelijkheid meer op JDBC, enzovoort.
afbeelding 3
Een voorbeeld van een bestand in JAVA_HOME dat is aangepast, is de property file voor currencies. In plaats van uit te gaan van een vaste locatie van het bestand, kun je de locatie wijzigen in een toekomstige release, zodat de property-file voor currencies naar een private-module verplaatst kan worden, mocht dat nodig zijn.
Hoe verandert Jigsaw de manier van werken?
Het beheer van externe modules wordt een stuk eenvoudiger, evenals refactoring. Dit omdat de interne werking van modules verborgen is achter een interface en je van een module meerdere versies binnen een JVM kunt gebruiken. Het onderhouden van applicaties verbetert hierdoor ook. De release van een groter systeem is beter te managen, omdat verschillende versies van modules als dependency naast elkaar kunnen bestaan en niet in de gehele applicatie één versie van de module nodig is.
Van JDK9 kan (op moment van schrijven) een early access met Jigsaw worden gedownload. Een modulaire opzet betekent dat je dependencies, versies en interfaces op moduleniveau kunt toevoegen. Hierdoor wordt het mogelijk om meerdere implementaties van een service te maken, die geëncapsuleerd kunnen worden binnen een module.
JigSaw is niet…
Jigsaw kent geen lifecycles en hot-deployment, zoals OSGI. Hiervoor blijft OSGI de aangewezen oplossing. Ook heeft Jigsaw geen dynamische service registry. Jigsaw en OSGI kunnen wel naast elkaar worden gebruikt. Op de webpagina van Project Penrose (http://openjdk.java.net/projects/penrose/) is hier het nodige over te vinden.
Conclusie
Na de introductie van invokedynamic in JDK7, lambda’s en Nashorn in JDK8 is Jigsaw de volgende grote verandering binnen het Java-platform. Een modulair systeem lost één van de grotere knelpunten van Java op en maakt er een nog krachtiger platform van dat klaar is voor the Internet of Things en grotere softwareprojecten. Jigsaw geeft ontwikkelaars middelen om objectgeoriënteerde principes, zoals information-hiding en encapsulatie verdergaand toe te passen en daarmee draagt Jigsaw bij aan meer volwassen software, die beter te onderhouden is.