SemaTrain Logo Ein Fachportal von SemaTrain

Modularisierung mit Java 9+ (JPMS)

Sie strukturieren Java-Anwendungen in Module: klare Abhängigkeitsgrenzen, gezieltes exports, sauberes requires und optional Service Provider (SPI) – für langfristig wartbaren Code im SemaTrain-Trainingskontext.

Hinweis: Module sind besonders sinnvoll für größere Codebasen (Architekturgrenzen). Für kleine Projekte kann ein „klassisches“ Build (Maven/Gradle ohne JPMS) ausreichen.

Java Fortgeschrittenen Schulung – Kursbezug

Dieses Kapitel ist Teil des Lernpfads zur Java Fortgeschrittenen Schulung. Fokus: Architekturgrenzen sichtbar machen (API nach außen, Implementierung nach innen) – z.B. für eine modulare SemaTrain-Trainingsplattform.

Ziel dieses Kapitels: Sie können module-info.java lesen/schreiben, Pakete gezielt exportieren, Abhängigkeiten sauber modellieren und (optional) ein Service-Provider-Muster mit JPMS umsetzen.

Worum geht’s?

Lehr-/Lernziele

Nach diesem Kapitel können Sie …

1) Minimal: Modul deklarieren

Beispiel: „Kurs-API“ wird exportiert, interne Implementierung bleibt verborgen.

Minimaler Modul-Descriptor: exports (Java)
// Datei: src/module-info.java
module de.sematrain.training.core {
  exports de.sematrain.training.api;   // öffentlich (API)
  // de.sematrain.training.internal wird NICHT exportiert → intern
}

2) Abhängigkeiten: requires & requires transitive

Transitive ist sinnvoll, wenn Ihr API Typen aus einer Abhängigkeit „durchreicht“.

requires: Modul braucht eine Abhängigkeit (Java)
// Core nutzt ein Logging-Modul
module de.sematrain.training.core {
  requires java.logging;
  exports de.sematrain.training.api;
}
requires transitive: Konsumenten bekommen die Abhängigkeit automatisch (Java)
// API-Modul „zieht“ eine Abhängigkeit für Konsumenten mit (Beispiel)
module de.sematrain.training.api {
  requires transitive java.logging;
  exports de.sematrain.training.api;
}

3) Strong Encapsulation: API vs. Internal

opens: Reflection gezielt erlauben (nicht pauschal alles öffnen) (Java)
module de.sematrain.training.core {
  exports de.sematrain.training.api;

  // Reflection nur für ein internes Paket und nur für ein Zielmodul:
  opens de.sematrain.training.internal.model to com.fasterxml.jackson.databind;
}

4) Services: uses / provides (Plugin-Architektur)

Szenario: SemaTrain will Ausgabeformate austauschbar machen (Plain/Uppercase/Markdown …), ohne die App anzupassen.

SPI: Formatter-Vertrag (Plugin-Schnittstelle) (Java)
// Service-Interface (SPI) – im API-Modul
package de.sematrain.format.spi;

public interface Formatter {
  String name();
  String format(String text);
}
API-Modul: exports + uses (Service wird konsumiert) (Java)
// module-info im API-Modul
module de.sematrain.format.api {
  exports de.sematrain.format.spi;
  uses de.sematrain.format.spi.Formatter;
}
Provider: konkrete Implementierung (Java)
// Provider-Implementierung (in separatem Modul)
package de.sematrain.format.upper;

import de.sematrain.format.spi.Formatter;

public class UppercaseFormatter implements Formatter {
  public String name() { return "GROSSBUCHSTABEN"; }
  public String format(String text) { return text.toUpperCase(); }
}
Provider-Modul: provides ... with ... (Java)
// module-info im Provider-Modul
module de.sematrain.format.upper {
  requires de.sematrain.format.api;

  provides de.sematrain.format.spi.Formatter
    with de.sematrain.format.upper.UppercaseFormatter;
}
ServiceLoader: Plugins dynamisch laden (Java)
// App lädt Provider per ServiceLoader (kein new UppercaseFormatter())
import java.util.ServiceLoader;
import de.sematrain.format.spi.Formatter;

public class FormatDemo {
  public static void main(String[] args) {
    String text = "SemaTrain: Java Fortgeschritten";

    for (Formatter f : ServiceLoader.load(Formatter.class)) {
      System.out.println("Formatter: " + f.name() + " => " + f.format(text));
    }
  }
}

Praxisaufgabe (Mini)

Beitrag zu den Lehr-/Lernzielen: LZ1 (exports/requires) · LZ2 (Grenzen) · LZ3 (optional Services)

  1. Erstellen Sie 2 Module: de.sematrain.training.api und de.sematrain.training.core.
  2. ...api exportiert nur sein API-Paket. ...core exportiert nichts (intern).
  3. Erstellen Sie im API ein Interface CourseLabeler und im Core eine Implementierung.
  4. Testen Sie: Der Konsument kann nur das API nutzen – interne Klassen aus Core sind nicht sichtbar.
  5. Optional: Bauen Sie das Ganze als Service (uses/provides), sodass weitere Provider später ergänzt werden können.
Lösung anzeigen
Lösung: API exportiert, Core bleibt intern (SemaTrain-Kontext) (Java)
// Modul: de.sematrain.training.api
// Datei: de.sematrain.training.api/module-info.java
module de.sematrain.training.api {
  exports de.sematrain.training.api;
}

// Datei: de.sematrain.training.api/de/sematrain/training/api/CourseLabeler.java
package de.sematrain.training.api;

