© LIORIKI/Shutterstock.com
Der Weg zu einer modularisierten Java-Anwendung

Möglichkeiten des JPMS


Mit der Veröffentlichung von Java 11 als Long-Term-Support-Version hält das Java Platform Module System (JPMS) endgültig Einzug in die Realität eines jeden Java-Entwicklers. Grund genug, die Möglichkeiten des JPMS in einem praxisorientierten Beitrag zu betrachten.

Dieser Artikel geht von rudimentärer Vertrautheit mit den Begrifflichkeiten rund um das JPMS aus [1].

Im Fokus steht die Migration einer kleinen Beispielanwendung hin zu einer modularisierten Applikation unter Verwendung des Java Platform Module System (JPMS).

Die Domäne des hier vorgestellten Beispiels ist bewusst einfach gehalten: Unsere Anwendung durchsucht einen übergebenen Suchtext nach einem Suchwort und liefert dann als Ergebnis Indices zu allen exakten Aufkommen des Suchworts innerhalb dieses Suchtexts.

Zur Lösung dieses Problems implementiert die Anwendung zwei String-Matching-Algorithmen, die unterschiedliche Charakteristiken aufweisen:

  • einen Brute-Force-basierten Ansatz, der insbesondere auf umfangreichen Textkörpern keine gute Performanz zeigt, und

  • einen fortgeschrittenen Ansatz mit dem Algorithmus nach Knuth, Morris und Pratt, der seine Stärken bei umfangreichen Textkörpern ausspielt.

Die Anwendung implementiert ein einfaches CLI. Ein Benutzer kann das CLI durch drei Pflichtargumente parametrieren: die Kennung eines ausgewählten Algorithmus (naive für den Brute-Force-Ansatz, kmp für den Knuth-Morris-Pratt-Algorithmus), den Suchtext und das Suchwort.

Struktur der Beispielanwendung

Die Anwendung ist mit Java 8 implementiert und wird mit Apache Maven gebaut. Der Quelltext liegt in zwei Maven-Modulen vor (Abb. 1). Wann immer lediglich von einem Modul die Rede ist und nicht explizit von einem Maven- oder Java-Modul, ist der Begriff synonym zu verstehen.

  • matchers-core beinhaltet das Java API für die String-Matching-Algorithmen und die Implementierungen des Brute-Force-Algorithmus und des Knuth-Morris-Pratt-Algorithmus.

  • matchers-cli referenziert matchers-core und implementiert das CLI.

guenther_modulapp_1.tif_fmt1.jpgAbb. 1: „matchers-core“ definiert das API und stellt konkrete Implementierung bereit, „matchers-cli“ bietet eine CLI-Anwendung auf dessen Basis an
guenther_modulapp_2.tif_fmt1.jpgAbb. 2: In beiden Modulen sind die Java-Klassen im selben Java Package verortet

Das Maven-Modul matchers-cor stellt sowohl die öffentliche Schnittstelle als auch die Implementierungen bereit. Die Methode match konsumiert einen Textkörper (vgl. Methodenargument haystack) und ein Suchwort (vgl. Methodenargument needle) und liefert alle Indices zu den Vorkommnissen des Suchworts als List<Integer>. Dieses Interface ist in der aktuellen Lösung im Package net.mguenther.matchers lokalisiert. Ko-lokalisiert mit dem Interface sind die zuvor genannten Algorithmen in den Klassen BruteForceMatcher und KnuthMorrisPrattMatcher (Abb. 2).

Listing 1 zeigt, wie die Auflösung des Algorithmus in Klasse MatchersCli in der Beispielanwendung implementiert ist.

Listing 1: Selektion eines spezifischen Matching-Algorithmus im CLI

Matcher matcher = null; switch (algorithm) { case "kmp": System.out.println("Using Knuth-Morris-Pratt matcher"); matcher = new KnuthMorrisPrattMatcher(); break; case "naive": default: System.out.println("Using Brute-Force matcher"); matcher = new BruteForceMatcher(); } List<Integer> matchingPositions = matcher.match(haystack, needle);

