Ein kurzer Überblick über das Framework

Afterburner.fx: Dependency Injection in JavaFX

Afterburner.fx: Dependency Injection in JavaFX

Ein kurzer Überblick über das Framework

Afterburner.fx: Dependency Injection in JavaFX


Adam Bien hat ein kleines Framework namens afterburner.fx entwickelt. Ziel ist die Integration von Dependency Injection I in JavaFX. Wie funktioniert sein Ansatz im Detail? Welche Vorteile ergeben sich aus der Verwendung? Wie kann man sie einsetzen? Und was kann es nicht?

Das JavaFX MVP Framework afterburner.fx von Adam Bien ist ein möglicher Ansatz Dependency Injection in JavaFX zu verwenden. Wir werden uns von der Projektinitialisierung bis hin zu der internen Funktionsweise das Ganze genauer ansehen.

Projektinitialisierung

Zu Beginn steht immer die Projektinitialisierung. In unserem Fall ist das recht einfach, da es lediglich eine minimalistische pom.xml ohne weitere Bibliotheken beinhaltet. Bei Verwendung des JDK8 ist die pom.xml ein wenig kleiner, da JavaFX keine besondere Definition benötigt, wie es bei dem JDK7 notwendig ist.

Convention over Configuration

Der Ansatz bei afterburner.fx ist Convention over Configuration. Das bedeutet, dass eine bestimmte Struktur vorausgesetzt wird. Leider auch wenn es sich lediglich um Platzhalter handelt. Gehen wir im Folgenden davon aus, dass sich alles unter dem Basis-Package org.rapidpm.demo.jaxenter.blog008 befindet (siehe git Repository [1]). Die Original-Anwendung befindet sich innerhalb des SubPackage orig. Die Klasse Main ist die JavaFX-Applikation, in der sich die main-Methode befindet und das Bootstrapping beginnt. Im Sub-Package von orig befindet sich das Package presentation, in dem sich wiederum die einzelnen GUI-Module befinden. In unserem Beispiel gibt es lediglich das Modul demo. Pro GUI-Modul gibt es zwei Klassen die vorausgesetzt werden. Das ist zum einen die Klasse mit der Endung View und zum anderen mit der Endung Presenter. In unserem Modul gibt es also die beiden Klassen DemoView und DemoPresenter. Bei letzterer handelt es sich um einen Controller der in der fxml-Datei angegeben wird. Die korrespondierende fxml-Datei muss sich im selben Package wie die Klasse DemoView befinden. Diese stellt hiermit  ie JavaFX-GUI-Implementierung dar.

Die View – FXMLView

Die View muss von der Klasse FXMLView erben. Der Initialisierungsprozess wird in dem Konstruktor durch den Aufruf der Methode init(Class clazz, String conventionalName) gestartet (Listing 1).

Listing 1

public FXMLView() {
    this.init(getClass(), getFXMLName());
}

private void init(Class clazz, String conventionalName) {
    final URL resource = clazz.getResource(conventionalName);
    String bundleName = getBundleName();
    ResourceBundle bundle = getResourceBundle(bundleName);
    this.loader = new FXMLLoader(resource, bundle);
    this.loader.setControllerFactory(new Callback<Class<? >, Object> () {
        @Override
        public Object call(Class<? > p) {
            return InjectionProvider.instantiatePresenter(p);
        }
    });
    try {
        loader.load();
    } catch (Exception ex) {
        throw new IllegalStateException("Cannot load " 
            + conventionalName, ex);
    }
}

Die Initialisierung beinhaltet das Laden des RessourcenBundles als auch die FXML-Initialisierung mittels FXMLoader. Das Setzen der ControllerFactory ist hier der wichtige Teil in dem das Injizieren stattfindet. Die Injektion selbst wird mittels InjectionProvider.instantiatePresenter(p) erreicht. Zu beachten ist, dass dieses nur innerhalb des Controllers, hier Presenter genannt, stattfindet. Der Presenter selbst bekommt keine Felder injiziert. Hier ist demnach die Verwendung von @Inject nicht möglich.

InjectionProvider – DI per Reflection

Der InjectionProvier ist das Herzstück von afterburner.fx. Der Einstieg findet wie in Listing 1 zu sehen ist, mittels der Methode instantiatePresenter(p) statt. Der prinzipielle Ablauf ist recht einfach und besteht aus folgenden Schritten:

  1. Erzeuge die Instanz
  2. Injiziere die Attribute mit der Annotation @Inject
  3. Rufe die Methode mit der Annotation @Postconstruct auf.

