Einführung in die Inversion der Steuerung und die Abhängigkeitsinjektion mit Feder

1. Übersicht

In diesem Artikel werden die Konzepte von IoC (Inversion of Control) und DI (Dependency Injection) vorgestellt und anschließend untersucht, wie diese im Spring-Framework implementiert werden.

2. Was ist Umkehrung der Kontrolle?

Inversion of Control ist ein Prinzip in der Softwareentwicklung, mit dem die Steuerung von Objekten oder Teilen eines Programms auf einen Container oder ein Framework übertragen wird. Es wird am häufigsten im Rahmen der objektorientierten Programmierung verwendet.

Im Gegensatz zur herkömmlichen Programmierung, bei der unser benutzerdefinierter Code eine Bibliothek aufruft, ermöglicht IoC einem Framework, die Kontrolle über den Programmfluss zu übernehmen und unseren benutzerdefinierten Code aufzurufen. Um dies zu ermöglichen, verwenden Frameworks Abstraktionen mit integriertem Verhalten. Wenn wir unser eigenes Verhalten hinzufügen möchten, müssen wir die Klassen des Frameworks erweitern oder unsere eigenen Klassen einbinden.

Die Vorteile dieser Architektur sind:

  • Entkopplung der Ausführung einer Aufgabe von ihrer Implementierung
  • Dies erleichtert den Wechsel zwischen verschiedenen Implementierungen
  • größere Modularität eines Programms
  • Einfacheres Testen eines Programms durch Isolieren einer Komponente oder Verspotten ihrer Abhängigkeiten und Ermöglichen der Kommunikation von Komponenten über Verträge

Die Inversion der Steuerung kann durch verschiedene Mechanismen erreicht werden, wie z. B.: Strategieentwurfsmuster, Service Locator-Muster, Factory-Muster und Abhängigkeitsinjektion (DI).

Wir werden uns als nächstes DI ansehen.

3. Was ist Abhängigkeitsinjektion?

Die Abhängigkeitsinjektion ist ein Muster zum Implementieren von IoC, wobei das invertierte Steuerelement die Einstellung der Abhängigkeiten des Objekts ist.

Das Verbinden von Objekten mit anderen Objekten oder das „Injizieren“ von Objekten in andere Objekte erfolgt eher durch einen Assembler als durch die Objekte selbst.

So würden Sie in der herkömmlichen Programmierung eine Objektabhängigkeit erstellen:

public class Store { private Item item; public Store() { item = new ItemImpl1(); } }

Im obigen Beispiel müssen wir eine Implementierung der Item- Schnittstelle innerhalb der Store- Klasse selbst instanziieren .

Mit DI können wir das Beispiel neu schreiben, ohne die gewünschte Implementierung von Item anzugeben :

public class Store { private Item item; public Store(Item item) { this.item = item; } }

In den nächsten Abschnitten werden wir sehen, wie wir die Implementierung von Item über Metadaten bereitstellen können .

Sowohl IoC als auch DI sind einfache Konzepte, haben jedoch tiefgreifende Auswirkungen auf die Strukturierung unserer Systeme. Daher sind sie es wert, gut verstanden zu werden.

4. Der Spring IoC Container

Ein IoC-Container ist ein gemeinsames Merkmal von Frameworks, die IoC implementieren.

Im Spring-Framework wird der IoC-Container durch die Schnittstelle ApplicationContext dargestellt . Der Spring-Container ist für die Instanziierung, Konfiguration und Zusammenstellung von Objekten verantwortlich, die als Beans bezeichnet werden , sowie für die Verwaltung ihres Lebenszyklus.

Das Spring-Framework bietet verschiedene Implementierungen der ApplicationContext- Schnittstelle: ClassPathXmlApplicationContext und FileSystemXmlApplicationContext für eigenständige Anwendungen und WebApplicationContext für Webanwendungen.

Zum Zusammenstellen von Beans verwendet der Container Konfigurationsmetadaten, die in Form von XML-Konfigurationen oder Anmerkungen vorliegen können.

Hier ist eine Möglichkeit, einen Container manuell zu instanziieren:

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

Um das Elementattribut im obigen Beispiel festzulegen, können wir Metadaten verwenden. Anschließend liest der Container diese Metadaten und verwendet sie zum Zusammenstellen von Beans zur Laufzeit.

Die Abhängigkeitsinjektion im Frühjahr kann über Konstruktoren, Setter oder Felder erfolgen.

5. Konstruktorbasierte Abhängigkeitsinjektion

Im Fall einer konstruktorbasierten Abhängigkeitsinjektion ruft der Container einen Konstruktor mit Argumenten auf, die jeweils eine Abhängigkeit darstellen, die wir festlegen möchten.

