Tapasztalatok a Java Platform Module Systemmel
Nagy rajongója vagyok a modularizálás témakörének, mint ezt több korábbi posztban is említettem (Modularizáció Servlet 3, Spring és Maven környezetben, Java Application Architecture). A Java Application Architecture könyv szerint a modularizálás első eszköze a csomagok, azonban ezek nem adnak megfelelő felügyeletet a láthatóság felett, hiszen nem lehet definiálni egy csomag láthatóságát, így azt sem lehet megmondani, hogy egy csomagban lévő osztályok, interfészek, stb. mely más csomagban található osztályok, interfészek, stb. számára legyenek láthatóak. A következő szint a JAR állományok, azonban az alapvető elvárásokat ez sem teljesítette, mint pl. az előbb említett láthatóságokkal kapcsolatos igényeket. Erre egy kezdeti megoldást a build eszköz (pl. Maven) adott, multi-module projektek használatával. Azonban itt sem lehetett definiálni, hogy egy modulon belül mi látható, és mi nem. Kizárólag modulokat lehetett definiálni, és ezek közötti függőségeket. A másik megoldás a Java részét nem képező OSGi jelentette, azonban ennek nehézkessége elég sokakat eltántorított.
A Java 9-ben jelent meg egy megoldás a fentebb vázolt problémákra, Java Platform Module System néven. Elég régóta húzódik ennek kiadása, egészen a Java 7-től kezdve ígérgették, akkor még Jigsaw néven. Figyelembe kell venni azonban azt is, hogy nem csak a lehetőségét adták meg a modularizációnak, hanem a teljes JDK-t is modularizálták. Már nem egy rt.jar
állományban van a teljes osztálykönyvtár, hanem a jmods
könytárban van majdnem száz jmod
kiterjesztésű állomány, mely a modulokat tartalmazza.
Ebben a posztban egy példa alkalmazás implementációja közben szerzett tapasztalatokat szeretném megosztani. A projekt elérhető a GitHubon.
Az alkalmazás könyvjelzők kezeléséért felelős, RESTful API-val rendelkező webes alkalmazás. Alapvetően két modulból áll, ebből egyik a backend
modul, mely a Bookmark
osztályt, valamint a példányainak kezeléséért felelős BookmarkService
interfészt deklarálja. Itt szerepel még ennek egy implementációja is BookmarkServiceImpl
néven, mely egy szinkronizált statikus kollekcióban tartja nyilván az elemeket. A frontend
modul Jersey alkalmazás, ami a Java SE beépített HTTP szerverét használja, és egy /bookmarks
erőforráson lehet JSON-ben a könyvjelzőket letölteni, valamint új könyvjelzőt postolni.
Java 8, de későbbi JDK-k esetén is felépíthető klasszikus multi-module Maven projektként, ahol függőség csak a frontend
modulról a backend
modulra van. Ezt szerettem volna úgy továbbfejleszteni, hogy megfeleljen a Java Platform Module Systemnek.
Ehhez mindkét modulban egy module-info.java
állományt kellett definiálni. A backend
modulban csak deklarálni kellett a modult a következőképp:
module backend {
exports jtechlog.backend to frontend;
}
Ez azt mondja meg, hogy a frontend
modul számára a backend
modul tegye láthatóvá a jtechlog.backend
csomag tartalmát. Itt van egy kis ellentmondás, hogy a backend
modul tud a frontend
modulról, de ez csak szigorítás, írhatnánk az exports
részt to
nélkül is. A modul többi csomagja rejtett marad a külvilág elől.
A frontend
modulnak csak annyit kell definiálnia, hogy használja a backend
modult. Ennek formája a következő:
module frontend {
requires backend;
}
Ezzel elméletileg működőképes is az alkalmazás, adott két modul, a frontend
modul használja a backend
modult, de számára abból csak a jtechlog.backend
csomag látszik, a többi nem hozzáférhető. Már ezzel elég sok architektúrális hibát ki lehet védeni. De látni fogjuk, hogy még sokat kellett dolgozni, hogy a projekt működőképessé váljon, miután megjelentek a module-info.java
állományok.
Amiről még szót szeretnék ejteni, az a Java SE service provider megoldása. Ez azt jelenti, hogy definiálunk egy interfészt, és a moduljainkban közreadhatjuk ezek implementációit. Az implementációra nem is kell rálátnia az azt használó modulnak, elég az interfészre. Az implementációkhoz utána a ServiceLoader
osztállyal tudunk később hozzáférni. Ez a mechanizmus tökéletesen alkalmas arra, hogy pl. létezik valamilyen szabványunk, valamilyen interfész gyűjteményünk, és ezen valamely implementációját szeretnénk használni. Elvárt igény, hogy az implementáció cserélhető legyen, lehetőleg egy függőség kicserélésével. (Ez valójában a Bridge tervezési minta.) Jó példa erre pl. a JDBC, az összes XML API, pl. JAXP, StAX, JAXB, stb. Ezek szabványok, és különböző implementációik léteznek.
Ennek demonstrálására készült a BookmarkService
és ennek implementációja, a BookmarkServiceImpl
. Azonban azért, hogy az interfész látható legyen a frontend
modul számára, az implementáció viszont ne, ez utóbbit elmozgattam a jtechlog.backend.impl
csomagba. A module-info.java
állományt a következőképp kellett módosítani:
module backend {
exports jtechlog.backend to frontend;
provides jtechlog.backend.BookmarkService with jtechlog.backend.impl.BookmarkServiceImpl;
}
Ez mondja meg, hogy a modul a BookmarkService
interfésznek ad egy implementációt a BookmarkServiceImpl
osztállyal.
Nézzük, hogyan kell ezt a frontend
modulból használni. A module-info.java
állományt a következőképp kellett módosítani:
module frontend {
requires backend;
uses jtechlog.backend.BookmarkService;
}
Ez mondja, hogy a modulnak szüksége van egy BookmarkService
implementációra.
Hozzáférni a ServiceLoader
osztállyal lehet. Mivel egyszerre több implementáció is lehet a classpath-on, akár különböző modulokban, ezért ezekhez egy iterátorral lehet hozzáférni. Nézzük, hogyan:
ServiceLoader<BookmarkService> bookmarkServices = ServiceLoader.load(BookmarkService.class);
Iterator<BookmarkService> i = bookmarkServices.iterator();
if (i.hasNext()) {
return i.next();
}
else {
throw new IllegalStateException("Service not found");
}
Bár itt van egy statikus metódus hívás, a Spring Framework pl. képes a ServiceLoader
példányt dependency injectionnel értékül adni.
Ezzel el is készült az alkalmazásunk. Nézzük sorban, hogy a megvalósítás során milyen problémákba futottam bele.
Egyrészt a pom.xml
állományban feljebb kellett állítani a Java verziószámot.
<maven.compiler.source>10</maven.compiler.source>
<maven.compiler.target>10</maven.compiler.target>
Mivel már a 10-es JDK-t telepítettem, ez valós verziószám. Azonban ahhoz, hogy működjön, az IntelliJ IDEA-ból is a legfrissebbet, a 2018.1.1 verziót kellett telepítenem, ugyanis korábbiban nem tudtam hozzáadni a 10-es JDK-t.
A következő meglepetés az volt, hogy a Jersey előző verziója nem volt hajlandó a 10-es JDK-val működni. A legutolsóra állítva (2.27) azonban a hiba megoldódott, így ez egy tanulság, hogy lehet, hogy egy 3rd party library régebbi verziója nem fog a legfrissebb Javaval és a Java Platform Module Systemmel együttműködni.
A következő meglepetés, hogy a Java SE 9-ben arról döntöttek, hogy eltávolítják a Java EE API-kat. Ennek esett áldozatául pl. a JAXB, valamint az Activation is. Ezért ezeket explicit módon fel kellett venni Maven függőségként.
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
A frontend
modulban a module-info.java
állományba is fel kellett venni azon modulokat, melyeket használ, ilyen a JAX-WS AP (java.ws.rs
), Jersey (jersey.server
, jersey.container.jdk.http
), valamint a Java SE HTTP szerver is (jdk.httpserver
).
Így módosul a frontend
modulban a module-info.java
állomány:
module frontend {
requires jdk.httpserver;
requires jersey.server;
requires jersey.container.jdk.http;
requires java.ws.rs;
requires backend;
uses jtechlog.backend.BookmarkService;
}
Ezen kívül a Jersey a működéséhez hozzá kell, hogy férjen a frontend
osztályaihoz, hiszen reflectionnel deríti fel őket, pl. a @Path
és @Provider
annotációval ellátott osztályokat. Valamint a JSON-né konvertáláshoz a backend
modulban a Bookmark
osztályhoz is hozzá kell férnie. Így azonban kialakul egy körkörös függőség, ami bár most szükséges, nem megnyugtató.
A végleges állományok tehát így néznek ki:
module backend {
exports jtechlog.backend to frontend, hk2.locator, jackson.databind;
provides jtechlog.backend.BookmarkService with jtechlog.backend.impl.BookmarkServiceImpl;
}
module frontend {
requires jdk.httpserver;
requires jersey.server;
requires jersey.container.jdk.http;
requires java.ws.rs;
requires backend;
exports jtechlog.frontend to jersey.server, hk2.locator;
uses jtechlog.backend.BookmarkService;
}