Eine Einführung in CDI (Contexts and Dependency Injection) in Java

1. Überblick

CDI (Contexts and Dependency Injection) ist ein in Java EE 6 und höher enthaltenes Standard-Framework https://en.wikipedia.org/wiki/Dependency__injection ().

  • Es ermöglicht uns, den Lebenszyklus von stateful-Komponenten über domänenspezifische Lebenszyklus-Kontexte zu verwalten und Komponenten (Services) typsicher in Client-Objekte einzufügen. **

In diesem Lernprogramm werden die wichtigsten Funktionen von CDI eingehend untersucht und verschiedene Ansätze zum Einfügen von Abhängigkeiten in Clientklassen implementiert.

2. DYDI (Do-it-Yourself Abhängigkeitsinjektion)

Kurz gesagt, es ist möglich, DI zu implementieren, ohne auf irgendwelche Rahmenbedingungen zurückgreifen zu müssen.

Dieser Ansatz wird allgemein als DYDI (Do-it-Yourself Dependency Injection) bezeichnet.

Mit DYDI halten wir den Anwendungscode von der Objekterstellung isoliert, indem wir die erforderlichen Abhängigkeiten an die Client-Klassen durch einfache alte Fabriken/Builder übergeben.

So könnte eine grundlegende DYDI-Implementierung aussehen:

public interface TextService {
    String doSomethingWithText(String text);
    String doSomethingElseWithText(String text);
}
public class SpecializedTextService implements TextService { ... }
public class TextClass {
    private TextService textService;

   //constructor
}
public class TextClassFactory {

    public TextClass getTextClass() {
        return new TextClass(new SpecializedTextService();
    }
}

Natürlich ist DYDI für einige relativ einfache Anwendungsfälle geeignet.

Wenn unsere Beispielanwendung an Größe und Komplexität zunimmt und ein größeres Netzwerk von miteinander verbundenen Objekten implementiert wird, würden wir sie mit Tonnen von Objektgraphiefabriken belasten.

Dies würde viel Boilerplate-Code erfordern, nur um Objektdiagramme zu erstellen. Dies ist keine vollständig skalierbare Lösung.

Können wir DI besser machen? Natürlich können wir. Genau hier kommt CDI ins Spiel.

** 3. Ein einfaches Beispiel

  • CDI macht DI zu einem unkomplizierten Prozess, der darauf ausgelegt ist, die Serviceklassen mit ein paar einfachen Anmerkungen zu dekorieren und die entsprechenden Einspritzpunkte in den Clientklassen zu definieren. **

Um zu zeigen, wie CDI DI auf der grundlegendsten Ebene implementiert, nehmen wir an, wir möchten eine einfache Bildbearbeitungsanwendung entwickeln. Kann Bilddateien öffnen, bearbeiten, schreiben, speichern usw.

3.1. Die "beans.xml" Datei

Als Erstes müssen wir eine Beans.xml Datei in den Ordner "src/main/resources/META-INF/" ablegen. Auch wenn diese Datei überhaupt keine bestimmten DI-Anweisungen enthält, ist sie für die Inbetriebnahme von CDI erforderlich :

<beans xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
  http://java.sun.com/xml/ns/javaee/beans__1__0.xsd">
</beans>

3.2. Die Serviceklassen

Als Nächstes erstellen wir die Serviceklassen, die die oben genannten Operationen für GIF-, JPG- und PNG-Dateien ausführen:

public interface ImageFileEditor {
    String openFile(String fileName);
    String editFile(String fileName);
    String writeFile(String fileName);
    String saveFile(String fileName);
}
public class GifFileEditor implements ImageFileEditor {

    @Override
    public String openFile(String fileName) {
        return "Opening GIF file " + fileName;
    }

    @Override
    public String editFile(String fileName) {
      return "Editing GIF file " + fileName;
    }

    @Override
    public String writeFile(String fileName) {
        return "Writing GIF file " + fileName;
    }

    @Override
    public String saveFile(String fileName) {
        return "Saving GIF file " + fileName;
    }
}
public class JpgFileEditor implements ImageFileEditor {
   //JPG-specific implementations for openFile()/editFile()/writeFile()/saveFile()
    ...
}
public class PngFileEditor implements ImageFileEditor {
   //PNG-specific implementations for openFile()/editFile()/writeFile()/saveFile()
    ...
}

3.3. Die Kundenklasse

Schließlich implementieren wir eine Client-Klasse, die eine ImageFileEditor -Implementierung im Konstruktor benötigt, und definieren einen Injektionspunkt mit der Annotation @ Inject :

public class ImageFileProcessor {

    private ImageFileEditor imageFileEditor;