public interface CourseLabeler {
  String label(String kursTitel, String ort);
}

// Modul: de.sematrain.training.core
// Datei: de.sematrain.training.core/module-info.java
module de.sematrain.training.core {
  requires de.sematrain.training.api;
  // Nichts exportieren → Implementierung bleibt intern
}

// Datei: de.sematrain.training.core/de/sematrain/training/core/DefaultCourseLabeler.java
package de.sematrain.training.core;

import de.sematrain.training.api.CourseLabeler;

public class DefaultCourseLabeler implements CourseLabeler {
  public String label(String kursTitel, String ort) {
    String o = (ort == null || ort.isBlank()) ? "remote" : ort;
    return "Kurs: " + kursTitel + " | Ort: " + o + " | Anbieter: SemaTrain";
  }
}
Optional: typische Stolperfallen
  • Split Packages: das gleiche Package in mehreren Modulen → vermeiden.
  • Zu viel exports: nur API-Pakete exportieren, interne Details intern lassen.
  • Reflection: Frameworks brauchen ggf. opens – gezielt statt „open module“.
  • Classpath vs. Modulepath: JPMS wirkt nur zuverlässig auf dem Modulepath.

Kurz-Takeaways

Quiz: Java 9+ Module (Lehr-/Lernziele Check)

1. (LZ1) Wofür ist exports in module-info.java da?

2. (LZ2) Was bedeutet „strong encapsulation“ im Modul-Kontext?

3. (LZ2) Wann ist requires transitive sinnvoll?

4. (LZ3) Welche JPMS-Kombination beschreibt Service Provider korrekt?

Praxisaufgabe

Mini-Projekt: „Plugin-Formatter“ (JPMS-Service, SemaTrain-Kontext)

Beitrag zu den Lehr-/Lernzielen: LZ1 (module-info: exports/requires) · LZ2 (API vs. Implementierung, Encapsulation) · LZ3 (Service: uses/provides + ServiceLoader)

Szenario: SemaTrain möchte Kursinformationen flexibel formatieren (z.B. „KLARTEXT“, „GROSSBUCHSTABEN“, später „MARKDOWN“), ohne die Anwendung jedes Mal umbauen zu müssen. Neue Formatierer sollen als Plugin hinzugefügt werden können. JPMS sorgt dafür, dass die API sauber sichtbar ist und die Implementierung getrennt bleibt.

Lösung anzeigen
Teil 1: SPI-Interface (öffentliches API) (Java)
// Modul: de.sematrain.format.api
// Datei: de.sematrain.format.api/de/sematrain/format/spi/Formatter.java
package de.sematrain.format.spi;

public interface Formatter {
  String name();
  String format(String text);
}
Teil 2: API-Modul (exports + uses) (Java)
// Modul: de.sematrain.format.api
// Datei: de.sematrain.format.api/module-info.java
module de.sematrain.format.api {
  exports de.sematrain.format.spi;
  uses de.sematrain.format.spi.Formatter;
}
Teil 3: Provider (Implementierung als Plugin) (Java)
// Modul: de.sematrain.format.upper
// Datei: de.sematrain.format.upper/de/sematrain/format/upper/UppercaseFormatter.java
package de.sematrain.format.upper;

import de.sematrain.format.spi.Formatter;

public class UppercaseFormatter implements Formatter {
  @Override
  public String name() { return "GROSSBUCHSTABEN"; }

  @Override
  public String format(String text) {
    return (text == null) ? "" : text.toUpperCase();
  }
}
Teil 4: Provider-Modul (provides ... with ...) (Java)
// Modul: de.sematrain.format.upper
// Datei: de.sematrain.format.upper/module-info.java
module de.sematrain.format.upper {
  requires de.sematrain.format.api;

  provides de.sematrain.format.spi.Formatter
    with de.sematrain.format.upper.UppercaseFormatter;
}
Teil 5: App nutzt ServiceLoader (Plugins dynamisch) (Java)
// Modul: de.sematrain.app
// Datei: de.sematrain.app/de/sematrain/app/SemaTrainApp.java
package de.sematrain.app;

import java.util.*;
import de.sematrain.format.spi.Formatter;

public class SemaTrainApp {

  public static void main(String[] args) {
    String kursText = "Kurs: Java Fortgeschritten | Ort: Hamburg | Dauer: 3 Tage";

    List<Formatter> formatter = new ArrayList<>();
    for (Formatter f : ServiceLoader.load(Formatter.class)) {
      formatter.add(f);
    }

    if (formatter.isEmpty()) {
      System.out.println("Kein Formatierer-Plugin gefunden (prüfen: Modulepath, uses/provides).");
      return;
    }

    System.out.println("Gefundene Formatierer: " + formatter.size());
    for (Formatter f : formatter) {
      System.out.println("- Formatierer: " + f.name());
      System.out.println("  Ausgabe: " + f.format(kursText));
    }
  }
}
Teil 6: App-Modul (requires API) (Java)
// Modul: de.sematrain.app
// Datei: de.sematrain.app/module-info.java
module de.sematrain.app {
  requires de.sematrain.format.api;
}
Teil 7: Typische Ursachen, wenn kein Provider geladen wird (Text)
// Start-Hinweis:
// - JPMS wirkt zuverlässig auf dem Modulepath (nicht nur Classpath).
// - Beim Start müssen App + Provider-Module auf dem Modulepath liegen.
// - Wenn kein Provider gefunden wird: prüfen, ob
//   * API-Modul: uses Formatter
//   * Provider-Modul: provides Formatter with ...
//   * Provider-Klassenname + Paket korrekt sind