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

1. Übersicht

CDI (Contexts and Dependency Injection) ist ein Standard-Framework für die Abhängigkeitsinjektion, das in Java EE 6 und höher enthalten ist.

Es ermöglicht uns, den Lebenszyklus zustandsbehafteter Komponenten über domänenspezifische Lebenszykluskontexte zu verwalten und Komponenten (Dienste) typsicher in Clientobjekte einzufügen.

In diesem Tutorial werden wir uns eingehend mit den wichtigsten Funktionen von CDI befassen und verschiedene Ansätze zum Einfügen von Abhängigkeiten in Clientklassen implementieren.

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

Kurz gesagt, es ist möglich, DI zu implementieren, ohne auf ein Framework zurückzugreifen.

Dieser Ansatz ist im Volksmund als DYDI (Do-it-Yourself Dependency Injection) bekannt.

Mit DYDI halten wir Anwendungscode von der Objekterstellung isoliert, indem wir die erforderlichen Abhängigkeiten über einfache alte Fabriken / Builder an die Clientklassen ü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 eignet sich DYDI für einige relativ einfache Anwendungsfälle.

Wenn unsere Beispielanwendung an Größe und Komplexität zunehmen und ein größeres Netzwerk miteinander verbundener Objekte implementieren würde, würden wir sie am Ende mit Tonnen von Objektgraphenfabriken verschmutzen.

Dies würde eine Menge Boilerplate-Code erfordern, nur um Objektgraphen 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 verwandelt DI in einen einfachen Prozess, bei dem es nur darum geht, die Serviceklassen mit ein paar einfachen Anmerkungen zu dekorieren und die entsprechenden Injektionspunkte in den Clientklassen zu definieren.

Nehmen wir an, wir möchten eine einfache Anwendung zum Bearbeiten von Bilddateien entwickeln, um zu zeigen, wie CDI DI auf der grundlegendsten Ebene implementiert. Kann eine Bilddatei öffnen, bearbeiten, schreiben, speichern und so weiter.

3.1. Die Datei "beans.xml"

Zuerst müssen wir eine Datei "beans.xml" im Ordner "src / main / resources / META-INF /" ablegen. Auch wenn diese Datei überhaupt keine spezifischen DI-Anweisungen enthält, ist sie erforderlich, um CDI zum Laufen zu bringen :

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 Client-Klasse

Lassen Sie uns abschließend eine Client-Klasse implementieren, die eine ImageFileEditor- Implementierung im Konstruktor verwendet, und einen Injektionspunkt mit der Annotation @Inject definieren :

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

Einfach ausgedrückt ist die Annotation @Inject das eigentliche Arbeitspferd von CDI. Es ermöglicht uns, Injektionspunkte in den Client-Klassen zu 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 Dienst mithilfe der Annotation @Inject in Feldern ( Feldinjektion ) und Setzern (Setterinjektion) zu injizieren . 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 zuerst eine Instanz der Klasse erhalten.

Da wir uns bei der Verwendung von CDI nicht auf einen Java EE-Anwendungsserver verlassen, tun wir dies mit Weld, 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 sind wir die Schaffung eines WeldContainer Objekt, dann ein immer ImageFileProcessor Objekt und schließlich seine Berufung openfile () Methode.

Wenn wir die Anwendung ausführen, wird sich CDI erwartungsgemäß lautstark beschweren, indem eine DeploymentException ausgelöst wird:

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 CDI-Terminologie ist dies als mehrdeutige Injektionsausnahme bekannt .

3.5. Die Annotationen @Default und @Alternative

Solving this ambiguity is easy. CDI, by default, annotates all the implementations of an interface with the @Default annotation.

So, we should explicitly tell it which implementation should be injected into the client class:

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

In this case, we've annotated GifFileEditor and JpgFileEditor with the @Alternative annotation, so CDI now knows that PngFileEditor (annotated by default with the @Default annotation) is the implementation that we want to inject.

If we rerun the application, this time it'll be executed as expected:

Opening PNG file file1.png 

Furthermore, annotating PngFileEditor with the @Default annotation and keeping the other implementations as alternatives will produce the same above result.

This shows, in a nutshell, how we can very easily swap the run-time injection of implementations by simply switching the @Alternative annotations in the service classes.

4. Field Injection

CDI supports both field and setter injection out of the box.

Here's how to perform field injection (the rules for qualifying services with the @Default and @Alternative annotations remain the same):

@Inject private final ImageFileEditor imageFileEditor;

5. Setter Injection

Similarly, here's how to do setter injection:

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

6. The @Named Annotation

So far, we've learned how to define injection points in client classes and inject services with the @Inject, @Default , and @Alternative annotations, which cover most of the use cases.

Nevertheless, CDI also allows us to perform service injection with the @Named annotation.

This method provides a more semantic way of injecting services, by binding a meaningful name to an implementation:

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

Now, we should refactor the injection point in the ImageFileProcessor class to match a named implementation:

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

It's also possible to perform field and setter injection with named implementations, which looks very similar to using the @Default and @Alternative annotations:

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

7. The @Produces Annotation

Sometimes, a service requires some configuration to be fully-initialized before it gets injected to handle additional dependencies.

CDI provides support for these situations, through the @Produces annotation.

@Produces allows us to implement factory classes, whose responsibility is the creation of fully-initialized services.

To understand how the @Produces annotation works, let's refactor the ImageFileProcessor class, so it can take an additional TimeLogger service in the constructor.

The service will be used for logging the time at which a certain image file operation is performed:

@Inject public ImageFileProcessor(ImageFileEditor imageFileEditor, TimeLogger timeLogger) { ... } public String openFile(String fileName) { return imageFileEditor.openFile(fileName) + " at: " + timeLogger.getTime(); } // additional image file methods 

In this case, the TimeLogger class takes two additional services, SimpleDateFormat and Calendar:

public class TimeLogger { private SimpleDateFormat dateFormat; private Calendar calendar; // constructors public String getTime() { return dateFormat.format(calendar.getTime()); } }

How do we tell CDI where to look at for getting a fully-initialized TimeLogger object?

We just create a TimeLogger factory class and annotate its factory method with the @Produces annotation:

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

Whenever we get an ImageFileProcessor instance, CDI will scan the TimeLoggerFactory class, then call the getTimeLogger() method (as it's annotated with the @Produces annotation), and finally inject the Time Logger service.

If we run the refactored sample application with Weld, it'll output the following:

Opening PNG file file1.png at: 17:46

8. Custom Qualifiers

CDI supports the use of custom qualifiers for qualifying dependencies and solving ambiguous injection points.

Custom qualifiers are a very powerful feature. They not only bind a semantic name to a service, but they bind injection metadata too. Metadata such as the RetentionPolicy and the legal annotation targets (ElementType).

Let's see how to use custom qualifiers in our application:

@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 {} 

Now, let's bind the custom qualifiers to the ImageFileEditor implementations:

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

Lastly, let's refactor the injection point in the ImageFileProcessor class:

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

If we run our application once again, it should generate the same output shown above.

Custom qualifiers provide a neat semantic approach for binding names and annotation metadata to implementations.

In addition, custom qualifiers allow us to define more restrictive type-safe injection points (outperforming the functionality of the @Default and @Alternative annotations).

If only a subtype is qualified in a type hierarchy, then CDI will only inject the subtype, not the base type.

9. Conclusion

Fraglos macht CDI Dependency Injection ein Kinderspiel , die Kosten der zusätzlichen Anmerkungen sehr wenig Aufwand ist für die Verstärkung der organisierten Dependency Injection.

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

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