# Dependency Injection
Dependency Injection (DI), oder auch Inversion of Control (IoC) genannt, beschreibt allgemein das automatische Bereitstellen von Abhängigkeiten. Man beschreibt also lediglich im Code, welche Klassen / Interfaces man benötigt, ohne festzulegen, wo die Instanzen davon genau herkommen. Das Gegenteil sind zur Compile-Zeit schon komplett ausdefinierte Abhängigkeiten.
Der Vorteil von DI ist, dass Klassen sich nicht explizit um eine Referenz auf ihre externen Abhängigkeiten bemühen müssen – diese werden stattdessen vom Spring IoC Container anhand verschiedener Gesichtspunkte bereitgestellt.
So kann z. B. ein fiktives Interface Database angefordert werden und je nach Umgebung wird dieses dann von einer MysqlDatabase oder einer PostgresqlDatabase Klasse bereitgestellt.
# Spring IoC Container
Beim Start einer Spring (Boot) Application wird u. a. eine BeanFactory (opens new window) angelegt. In dieser werden alle "Beans" (s. u.) gesammelt. Sie ist außerdem dafür zuständig die angeforderten Objekte bereitzustellen und zu instanziieren. Beans können auch selbst benötigte Objekte definieren, welche dann wiederum zunächst automatisch geladen werden.
Anhand von u. a. des Typs und des Namens eines Beans sucht Spring die passende Implementation heraus. Dabei werden auch Typabhängigkeiten beachtet, so erfüllt ein Bean vom Typ SubType die Anfrage nach BaseType, falls SubType von BaseType abgeleitet ist (Superklasse) bzw. BaseType implementiert (Interface).
Weiterführende Details finden sich in der Spring Dokumentation (opens new window).
# @Bean, @Configuration
Welche Beans es gibt, wird durch Metadaten festgelegt. Diese Metadaten können in verschiedenen Formen bereitgestellt werden:
- Annotations
- XML Dateien
- Code (angepasstes Erzeugen der Application)
Am komfortabelsten sind Annotations. Hierbei wird eine Klasse, die Beans bereitstellt, mit einer @Configuration Annotation markiert. In dieser Klasse werden dann dank dem Classpath-Scanning von Spring automatisch Beans gesucht.
Beans an sich sind einfach nur Methoden dieser Klasse, welche mit @Bean markiert sind. Der Methodenname stellt gleichzeitig den Beanname dar (kann in der Annotation aber auch überschrieben werden). Der Rückgabewert der Methode beschreibt den Typ des Beans. Anhand dieser und weiterer Kriterien kann das Bean dann später referenziert und gefunden werden.
Benötigt das Bean selbst wiederum andere Beans, um die entsprechende Instanz bereitzustellen, können diese Abhängigkeiten einfach als Parameter der Methode deklariert werden. Falls mehrere Methoden der Klasse dasselbe Bean benötigen, kann dieses auch im Konstruktor als Property deklariert werden und steht dann der gesamten Klasse zur Verfügung.
Beispiel:
@Configuration
class DatabaseConfiguration(
private val envScanner: EnvironmentScanner
) {
@Bean
fun database(dbOptions: DatabaseOptions) = when {
envScanner.isMysql() -> MysqlDatabase(dbOptions)
envScanner.isPostgres() -> PostgresDatabase(dbOptions)
else -> EmbeddedDatabase()
}
}
An anderer Stelle kann dann einfach durch beispielsweise database: Database das für die aktuelle Umgebung passende Database Objekt angefordert werden.
Configurations sind also Klassen, die über ihre Methoden Beans bereitstellen.
# @Component
Nebst @Bean Methoden in @Configuration Klassen gibt es noch die Möglichkeit eine Klasse direkt als Bean zu definieren. Dies wird mit @Component erledigt und das Ergebnis ist ein ganz normales Bean, dessen Typ der Klasse entspricht:
@Component
class MyClassComponentBean(
private val someOtherBean: OtherBean
) {
fun someMethod() { /* ... */ }
fun someOtherMethod() { /* ... */ }
}
Hier werden die Methoden nicht mit @Bean annotiert, da die Klasse an sich das Bean ist. Der Name des aus der Klasse resultierenden Beans entspricht dem Namen der Klasse, lediglich der Anfangsbuchstabe wird kleingeschrieben (im Beispiel oben wäre der Name also "myClassComponentBean").
TIP
Theoretisch können auch @Bean Methoden innerhalb einer Component verwendet werden. Diese sollten aber besser in einer @Configuration sein, da die Component-Klasse an sich ja als Bean angefordert wird und deren Methoden benutzt werden, da würden Bean-Methoden nur stören.
Spezialisierungen von @Component sind beispielsweise @Service und @Controller, diese können genauso verwendet werden. @Service Klassen werden für Business-Logik benutzt und fordern als Beans meist Repositories für den Datenzugriff an. Sie sind auch die einzige Stelle, die mit den Repositories kommuniziert. @Controller sind meisten Schnittstellen nach draußen, wie @RestController.
# Name, @Qualifier, @Primary
Passen mehrere Beans vom Typ her zu einer angefragten Referenz, weiß Spring nicht, welches Bean genau gemeint ist. Zur Auflösung dieser Problematik gibt es mehrere Möglichkeiten. Eine davon ist der Beanname. Dieser Name darf dabei nur einmal in der gesamten Spring-Application vorkommen.
Hier sind zwei Möglichkeiten ein Bean namens "myBean" zu registrieren:
@Bean
fun myBean() /* ... */
// oder
@Bean("myBean")
fun gimmeBean() /* ... */
Um auf ein Bean mit einem bestimmten Namen zuzugreifen kann @Qualifier verwendet werden:
@Qualifier("myBean") private val theBean: TheType
@Bean
fun someMethod(@Qualifier("myBean") theBean: TheType)
Falls @Qualifier nicht benutzt wurde, wird automatisch der Property- oder Parametername als gesuchter Beanname benutzt. Im obigen Beispiel würde ohne @Qualifier("myBean") also nach einem Bean namens "theBean" gesucht werden.
Eine andere Möglichkeit der Auswahl, falls mehrere passende Beans gefunden werden, ist @Primary. Ist genau eines der passenden Beans mit @Primary annotiert, wird dieses genommen.
TIP
Gibt es mehrere zum Typ TheType passende Beans und man möchte tatsächlich alle haben, kann man auch eine Collection angeben:
things: Set<TheType>
// oder
things: List<TheType>
# Erweiterte Features
Hier werden noch ein paar tiefergehende Sachen erläutert, die aber für den Einstieg und die meiste Logik nicht relevant sind.
# Optional, @Conditional*
Ist ein Bean nicht unbedingt erforderlich (möchte man also prüfen ob es existiert oder nicht) kann der gesuchte Typ auch als nullable spezifiziert werden:
theBean: TheType?
Existiert kein passendes Bean übergibt Spring hier null. Für die konditionale Bereitstellung von Beans anhand des (Nicht-)Vorhandenseins anderer Beans, gibt es auch eine deklarative Möglichkeit:
@ConditionalOnMissingBean
@Bean
fun BeanType myBean() /* ... */
Hier würde myBean nur registriert werden, falls es noch kein Bean vom Typ BeanType gibt. Dies wird z.B. von Spring Web benutzt, um ein Standard Fehlerseiten-Bean zu registrieren, falls sonst keins definiert wurde.
TIP
Falls nicht nach dem Rückgabetyp der Methode, sondern einem anderen Typ geschaut werden soll, kann dieser in der Annotation angegeben werden:
@ConditionalOnMissingBean(ErrorController::class)
WARNING
Bei @ConditionalOnMissingBean gilt es ein paar Sachen zu beachten. So ist beispielsweise die Reihenfolge der Bean-Definitionen relevant. Es kann sonst zu einer BeanDefinitionOverrideException kommen.
Es gibt noch viele weitere @Conditional* Annotations – man kann auch eigene definieren. Hier sind noch ein paar Beispiele von mitgelieferten Conditionals:
// nur falls com.batix.feature.enabled=true (oder sonstiger Wert, außer false)
@ConditionalOnProperty("com.batix.feature.enabled")
// nur falls com.batix.error-mode=mayhem
@ConditionalOnProperty(prefix = "com.batix", name = ["error-mode"], havingValue = "mayhem")
// nur wenn die Application in Kubernetes läuft
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
// falls eine bestimmte Klasse im Classpath verfügbar ist, also geladen werden kann
@ConditionalOnClass(EmbeddedWebServer::class)
// falls eine bestimmte Klasse nicht verfügbar ist
@ConditionalOnMissingClass("com.company.webserver.Embedded")
# @Profile
Eine Spring Anwendung kann in verschiedenen Profilen laufen. Diese können beliebig definiert und kombiniert werden. Was diese dabei genau bedeuten, ist dem Programmierer überlassen. Beispiele für Profile könnten z. B. sein:
- dev
- staging
- prod
- embedded
- loadbalancer
@Component Klassen können mit @Profile annotiert werden, um deren Beans nur in bestimmten Profilen zur Verfügung zu stellen.
WARNING
Profilspezifische Beans können schnell sehr komplex werden, sodass man gedanklich nicht mehr hinterherkommt, wann welche Beans verfügbar sind und wann nicht.
Als Alternative bieten sich stattdessen profilspezifische Application-Properties an, die je nach Profil bestimmte Werte überschreiben oder überhaupt erst definieren.
# Scope
Standardmäßig wird ein Bean in der Laufzeit einer Spring Application nur ein einziges Mal instanziiert und dieselbe Instanz für alle Anfragen nach dem Bean wiederverwendet. Das ist der sogenannte Singleton Scope. Dieser ist für die meisten Sachen auch sinnvoll, denn es wird fast immer nur z.B. eine Datenbank-Verbindung oder ein User-Service benötigt.
Daneben gibt es den Prototype Scope. Hier wird jedem, der nach einem Bean fragt, eine frische Instanz dieses Beans gegeben. Dieser Scope wird aber selten genutzt. In Spring Web Anwendungen gibt es außerdem noch die Scopes Request, Session, Application und WebSocket.
Um den Scope eines Beans zu ändern, kann z.B. @Scope("prototype") benutzt werden.