Dependency Injection lässt sich am besten durch "Einbringen von Abhängigkeiten" übersetzen und ist in der Software-Entwicklung ein bekannter Begriff, bei dem es wie bei der Fabrikmethode um die Auslagerung von Konstruktoraufrufen zur Objekterzeugung geht. Die Ziel der Dependency Injection ist es, die Abhängigkeiten zwischen Klassen, die durch import
-Anweisungen entstehen, zu reduzieren, und die Klassen dadurch zu entkoppeln. Lose Kopplung von Klassen (oder Komponenten im Allgemeinen) hat den Vorteil, dass sich Änderungen einfacher durchführen lassen, da diese sich nur lokal auswirken.
Dependency Injection ist kein GoF-Muster. Den Begriff hat Martin Fowler in seinem Artikel "Inversion of Control Containers and the Dependency Injection pattern" geprägt [Fow04]. Er suchte nach einem Begriff für die Entkopplung bei der Objekterzeugung, der sich von dem allgemeinen Begriff Inversion of Control (IoC) (s. Kapitel Spring) abgrenzen lässt. Dependency Injection folgt dem Prinzip der eindeutigen Verantwortlichkeit (engl. Single-Responsibility-Prinzip) [Mar18], das nach Robert Martin besagt, dass jede Klasse in der objekt-orientierten Programmierung nur eine wesentliche Aufgabe erfüllen soll und entsprechend dieser Aufgabe weiterentwickelt wird:
Demzufolge ist die Situation im folgenden Code-Beispiel zu vermeiden: Die Klasse MyClass
verwendet intensiv das Interface MyLogger
, ist aber aufgrund eines direkten Konstruktoraufrufs auch von der konkreten Klasse ConsoleLogger
abhängig, die dieses Interface implementiert. Wenn nun die konkrete Klasse ausgetauscht werden soll, muss die Klasse MyClass
geändert werden, obwohl sich an ihrer eigentlichen Funktionalität nichts geändert hat.
import MyLogger;
import ConsoleLogger; // this dependency should be avoided
class MyClass {
MyLogger logger = new ConsoleLogger();
// ...
}
Dependency Injection hat zum Ziel diese Abhängigkeit aufzulösen. Dies erfolgt im einfachsten Fall über einen Konstruktor oder über eine Setter-Methode.
import MyLogger; // no import of concrete implementation required
class MyClass {
MyLogger logger;
MyClass(MyLogger logger) { this.logger = logger; } // constructor dependency injection
void setLogger(MyLogger logger) { this.logger = logger; } // setter dependency injection
// ...
}
Nun stellt sich die Frage, wer das Objekt erzeugt und von außen der Klasse MyClass
injiziert. Allgemein ist anzustreben, die Konfiguration darüber, welche konkrete Implementierungsklasse je Interface genutzt werden soll, an einer zentralen Stelle zusammenzuführen, z.B. in einer Konfigurationsklasse oder in einer Konfigurationsdatei (z.B. XML, YAML). Eine derartige Konfiguration wird i.d.R. von einem Framework in seiner Funktion als IoC-Container vorgesehen und verwaltet. Die Ansätze je Framework können sich dabei leicht unterscheiden. Im Folgenden werden die Dependency Injection-Ansätze von Spring und Google Guice vorgestellt. Beide nutzen die Java Reflection API, um zu ermitteln, welche konkrete Klasse an ein Interface gebunden werden kann. Guice spricht hier von Binding, während der entsprechende Begriff in Spring Wiring heißt.
Alle Klassen, die mit der Annotation @Component
(aus dem Package org.springframework.stereotype
) oder einer ihrer Spezialisierungen wie @Service
, @Repository
oder @Controller
versehen sind, werden von Spring automatisch als Beans erkannt und registriert. Die registrierten Beans können zur Laufzeit über den Anwendungskontext erreicht werden. In dem folgenden Code-Beispiel soll ein Objekt der Klasse CapsLockConsoleLogger
als Bean an anderen Stellen eingebunden werden können. Daher ist die Klasse als @Component
annotiert (Zeile 1).
@Component
class CapsLockConsoleLogger implements MyLogger {
@Override
public void log(String message) { System.out.println(message.toUpperCase()); }
}
In der folgenden Klasse SpringClient
wird diese Bean in das Attribut MyLogger logger
injiziert (Zeilen 4-5), d.h. genau an dieser Stelle findet die Dependency Injection statt. Obwohl kein Konstruktoraufruf zu sehen ist, kann der Logger später verwendet werden (Zeile 13). Die Annotation @Autowired
sorgt dafür, dass das Framework per Reflection nach einer passenden Bean für das Interface MyLogger
sucht und diese Bean an das Attribut bindet. Wichtig ist, dass genau eine passende Bean gefunden wird – und nicht keine oder mehrere.
Die Klasse SpringClient
ist hier eine einfache Spring Boot-Anwendung, die als solche annotiert ist (Zeile 1) und wie üblich gestartet wird (Zeile 8). Durch den Aufruf von SpringApplication.run()
wird ein ApplicationContext
-Objekt erzeugt, das den Spring IoC-Container repräsentiert und über das als zentraler Anwendungskontext alle Beans verwaltet werden und erreichbar sind.
@SpringBootApplication
class SpringClient { // this example is a Spring Boot application
@Autowired
MyLogger logger;
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(SpringClient.class, args);
}
@Bean
CommandLineRunner run(ApplicationContext ctx) {
return args -> logger.log("Hello World!");
}
}
Wenn mehrere Beans dieselbe Schnittstelle erfüllen, muss über einen qualifizierenden Namen bestimmt werden, welche Bean das Framework injizieren soll. Dazu kann eine Konfigurationsklasse angelegt werden – wie z.B. die folgende Klasse MyConfiguration
, in der die 3 Beans definiert sind (2 vom Typ MyLogger
, 1 vom Typ String
).
@Configuration
class MyConfiguration {
@Bean
MyLogger loggerA() { return new CapsLockConsoleLogger(); }
@Bean
MyLogger loggerB() { return new TimestampConsoleLogger(dateFormat()); }
@Bean
String dateFormat() { return "yyyy-MM-dd HH:mm:ss"; }
}
@Component
class TimestampConsoleLogger implements MyLogger {
SimpleDateFormat dateFormat;
TimestampConsoleLogger(String pattern) { dateFormat = new SimpleDateFormat(pattern); }
@Override
public void log(String message) { System.out.println(dateFormat.format(new Date()) + "\t" + message); }
}
Die Beans können über eine @Qualifier
-Annotation und ihren Methodennamen wie folgt injiziert werden.
@Autowired @Qualifier("loggerA")
MyLogger logger;
Alternativ kann eine Bean über ihren Methodennamen beim Anwendungskontext abgerufen werden.
logger = ctx.getBean("loggerA", MyLogger.class); // request bean from application context
logger.log("Hello World!");
Anstatt in der Konfigurationsklasse MyConfiguration
können die Beans auch in einer entsprechenden XML-Konfigurationsdatei definiert werden, welche dann als Anwendungskontext zu laden oder diesem hinzuzufügen ist.
class SpringXMLConfigClient {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
MyLogger logger = ctx.getBean("loggerA", MyLogger.class);
logger.log("Hello World!");
}
}
Google Guice bietet eine bewährte Alternative für Dependency Injection, falls in einem Projekt bewusst ohne Spring gearbeitet werden soll. Als Guice in 2008 durch Google veröffentlicht wurde, war es das erste Framework, das Dependency Injection in Java mittels Annotationen möglich machte. In Guice wird die Konfigurationsklasse, die Interfaces an konkrete Implementierungsklassen bindet als Modul bezeichnet.
Der folgende Beispiel-Code zeigt das Modul MyModule
, das das Interface MyLogger
an seine Implementierung CapsLockConsoleLogger
bindet.
class MyModule extends AbstractModule { // a Guice configuration module
@Override
protected void configure() {
bind(MyLogger.class).to(CapsLockConsoleLogger.class);
// more bindings follow here ...
}
}
Diese Konfigurationsklasse wird in dem folgenden Code-Beispiel verwendet, um ein Injector
-Objekt zu erzeugen (Zeile 9). Den Objekten, die später über die Methode getInstance
dieses Injector
-Objekts erzeugt werden (Zeile 12), können ihre Abhängigkeiten über die Annotation @Inject
injiziert werden – wie z.B. bei dem Attribut MyLogger logger
(Zeilen 3-4).
class GuiceClient {
@Inject
MyLogger logger;
public static void main(String[] args){
// create an injector based on module configuration
Injector injector = Guice.createInjector(new MyModule());
// create objects using the injector
GuiceClient client = injector.getInstance(GuiceClient.class);
client.logger.log("Hello World!");
}
}
Die gezeigten Code-Beispiele bezüglich Dependency Injection finden sich im Verzeichnis /patterns/dependency-injection des Modul-Repository.