Defizite

Das grundlegende Problem, mit dem wir uns bei diesem Lösungsansatz konfrontiert sehen, liegt in den Sichtbarkeits- und Zugriffsregeln von Java. Das Maven-Modul matchers-cli kennt Implementierungsdetails des Moduls matchers-core – andernfalls könnten wir über das CLI keine konkreten Algorithmen instanziieren. Dies führt jedoch zu einer engen Kopplung zwischen den beiden Modulen.

Das ist allerdings nicht das einzige Problem. Möchten wir einen neuen Algorithmus für das CLI bereitstellen, muss der Algorithmus nicht nur in matchers-core implementiert, sondern auch in matchers-cli integriert werden. Unsere Lösung verletzt damit das Open Closed Principle (OCP). Diesem Problem kann man – unabhängig vom Modulsystem – mit einem entsprechenden Lösungsentwurf begegnen. Wir werden jedoch im Lauf des Artikels sehen, dass wir durch Ausnutzung des JPMS auf natürliche Weise zu einer OCP-konformen Lösung gelangen. Idealerweise müssen wir nur noch diejenigen Abhängigkeiten deklarieren, die zum Kompilierungszeitpunkt erforderlich sind.

Modularisieren der Beispielanwendung

Bevor wir mit der Modularisierung der Beispielanwendung beginnen, müssen wir zunächst die Implikationen diskutieren, die bei der Verwendung von Maven mit dem JPMS einhergehen. Grundsätzlich erlaubt das JPMS, mehrere Java-Module innerhalb eines src-Verzeichnisses zu verwalten – solange die Modulgrenzen durch die entsprechenden Moduldeskriptoren gepflegt sind. Zwischen einem Java-Modul und einem JAR gibt es eine 1:1-Beziehung. Da das Zielartefakt eines Maven-Moduls ein JAR ist, das über seine Maven-Koordinaten eindeutig identifizierbar ist, besteht diese 1:1-Beziehung auch für ein Maven-Modul und das resultierende JAR. Zusammengenommen sorgt das dafür, dass zwischen einem Maven-Modul und dem innen liegenden Java-Modul ebenfalls eine 1:1-Beziehung gelten muss.

Populäre IDEs verweigern die Erzeugung eines zweiten Java-Moduls innerhalb eines Maven-Moduls. Sollte es dennoch gelingen, multiple Java-Module anzulegen, zeigt beispielsweise IntelliJ IDEA das folgende Fehlerbild:

module-info.java already exists within module

Auftrennen von API und Implementierung

Schauen wir uns zunächst nochmal die interne Struktur des Maven-Moduls matchers-core an (Abb. 3).

guenther_modulapp_3.tif_fmt1.jpgAbb. 3: API und Implementierung sind im gleichen Package verortet

Alle Klassen und Interfaces sind im selben Package net.mguenther.matchers lokalisiert. Zunächst erzeugen wir ein neues Maven-Modul namens matchers-api, das das Java-Modul matchers.api beheimatet. matchers.api stellt die öffentliche Schnittstelle für unsere Anwendung bereit. Das Interface Matcher verschieben wir in dieses Modul und exponieren es über einen Package Export im Moduldeskriptor module-info.java des Java-Moduls:

module matchers.api { exports net.mguenther.matchers; }

Die Implementierungen zu den Algorithmen bleiben in Maven-Modul machers-core. Der Quelltext in matchers-core ist aber noch keinem dedizierten Java-Modul zugeordnet. Das erreichen wir, indem wir eine module-info.java in Verzeichnis src/main/java anlegen. Den Namen des Java-Moduls legen wir auf matchers.impl fest und deklarieren die Abhängigkeit zu matchers.api über eine requires-Beziehung. Die Implementierungen in match...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang