Migration zu Java-Modulen
Strategien zur Migration classpath-basierter Java-Anwendungen auf das Java Platform Module System.
Sie müssen nicht modularisieren, um auf Java 9+ umzusteigen. Eine Classpath-Anwendung läuft unverändert — alles wird zu einem einzigen großen unbenannten Modul. Migration ist ein optionaler Schritt, den Sie unternehmen, wenn Sie starke Kapselung, ein benutzerdefiniertes jlink-Runtime oder einen saubereren Abhängigkeitsgraphen wünschen. Die Kunst besteht darin, dies schrittweise zu tun, denn die drei Modularten — benannte, automatische und unbenannte — ermöglichen die Koexistenz von modularem und nicht-modularem Code.
Dieses Kapitel behandelt, wann Migration sich lohnt, wie Sie eine bestehende Anwendung zuerst auf einem modernen JDK zum Laufen bringen, die zwei Modularisierungsrichtungen (Bottom-up vs. Top-down), die Werkzeuge, die die schwere Arbeit übernehmen, und ein ausführbares Beispiel, das den Modulgraphen so inspiziert, wie jdeps es tut. Falls die Begriffe hier unbekannt sind, beginnen Sie mit der Module-Einführung und Deklaration eines Moduls.
Zuerst: einfach auf Java 9+ ausführen (ohne Module)
Bevor Sie eine module-info.java hinzufügen, kompilieren Sie Ihre bestehende Anwendung neu und führen Sie sie auf dem neuen JDK mit allem im Classpath aus. Die wahrscheinlichen Fehler haben nichts mit Ihren Modulen zu tun, sondern alles mit dem jetzt modularen JDK:
- Entfernte Java EE-Module —
java.xml.bind(JAXB),java.activation, CORBA usw. wurden aus dem JDK entfernt. Fügen Sie sie als gewöhnliche Abhängigkeiten hinzu. - Gekapselte Interna —
sun.misc.Unsafeund ähnliches ist nicht mehr zugänglich.--add-exports/--add-opens-Flags sind ein Notausgang, während Sie die Verwendung entfernen. - Geteilte Pakete — zwei JARs, die dasselbe Paket bereitstellen, kollidieren nun auf dem Modulpfad (aber nicht auf dem Classpath).
Bringen Sie die Anwendung zuerst auf dem Classpath zum Laufen. Erst dann beginnen Sie mit der Modularisierung.
Bottom-up-Migration
Die bevorzugte Strategie, wenn Sie die gesamte Codebasis kontrollieren:
- Beginnen Sie mit den Blatt-Bibliotheken — Module, die von nichts Ihrem abhängen.
- Geben Sie jeder eine
module-info.javaund verschieben Sie sie auf den Modulpfad. - Ihre Abhängigen können sie nun mit
requireseinbinden. Arbeiten Sie aufwärts im Abhängigkeitsbaum bis zum Einstiegspunkt der Anwendung.
Das funktioniert, weil ein benanntes Modul andere benannte Module und automatische Module einfordern kann — solange die eigenen Abhängigkeiten eines Blatts mindestens automatisch sind, können Sie es modularisieren. Jeder Schritt hält den Build grün. Wenn ein Blatt steckbares Verhalten bereitstellt, ist dies auch ein natürlicher Punkt, um eine Service-Grenze einzuführen, sodass die Abhängigen uses eine Schnittstelle statt einer konkreten Klasse verwenden.
Top-down-Migration
Wenn Sie die Bibliotheken nicht kontrollieren (Drittanbieter-JARs ohne Deskriptoren), gehen Sie den umgekehrten Weg:
- Modularisieren Sie zuerst Ihren eigenen Code auf oberster Ebene.
- Platzieren Sie die noch nicht modularen Drittanbieter-JARs auf dem Modulpfad, wo sie zu automatischen Modulen werden.
- Fordern Sie sie mit
requiresunter ihrem automatischen Namen an (aus dem JAR-Dateinamen oderAutomatic-Module-Name). - Sobald jede Bibliothek eine echte
module-infoliefert, tauschen Sie die automatische Abhängigkeit gegen die benannte aus — ohne Änderung an Ihremrequires.
Automatische Module sind das Gerüst, das Top-down möglich macht; sie erlauben Ihrem benannten Code, von JARs abzuhängen, die noch nicht modular sind.
Werkzeuge und Fallstricke
jdepsanalysiert die tatsächlichen Abhängigkeiten eines JARs und kann sogar eine erstemodule-info.javagenerieren (jdeps --generate-module-info). Beginnen Sie dort, anstatt Direktiven von Hand zu schreiben.Automatic-Module-Name— wenn Sie eine Bibliothek veröffentlichen, fügen Sie diesen Manifest-Eintrag hinzu, bevor Sie einen vollständigen Deskriptor schreiben. Er fixiert einen stabilen Modulnamen, damit Downstream-Nutzer des automatischen Moduls nicht beeinträchtigt werden, wenn Sie das JAR später umbenennen.- Geteilte Pakete müssen zusammengeführt werden. Der Modulpfad verbietet zwei Modulen, dasselbe Paket zu besitzen; der Classpath tolerierte dies. Dies ist der häufigste Migrationsblockierer.
opensfür Reflexion. Frameworks, die über Ihre Klassen reflektieren (Jackson, JPA, Spring), benötigenopens, sonst erhalten Sie zur LaufzeitInaccessibleObjectException, obwohl die Kompilierung erfolgreich war.
Geteilte Pakete sind der Fehler, der die meisten überrascht. Auf dem Classpath fusionierten zwei JARs, die jeweils Klassen in com.example.util enthielten, einfach; auf dem Modulpfad lehnt der Resolver sie mit einem "module reads package ... from both"-Fehler ab. Es gibt kein Flag, das dies legal macht — Sie müssen das duplizierte Paket in ein einziges Modul zusammenführen (oder eines umbenennen). Prüfen Sie auf geteilte Pakete mit jdeps, bevor Sie mit dem Schreiben von Deskriptoren beginnen.
Ein praktisches Beispiel: Inspektion eines Abhängigkeitsgraphen wie jdeps
Die Migrationsplanung beginnt mit „was hängt wovon ab." Dieses Programm liest zur Laufzeit die Moduldeskriptoren der Boot-Layer — dieselben requires-Informationen, die jdeps ausgibt — und erkennt auch, ob es als benanntes Modul oder auf dem Classpath läuft, genau die Prüfung, die anzeigt, wie weit eine Migration fortgeschritten ist.
Was Sie aus der Ausführung mitnehmen:
- Das Programm meldete, dass es als UNNAMED-Modul läuft — genau das, was Sie von Classpath-Code erwarten, und das Laufzeitsignal, dass diese Codebasis noch nicht modularisiert wurde. Wenn Sie nach dem Hinzufügen einer
module-infound dem Wechsel auf den Modulpfad erneut ausführen, würde dies auf NAMED umschalten — eine konkrete „Sind wir schon da?"-Prüfung während der Migration. inspect("java.sql")gab seine echterequires-Liste aus, einschließlich einer transitiven Abhängigkeit im Stil vonjava.transaction.xaoderjava.logging. Dies sind dieselben Informationen, diejdepsliefert — die wahren Abhängigkeiten eines Moduls zu kennen ist Schritt eins beim Schreiben seinermodule-info.java, und der Deskriptor enthält sie bereits.- Einige requires gaben
(transitive)aus. Das sind die Abhängigkeiten, die ein Modul re-exportiert; wenn Sierequires java.sqlverwenden, lesen Sie automatisch auch seine transitiven Abhängigkeiten. Das Erkennen dieser Abhängigkeiten zeigt Ihnen, welcherequires transitive-Zeilen Sie kopieren müssen, wenn Sie Code modularisieren, der ein solches Modul umhüllt. findModulegab einOptionalzurück und verdeutlicht damit, dass ein Modul in einem gegebenen Runtime möglicherweise einfach nicht vorhanden ist — ein mitjlinkgekürztes Image könntejava.desktopvollständig weglassen. Migrationspläne müssen berücksichtigen, welche Module tatsächlich im Ziel-Runtime vorhanden sind.- Jedes Modul hängt letztendlich von
java.baseab, und es erscheint nie in einerrequires-Liste, weil es implizit ist. Die Gesamtanzahl der Boot-Module zeigt, dass das JDK ein Graph ist, den Sie programmatisch abfragen können — die Grundlage, auf der Werkzeuge wiejdepsundjlinkaufbauen, um eine Anwendung zu analysieren und zu verkleinern.
Eine sinnvolle Migrationsreihenfolge, zusammengefasst
- Auf dem Classpath auf dem neuen JDK ausführen; Fehler durch entfernte Module und interne API-Nutzung beheben.
jdepsverwenden, um Abhängigkeiten zu kartieren und geteilte Pakete zu finden.Automatic-Module-Namezu allen Bibliotheken hinzufügen, die Sie veröffentlichen.- Bottom-up modularisieren, wenn Sie den Baum besitzen; top-down (mit Stütze auf automatische Module), wenn nicht.
openshinzufügen, wo Frameworks reflektieren; zur Laufzeit verifizieren, nicht nur zur Kompilierzeit.
Damit ist der Teil über das Java Platform Module System abgeschlossen: Sie wissen jetzt, was Module sind, wie man eines deklariert, die drei Arten und wie sie koexistieren, wie Services Module entkoppeln und wie man eine bestehende Anwendung ohne Neuentwicklung auf den Modulpfad migriert.