Module verwenden v.9

Die Einführung der Modularisierung in Java 9 erlaubt die Untergliederung eines Quelltextes in mehrere strukturelle Einheiten.

Motivation

Mit der Version 9 wurde in Java ein Modulkonzept eingeführt, das es erlaubt, den über zwanzig Jahre monolithisch gewachsenen JDK-Bibliotheken oberhalb der package-Ebene eine weitere Gliederungsmöglichkeit hinzuzufügen. Der Nutzen ist vielfältig. So können die jeweils benötigten Bibliothekselemente vergleichsweise differenziert gewählt werden. Die Vorteile liegen z.B.

  • in einer sichereren Steuerung der Zugriffsrechte
  • übersichtlicherer und besser zu gliedernder Entwicklungarbeit
  • in der besseren Skalierbarkeit einer Anwendung
  • und damit gekoppelt in der Möglichkeit zur besseren Anpassbarkeit der Größe ausführbarer Dateien, da nicht mehr die gesamte JDK-Bibliothek verwendet werden muss.

Es liegt nahe, dass die Nutzung des modularen Konzeptes hauptsächlich bei der Erstellung von Bibliotheken oder eher umfangreicher, differenzierter Anwendungs-Software ihre Vorteile ausspielen wird. Dennoch soll hier ein mögliches Verfahren bei der Erstellung einer modularisierten Anwendung mit Hilfe der IDE Eclipse anhand eines Minimalbeispiels gezeigt werden.

Das Beispielprogramm

Der prinzipielle Aufbaus eines modularisierten Projektes wird anhand der Erstellung eines sehr einfachen Programms gezeigt, das ein von JFrame abgeleitetes Fenster mit einem Label zeigt. Der Programmaufruf in main() und die Erzeugung der GUI sollen dabei in zwei unterschiedlichen Modulen erfolgen. Zur Demonstration der Einbindung von Interfaces wird zusätzlich ein drittes Modul erzeugt, dessen darin enthaltenes Interface von der abgeleiteten Fensterklasse implementiert wird.

Jedes Modul wird in einem gesonderten Eclipse-Projekt deklariert.

Anlegen der Projekte

Bei der Anlage eines neuen Projektes wird im zweiten Fenster abgefragt, ob die Anlage einer Datei module-info.java gewünscht wird. Sie dient als Moduldeklaration, in der durch spezielle Direktiven angegeben wird, welche Bestandteile das Modul enthält und auf welche davon und auf welche Weise von außen darauf zugegriffen werden kann.

#

Im darauf folgenden Fenster wird der Modulname angelegt, bei dem nach den Code-Konventionen darauf geachtet werden sollte, dass er mit einem Kleinbuchstaben beginnt und keine Leerzeichen enthält. Wir geben hier 'hello' ein.
Nach Abschluss der Projekterzeugung findet sich module-info.java auf oberster Ebene, also direkt im Verzeichnis src. Sie kann auch auf einer höheren package-Ebene angelegt werden, muss jedoch zwingend im Wurzelverzeichnis des Moduls liegen. Die Datei enthält einen durch das Schlüsselwort module und den Modulnamen eingeleiteten, zunächst noch leeren Block in geschweiften Klammern:

module hello {
             }

Ein Modul kann eine beliebige package-Struktur enthalten, deren Zugriff detailliert gesteuert werden kann.
Im Beispielprojekt wird zunächst das package de.javabeginners.hello und darin eine Klasse HelloWorld in einer Datei HelloWorld.java erzeugt.

Auf die gleiche Weise werden nun zwei weitere Projekte angelegt: HelloGUI mit einem Modul helloGUI, einem package de.javabeginners.helloGUI und der darin deklarierten GUI-Klasse HelloFrame, sowie ein Projekt HelloInt mit dem Modul helloInt, einem package de.javabeginners.helloInt und einem Interface HelloInt. Zur besseren Übersicht lassen sich die drei Projekte in Eclipse bei Bedarf zu einem Working Set zusammenfassen:
Die Struktur im package manager sieht nun folgendermaßen aus:

#

Die drei Quelltextdateien mit den Klassendeklarationen werden nun wie folgt ergänzt. Man beachte, dass HelloFrame von JFrame abgeleitet wird und HelloInt implementiert, somit zwei verschiedene Formen der Abhängigkeit vorliegen:

package de.javabeginners.hello;

import de.javabeginners.helloGui.HelloFrame;

public class HelloWorld {

	public static void main(String[] args) {
		HelloFrame frame = new HelloFrame();
		frame.setSize(300, 300);
		frame.setDefaultCloseOperation(HelloFrame.EXIT_ON_CLOSE);
		var txt = "Text";
		frame.setText(txt);
		frame.sprich();
		frame.setVisible(true);
	}
}

HelloWorld.java

package de.javabeginners.helloGui;

import java.awt.GridBagLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;

import de.javabeginners.helloInt.HelloInt;

public class HelloFrame extends JFrame implements HelloInt {
	private JLabel label;
	
	public HelloFrame() {
		label = new JLabel();
		this.add(label);
		this.setLayout(new GridBagLayout());
	}
	
	public void setText(String txt) {
		label.setText(txt);
	}

	public void sprich() {
		System.out.println("Hallo Welt!");
	}
}

				

HelloFrame.java

package de.javabeginners.helloInt;

public interface HelloInt {
    public void sprich();
}

HelloInt.java

Nach Ergänzung der Dateien werden in den Quelltexten eine Reihe an Fehlern durch rote Schlangenlinien angezeigt. Es fällt auf, dass selbst die korrekt angegebenen import-Anweisungen derartig gekennzeichnet sind.
Die Ursache liegt darin, dass in modularisierten Projekten die benötigten Zugriffe in der zugehörigen Datei module-info.java geregelt werden müssen. Dies gilt auch für die Bausteine der JDK-Bibliothek.
Ergänzt man die Datei module-info.java des Projektes HelloGUI folgendermaßen