    @Inject
    public ImageFileProcessor(ImageFileEditor imageFileEditor) {
        this.imageFileEditor = imageFileEditor;
    }
}

Einfach ausgedrückt ist die Annotation @ Inject das eigentliche Arbeitspferd des CDI. Damit können wir Einspritzpunkte in den Kundenklassen definieren.

In diesem Fall weist @ Inject CDI an, eine ImageFileEditor -Implementierung in den Konstruktor einzufügen.

Darüber hinaus ist es auch möglich, einen Service mithilfe der Annotation @ Inject in Feldern (Feldinjektion) und Setzern (Setterinjektion) einzufügen. Wir werden uns diese Optionen später ansehen.

3.4. Erstellen des ImageFileProcessor -Objektdiagramms mit Weld

Natürlich müssen wir sicherstellen, dass CDI die richtige ImageFileEditor -Implementierung in den ImageFileProcessor -Klassenkonstruktor einfügt.

Dazu sollten wir uns zunächst eine Instanz der Klasse besorgen.

  • Da wir für die Verwendung von CDI keinen Java EE-Anwendungsserver benötigen, werden wir dies mit Weld tun, der CDI-Referenzimplementierung in Java SE

public static void main(String[]args) {
    Weld weld = new Weld();
    WeldContainer container = weld.initialize();
    ImageFileProcessor imageFileProcessor = container.select(ImageFileProcessor.class).get();

    System.out.println(imageFileProcessor.openFile("file1.png"));

    container.shutdown();
}

Hier erstellen wir ein WeldContainer -Objekt, erhalten dann ein ImageFileProcessor -Objekt und rufen schließlich seine openFile () -Methode auf.

Wie erwartet, wird sich CDI beim Ausführen der Anwendung laut mit einer DeploymentException laut beschweren:

Unsatisfied dependencies for type ImageFileEditor with qualifiers @Default at injection point...
  • Wir erhalten diese Ausnahme, weil CDI nicht weiß, welche ImageFileEditor -Implementierung in den ImageFileProcessor -Konstruktor eingefügt werden soll. **

In der Terminologie von CDI wird dies als mehrdeutige Injektionsausnahme bezeichnet.

3.5. Die @ Default - und @ Alternative -Anmerkungen

Diese Mehrdeutigkeit zu lösen ist einfach. CDI kommentiert standardmäßig alle Implementierungen einer Schnittstelle mit der Annotation @ Default .

Wir sollten also explizit angeben, welche Implementierung in die Client-Klasse eingefügt werden soll:

@Alternative
public class GifFileEditor implements ImageFileEditor { ... }
@Alternative
public class JpgFileEditor implements ImageFileEditor { ... }
public class PngFileEditor implements ImageFileEditor { ... }

In diesem Fall haben wir GifFileEditor und JpgFileEditor mit der Annotation @ Alternative kommentiert, sodass CDI jetzt weiß, dass PngFileEditor (standardmäßig mit Annotation @ Default annotiert) die Implementierung ist, die wir einfügen möchten.

Wenn wir die Anwendung erneut ausführen, wird sie diesmal wie erwartet ausgeführt:

Opening PNG file file1.png

Das Annotieren von PngFileEditor mit der Annotation @ Default und das Beibehalten der anderen Implementierungen als Alternativen führen zu demselben Ergebnis.

Dies zeigt, kurz gesagt, wie wir die Laufzeit-Injektion von Implementierungen sehr einfach austauschen können, indem Sie einfach die @ Alternative -Annotationen in den Serviceklassen umschalten.

4. Feldinjektion

CDI unterstützt die Feld- und Setterinjektion aus der Box.

So führen Sie eine Feldinjektion durch ( die Regeln für qualifizierte Services mit den Annotationen @ Default und @ Alternative bleiben unverändert )

@Inject
private final ImageFileEditor imageFileEditor;

5. Setterinjektion

In ähnlicher Weise wird eine Setterinjektion durchgeführt:

@Inject
public void setImageFileEditor(ImageFileEditor imageFileEditor) { ... }

6. Die @ Named -Anmerkung

Bisher haben wir gelernt, wie Sie Einspritzpunkte in Client-Klassen definieren und Services mit den Annotationen @ Inject , @ Default und @ Alternative definieren, die die meisten Anwendungsfälle abdecken.

Trotzdem erlaubt uns CDI auch die Service-Injection mit der Annotation @ Named .

  • Diese Methode bietet eine semantischere Methode zum Einfügen von Services, indem ein Name mit einer Implementierung verbunden wird: **

@Named("GifFileEditor")
public class GifFileEditor implements ImageFileEditor { ... }

@Named("JpgFileEditor")
public class JpgFileEditor implements ImageFileEditor { ... }

@Named("PngFileEditor")
public class PngFileEditor implements ImageFileEditor { ... }

Jetzt sollten wir den Injektionspunkt in der ImageFileProcessor -Klasse so umgestalten, dass er mit einer benannten Implementierung übereinstimmt:

@Inject
public ImageFileProcessor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

Es ist auch möglich, Feld- und Setter-Injection mit benannten Implementierungen durchzuführen, was der Verwendung der Annotationen @ Default und @ Alternative sehr ähnlich ist:

@Inject
private final @Named("PngFileEditor") ImageFileEditor imageFileEditor;

@Inject
public void setImageFileEditor(@Named("PngFileEditor") ImageFileEditor imageFileEditor) { ... }

7. Die @ Produces -Anmerkung

Manchmal erfordert ein Dienst, dass einige Konfigurationen vollständig initialisiert werden, bevor er für die Verarbeitung zusätzlicher Abhängigkeiten eingefügt wird.

CDI unterstützt diese Situationen durch die Annotation @ Produces .

