Modulare Java-Programmierung via Services

Eine umfangreichere Anwendung ist unübersichtlich, schwierig zu „pflegen“ und erweitern, falls ich sie in einem einzigen JAR unterbringe. Teile ich sie auf in mehrere Projekte und jedes Projekt ergibt ein JAR, ist dies besser. Die Abhängigkeiten der Projekte untereinander sollten minimal sein (eine Klasse des Projekts X sollte nicht nach einer Klasse des Projekts Y verlangen).

Bevor ich eine größere Anwendung programmiere, sollte ich mich einarbeiten in ein Framework, das zu diesem Zweck entwickelt wurde, beispielsweise der NetBeans Platform und dieses benutzen. Die Einarbeitung kostet Zeit, die Benutzung spart diese jedoch auf lange Sicht und schont die Nerven.

Falls ich mich nicht einarbeiten will, ist der ServiceLoader nach meinen aktuellen Kenntnissen am geeignetsten, dieser lässt sich auch in einem Framework benutzen.

Dazu definiere ich in einem oder mehreren Projekten Interfaces (Services). Diese implementiere ich in anderen Projekten.

Beispielsweise könnte ich entscheiden, das Programmfenster hat drei Bereiche, in die andere Module (Projekte) „Fenster“ einfügen können. Die Interface-Definition sähe z.B. so aus:

public interface WindowSystem {
    void dockIntoSelectView(Component component);
    void dockIntoEditView(Component component);
    void dockIntoPropertiesView(Component component);
}

In irgend einem Projekt implementiere ich dies und publiziere diese Implementierung gemäß der JavaDoc des ServiceLoader im META-INF/services-Verzeichnis:

public final class ApplicationFrame implements WindowSystem {

    @Override
    public void dockIntoSelectView(Component component) {
        tabbedSelectViewPane.add(component);
    }

    @Override
    public void dockIntoEditView(Component component) {
        tabbedPaneEditView.add(component);
    }

    @Override
    public void dockIntoPropertiesView(Component component) {
        tabbedPanePropertiesView.add(component);
    }
}

Will ich in einem anderen Projekt der Select-View des Anwendungsfensters etwas hinzufügen, benutze ich den ServiceLoader. Ich vereinfache mir die Arbeit mit der Klasse ServiceLookup eines Projekts, das alle anderen Projekte nutzen dürfen und das keine Abhängigkeit zu einem anderen Projekt hat, siehe Implementierung weiter unten:

public final class ImageFileBrowser implements Module {

    private JTree fileSystemTree;

    @Override
    public void initModule() {
        java.awt.EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                createFileSystemTree();
                addFileSystemTree();
            }
        );
    }

    private void createFileSystemTree() {
        FileFilter directoryFilter = FileSystemUtil.getDirectoriesOnlyFileFilter();
        TreeModel fileSystemTreeModel = FileSystemUtil.createFileSystemTreeModel(directoryFilter);
        
        fileSystemTree = new JTree(fileSystemTreeModel);
        fileSystemTree.setCellRenderer(new FileSystemTreeCellRenderer());
        fileSystemTree.addTreeSelectionListener(new ImageDisplayTreeSelectionListener());
    }

    private void addFileSystemTree() {
        WindowSystem windowSystem = ServiceLookup.lookup(WindowSystem.class);

        windowSystem.dockIntoSelectView(fileSystemTree);
    }
}

Klasse ServiceLookup eines „Utility“-Projekts, das keine Abhängigkeiten zu anderen Projekten hat und das alle anderen Projekte benutzen dürfen:

public final class ServiceLookup {

    public static <T> T lookup(Class<T> serviceClass) {
        if (serviceClass == null) {
            throw new NullPointerException("serviceClass == null");
        }

        ServiceLoader<T> serviceLoader = ServiceLoader.load(serviceClass);
        Iterator<T> serviceImplementations = serviceLoader.iterator();

        return serviceImplementations.hasNext()
                ? serviceImplementations.next()
                : null;
    }

    public static <T> Collection<? extends T> lookupAll(Class<T> serviceClass) {
        if (serviceClass == null) {
            throw new NullPointerException("serviceClass == null");
        }

        Collection<T> serviceImplementations = new ArrayList<T>();
        ServiceLoader<T> serviceLoader = ServiceLoader.load(serviceClass);

        for (T service : serviceLoader) {
            serviceImplementations.add(service);
        }

        return serviceImplementations;
    }

    private ServiceLookup() {}
}

Prinzip einer modularen Anwendung mit Hilfe des ServiceLoader:

  • Die Anwendung besteht aus unterschiedlichen Projekten, jedes hat eine bestimmte Aufgabe („Zuständigkeit“)
  • Einige Projekte deklarieren verschiedene Services als Interface
  • Andere Projekte implementieren diese Services und veröffentlichen die Implementierungen im META-INF/services-Verzeichnis
  • Projekte benutzen die Services und fügen so der Anwendung weitere Fähigkeiten hinzu
  • Ein „Sammelprojekt“ enthält die JARs aller Teilprojekte und den „Startcode“, der eine von allen Teilprojekten zu implementierende „init“-Methode aufruft (im Beispiel oben die Methode initModule() des Services Module, „Abholen“ der Implementierungen mit ServiceLookup.lookupAll())

Will ich die Anwendung erweitern, eröffne ich ein neues Projekt, benutze die Services und füge das Projekt-JAR dem Sammelprojekt hinzu. Ich brauche nicht die bisherigen Projekte anzupassen.

Das Beispiel oben könnte ich erweitern: Ein neues Projekt implementiert analog zu ImageFileBrowser über die Module-Schnittstelle beispielsweise eine Klasse Mp3FileBrowser, ein weiteres VideoFileBrowser. Diese Projekte müssen nichts voneinander wissen ebensowenig wie das Fenstersystem wissen muss, welche Fenster mit welcher Funktion ihm hinzugefügt werden.

Stichwörter:

Zu diesem Artikel können keine Kommentare mehr geschrieben werden.