module helloGUI {
	requires java.desktop;
}

so verschwindet diese Fehleranzeige in der Klasse HelloFrame. Der Zugriff auf die Elemente des Moduls java.desktop, hier die benötigten AWT- und Swing-Bibliotheken, wird durch die Angabe des Moduls nach dem Schlüsselwort requires in module-info.java auf diese Weise erlaubt. Das Schlüsselwort requires kennzeichnet die Abhängigkeit eines Moduls von einem anderen.
Ein Überblick über die hier möglichen Schlüsselworte und deren Funktionen findet sich im Abschnitt Schlüsselworte der Moduldirektiven.

In der Klasse HelloWorld kennzeichnet der Compiler alle Einträge als fehlerhaft, die die Klasse HelloFrame referenzieren. Man vermutet zurecht, dass ein entsprechender Eintrag in module-info.java des Moduls hello benötigt wird, um Zugriff auf die Klasse zu erhalten:

module hello {
	requires helloGUI;
}

Erstaunlicherweise behebt dieser Eintrag jedoch nicht alle Fehleranzeigen. Das ist erst dann der Fall, wenn das package de.javabeginners.helloGui explizit in der zugehörigen Moduldeklaration zum Export vorgesehen wird:

module helloGUI {
	requires java.desktop;
	exports de.javabeginners.helloGui;
}

Nun fehlt noch die Behebung eines Fehlers in der Klasse HelloFrame. Es verwundert nun nicht mehr, dass der Import des Interfaces noch markiert ist: Das package muss noch exportiert werden:

module helloInt {
	exports de.javabeginners.helloInt;
}

Der Fehler wird hierdurch jedoch immer noch nicht vollständig behoben, da die Nutzung des Interfaces durch das Modul helloGUI noch angezeigt werden muss:

module helloGUI {
	exports de.javabeginners.helloGui;
	requires java.desktop;
	requires helloInt;
	uses de.javabeginners.helloInt.HelloInt;
}

Bei der Nutzung des Interface muss sowohl eine requires-, als auch eine uses-Direktive angegeben werden. requires kennzeichnet hier wiederum die Abhängigkeit des einen Moduls von dem anderen, uses bezeichnet den 'Service', hier also das Implementieren des Interface HelloInt. Statt der uses-Direktive könnte hier übrigens auch

provides  de.javabeginners.helloInt.HelloInt with de.javabeginners.helloGui.HelloFrame;

angegeben werde.

Die JDK-Module

Auch wenn Eclipse mit dem Content-Assistenten beim Fehlen einer Modul-Direktive die notwendigen Vorschläge macht, kann es nicht schaden, gelegentlich einen Überblick über die zur Verfügung stehenden JDK-Module zu erhalten.

Fanden sich in den Java-Versionen vor der Version 9 die Bibliotheken innerhalb des Verzeichnisses $JAVA_HOME/jre/lib, so wurde, nach Einführung der Modularisierung mit Java 9, das Verzeichnis jre durch jmods ersetzt. In diesem befinden sich die JDK-Module. Deren Auflistung kann über die Kommandozeile durch Aufruf der ausführbaren Datei java im bin-Verzeichnis der Installation mit der Option --list-modules erfolgen:

# java --list-modules

Eine ausführliche Erläuterung der einzelnen Module findet sich dann in der Java-Dokumentation der verwendeten JDK-Version.

Schlüsselworte der Moduldirektiven

Die im Folgenden gelisteten Schlüsselworte sind im Kontext einer Moduldeklaration für die jeweilige Funktion reserviert und dürfen nicht in einem anderen Kontext verwendet werden.

requires <modulname> Gibt die Abhängigkeit von einem anderen Modul an. Jedes Modul muss seine Abhängigkeiten explizit benennen.
requires transitive <modulname> Gibt an, dass Module, die das aktuelle Modul lesen, auch die genannte Abhängigkeit lesen.
exports <modulname> Gibt an, dass ein oder mehrere enthaltene Typen für alle anderen Module sichtbar sein soll. Selbstverständlich müssen die Zugriffsmodifizierer der Klassen und Interfaces dies auch erlauben.
uses <modulname> Gibt an, dass ein Objekt des Moduls das genannte nutzt, etwa im Sinne des Implementierens eines Interfaces.
provides <type> with <class> Definiert einen "Service", also die Abhängigkeiten zwischen einer Klasse einerseits und einem von dieser implementierten Interface oder einer abstrakten Klasse andererseits.
Bsp.: Implementiert die Klasse A aus Modul ModA das Interface B aus Modul ModB, so wird in der Moduldeklaration des Moduls A angegeben provides B with A;. Man beachte, dass hier die Typen (natürlich mit package-Angabe) und nicht die Module genannt werden.
opens <package> Erlaubt den Zugriff auf öffentliche Typen des package zur Laufzeit(!). Zudem sind alle Typen des genannten package durch reflection erreichbar.
opens <package> to <modulliste> Erlaubt den Zugriff auf öffentliche Typen des package zur Laufzeit(!) durch die aufgelisteten Module. Zudem sind alle Typen des genannten package durch reflection für die genannten Module erreichbar. Die Liste der Module muss kommasepariert sein.
open module <modulname> Erlaubt den Zugriff auf alle Typen des moduls zur Laufzeit(!) und via reflection.
Quellen

http://www.javamagazine.mozaicreader.com/SeptOct2017#&pageSet=18&page=0