  • @ Produces ermöglicht es uns, Factory-Klassen zu implementieren, deren Verantwortung in der Erstellung vollständig initialisierter Services liegt. **

Um zu verstehen, wie die Annotation @ Produces funktioniert, lass uns die ImageFileProcessor -Klasse umgestalten, so dass ein zusätzlicher TimeLogger -Service im Konstruktor benötigt wird.

Der Dienst wird zum Protokollieren der Zeit verwendet, zu der eine bestimmte Bilddateioperation ausgeführt wird:

@Inject
public ImageFileProcessor(ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... }

public String openFile(String fileName) {
    return imageFileEditor.openFile(fileName) + " at: " + timeLogger.getTime();
}
    //additional image file methods

In diesem Fall benötigt die TimeLogger -Klasse zwei zusätzliche Dienste, SimpleDateFormat und Calendar :

public class TimeLogger {

    private SimpleDateFormat dateFormat;
    private Calendar calendar;

   //constructors

    public String getTime() {
        return dateFormat.format(calendar.getTime());
    }
}

Wie können wir dem CDI mitteilen, wohin er schauen soll, um ein vollständig initialisiertes TimeLogger -Objekt zu erhalten?

Wir erstellen einfach eine TimeLogger -Factory-Klasse und kommentieren ihre Factory-Methode mit der Annotation @ Produces :

public class TimeLoggerFactory {

    @Produces
    public TimeLogger getTimeLogger() {
        return new TimeLogger(new SimpleDateFormat("HH:mm"), Calendar.getInstance());
    }
}

Wenn wir eine ImageFileProcessor -Instanz erhalten, durchsucht CDI die TimeLoggerFactory -Klasse, ruft dann die getTimeLogger () -Methode (wie mit der Annotation @ Produces annotiert) auf und injiziert schließlich den Time Logger -Dienst.

Wenn wir die refactored-Beispielanwendung mit Weld ausführen, wird Folgendes ausgegeben:

Opening PNG file file1.png at: 17:46

8. Benutzerdefinierte Qualifikanten

CDI unterstützt die Verwendung von benutzerdefinierten Qualifikationsmerkmalen für das Bestimmen von Abhängigkeiten und das Lösen mehrdeutiger Injektionspunkte.

Benutzerdefinierte Qualifier sind eine sehr leistungsfähige Funktion. Sie binden nicht nur einen semantischen Namen an einen Dienst, sondern auch an Injektionsmetadaten. Metadaten wie https://docs.oracle.com/javase/7/docs/api/java/lang/annotation/RetentionPolicy html[RetentionPolicy]und die legalen Anmerkungsziele ( ElementType ).

Sehen wir uns an, wie Sie benutzerdefinierte Qualifier in unserer Anwendung verwenden:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface GifFileEditorQualifier {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface JpgFileEditorQualifier {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface PngFileEditorQualifier {}

Binden wir nun die benutzerdefinierten Qualifizierer an die ImageFileEditor -Implementierungen:

@GifFileEditorQualifier
public class GifFileEditor implements ImageFileEditor { ... }
@JpgFileEditorQualifier
public class JpgFileEditor implements ImageFileEditor { ... }
@PngFileEditorQualifier
public class PngFileEditor implements ImageFileEditor { ... }

Als letztes wollen wir den Injektionspunkt in der ImageFileProcessor -Klasse _ ändern: _

@Inject
public ImageFileProcessor(@PngFileEditorQualifier ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... }

Wenn wir unsere Anwendung erneut ausführen, sollte dieselbe Ausgabe wie oben gezeigt generiert werden.

Benutzerdefinierte Qualifikationsmerkmale bieten einen sauberen semantischen Ansatz für die Bindung von Namen und Annotationsmetadaten an Implementierungen.

Darüber hinaus ermöglichen benutzerdefinierte Qualifikationsmerkmale die Definition restriktiverer typsicherer Injektionspunkte (übertreffen die Funktionalität der Annotationen @Default und @Alternative) .

  • Wenn nur ein Subtyp in einer Typhierarchie qualifiziert ist, wird CDI nur den Subtyp und nicht den Basistyp einfügen. **

9. Fazit

Ohne Frage macht CDI die Abhängigkeitsinjektion zu einem Kinderspiel , die Kosten für die zusätzlichen Anmerkungen sind sehr geringer Aufwand für den Gewinn der organisierten Abhängigkeitsinjektion.

Es gibt Zeiten, in denen DYDI noch immer über CDI verfügt. Wie bei der Entwicklung relativ einfacher Anwendungen, die nur einfache Objektdiagramme enthalten.

Wie immer sind alle in diesem Artikel gezeigten Codebeispiele verfügbar: over auf GitHub .