Schritt eins ist recht einfach erledigt. Es wird, wie bei DI üblich, ein default-Konstruktor vorausgesetzt. Die Instanz wird dann mittels clazz.newInstance() erzeugt. Bei Schritt zwei wird es ein wenig aufwendiger. Hier ist zu beachten, dass nicht nur das Attribut selbst injiziert wird, sondern auch alle Attribute die das injizierte Attribut wiederum per @Inject deklariert hat. Es ist also rekursiv vorzugehen (Listing 2). Hier gibt es in der Implementierung allerdings eine Besonderheit. Es wird von jeder Klasse immer nur eine Instanz erzeugt. Es handelt sich damit praktisch immer um Singletons. Werden also zwei Attribute derselben Klasse injiziert, so handelt es sich um dieselbe Referenz. Das allerdings in der gesamten Applikation! Schritt drei ist nichts anderes als die Methode mit der Annotation @Postconstruct auszuführen, auch hier wieder per Reflection.

Listing 2

static Object instantiateModel(Class clazz) {
    Object product = models.get(clazz);
    if (product == null) {
        try {
            product = injectAndInitialize(clazz.newInstance());
            models.put(clazz, product);
        } catch (InstantiationException | IllegalAccessException ex) {
            throw new IllegalStateException(
                "Cannot instantiate view: " + clazz, ex);
        }
    }
    return product;
}

static Object injectAndInitialize(Object product) {
injectMembers(product);
    initialize(product);
    return product;
}

static void injectMembers(final Object instance) {
    Class<? extends Object> aClass = instance.getClass();
    Field[] fields = aClass.getDeclaredFields();
    for (final Field field : fields) {
        if (field.isAnnotationPresent(Inject.class)) {
            Class<? > type = field.getType();
            final Object target = instantiateModel(type);
            AccessController.doPrivileged(new PrivilegedAction() {
                @Override
                public Object run() {
                    boolean wasAccessible = field.isAccessible();
                    try {
                        field.setAccessible(true);
                        field.set(instance, target);
                        return null; // return nothing...
                    } catch (IllegalArgumentException | 
                                IllegalAccessException ex) {
                        throw new IllegalStateException(
                            "Cannot set field: " + field, ex);
                    } finally {
                        field.setAccessible(wasAccessible);
                    }
                }
            });
        }
    }
}
static void initialize(Object instance) {
    invokeMethodWithAnnotation(instance, PostConstruct.class);
}

Fazit

Das Framework afterburner.fx von Adam Bien ist sehr klein. Es besteht derzeit nur aus den beiden Klassen und bietet einem die Möglichkeit in JavaFX sich mittels @Inject-Instanzen in den Controller/Presenter injizieren zu lassen. Allerdings gibt es auch einige Einschränkungen die genau bedacht werden sollten:

  • Keine Scopes: Keine Möglichkeit per Annotation die Lebensdauer einer Instanz zu bestimmen. Das muss selbst verwaltet werden
  • Alle Instanzen sind Singletons auf Applikationsebene
  • Instanzen leben, bis sie durch den Aufruf der Methode forgetAll() terminiert werden. Es wird jeweils noch die Methode mit der Annotation @PreDestroy aufgerufen, die Reihenfolge ist allerdings nicht zu beeinflussen. Es können keine einzelnen Instanzen terminiert werden.
  • Kein Einsatz von Producern möglich
  • Keine Qualifier um zwischen verschiedenen Implementierungen eines Interface unterscheiden zu können.

Das Framework afterburner.fx ist schnell und einfach, wenn die oben genannten Einschränkungen für das Projekt keine Rolle spielen. Als „Lernprojekt“ um die Grundlagen von Di zu verstehen ist es auf jeden Fall sehr gut.

Die Quelltexte zu diesem Text sind unter [1] zu finden. Wer umfangreichere Beispiele zu diesem Thema sehen möchte, dem empfehle einen Blick auf [2].

Aufmacherbild: Online trading concept von Shutterstock / Urheberrecht: svilen_mitkov

Sven Ruppert

Sven Ruppert arbeitet als Principal IT Consultant bei der codecentric AG am Standort München, spricht seit 1996 Java und arbeitet seitdem in nationalen und internationalen Projekten. In seiner Freizeit entwickelt er an seinem Projekt www.rapidpm.org und hält Vorträge bei JUGs.


Weitere Artikel zu diesem Thema