Spring löst jedes Argument hauptsächlich nach Typ auf, gefolgt vom Namen des Attributs und dem Index für die Begriffsklärung. Sehen wir uns die Konfiguration einer Bean und ihre Abhängigkeiten mithilfe von Anmerkungen an:

@Configuration public class AppConfig { @Bean public Item item1() { return new ItemImpl1(); } @Bean public Store store() { return new Store(item1()); } }

Die Annotation @Configuration gibt an, dass die Klasse eine Quelle für Bean-Definitionen ist. Wir können es auch mehreren Konfigurationsklassen hinzufügen.

Die Annotation @Bean wird für eine Methode zum Definieren einer Bean verwendet. Wenn wir keinen benutzerdefinierten Namen angeben, wird der Bean-Name standardmäßig als Methodenname verwendet.

Bei einer Bean mit dem Standard- Singleton- Bereich prüft Spring zunächst, ob eine zwischengespeicherte Instanz der Bean bereits vorhanden ist, und erstellt nur dann eine neue, wenn dies nicht der Fall ist. Wenn wir den Prototypbereich verwenden , gibt der Container für jeden Methodenaufruf eine neue Bean-Instanz zurück.

Eine andere Möglichkeit, die Konfiguration der Beans zu erstellen, ist die XML-Konfiguration:

6. Setter-basierte Abhängigkeitsinjektion

Für Setter-basiertes DI ruft der Container Setter-Methoden unserer Klasse auf, nachdem er einen Konstruktor ohne Argumente oder eine statische Factory-Methode ohne Argumente aufgerufen hat, um die Bean zu instanziieren. Erstellen wir diese Konfiguration mithilfe von Anmerkungen:

@Bean public Store store() { Store store = new Store(); store.setItem(item1()); return store; }

Wir können XML auch für dieselbe Konfiguration von Beans verwenden:

Constructor-based and setter-based types of injection can be combined for the same bean. The Spring documentation recommends using constructor-based injection for mandatory dependencies, and setter-based injection for optional ones.

7. Field-Based Dependency Injection

In case of Field-Based DI, we can inject the dependencies by marking them with an @Autowired annotation:

public class Store { @Autowired private Item item; }

While constructing the Store object, if there's no constructor or setter method to inject the Item bean, the container will use reflection to inject Item into Store.

We can also achieve this using XML configuration.

This approach might look simpler and cleaner but is not recommended to use because it has a few drawbacks such as:

  • This method uses reflection to inject the dependencies, which is costlier than constructor-based or setter-based injection
  • It's really easy to keep adding multiple dependencies using this approach. If you were using constructor injection having multiple arguments would have made us think that the class does more than one thing which can violate the Single Responsibility Principle.

More information on @Autowired annotation can be found in Wiring In Spring article.

8. Autowiring Dependencies

Wiring allows the Spring container to automatically resolve dependencies between collaborating beans by inspecting the beans that have been defined.

There are four modes of autowiring a bean using an XML configuration:

  • no: the default value – this means no autowiring is used for the bean and we have to explicitly name the dependencies
  • byName: autowiring is done based on the name of the property, therefore Spring will look for a bean with the same name as the property that needs to be set
  • byType: similar to the byName autowiring, only based on the type of the property. This means Spring will look for a bean with the same type of the property to set. If there's more than one bean of that type, the framework throws an exception.
  • constructor: autowiring is done based on constructor arguments, meaning Spring will look for beans with the same type as the constructor arguments

For example, let's autowire the item1 bean defined above by type into the store bean:

@Bean(autowire = Autowire.BY_TYPE) public class Store { private Item item; public setItem(Item item){ this.item = item; } }

We can also inject beans using the @Autowired annotation for autowiring by type:

public class Store { @Autowired private Item item; }

If there's more than one bean of the same type, we can use the @Qualifier annotation to reference a bean by name:

public class Store { @Autowired @Qualifier("item1") private Item item; }

Now, let's autowire beans by type through XML configuration:

Next, let's inject a bean named item into the item property of store bean by name through XML:

We can also override the autowiring by defining dependencies explicitly through constructor arguments or setters.

9. Lazy Initialized Beans

By default, the container creates and configures all singleton beans during initialization. To avoid this, you can use the lazy-init attribute with value true on the bean configuration:

As a consequence, the item1 bean will be initialized only when it's first requested, and not at startup. The advantage of this is faster initialization time, but the trade-off is that configuration errors may be discovered only after the bean is requested, which could be several hours or even days after the application has already been running.

10. Conclusion

In this article, we've presented the concepts of inversion of control and dependency injection and exemplified them in the Spring framework.

You can read more about these concepts in Martin Fowler's articles:

  • Inversion von Kontrollcontainern und das Abhängigkeitsinjektionsmuster.
  • Umkehrung der Kontrolle

Weitere Informationen zu den Spring-Implementierungen von IoC und DI finden Sie in der Spring Framework-Referenzdokumentation.