Einstieg in Spring Boot, Teil 8 Die Konfiguration von Beans in Spring
15.03.2021
Autor / Redakteur: Dr. Dirk Koller / Stephan Augsten
Der Dependency-Injection-Container bildet das Herz von Spring. Dieser kann verschiedene Komponenten verwalten, darunter natürlich auch Java Beans. Die Konfiguration dieser Beans erfolgt zeitgemäß und typsicher mit Hilfe einer Java-Klasse.
Firmen zum Thema
Unsere Beispiel-Bean hier soll ein DatabasePersister sein, also eine Klasse, die Daten in einer Datenbank abspeichert. Da es in unserer hypothetischen Anwendung verschiedene Speichertypen – wie etwa einen FilesystemPersister oder einen CloudPersister – geben könnte, implementiert die Klasse das Interface Persister mit der persist-Methode:
public interface Persister { public void persist();}
public class DatabasePersister implements Persister {
@Override public void persist() { System.out.println("In DatabasePersister persist"); }}
„Spring Boot“-Tutorial
Bildergalerie mit 20 Bildern
Konfiguration in der Java-Klasse
Der Database-Persistor soll in eine andere Klasse injiziert werden, muss also dem Container bekannt gemacht werden. Dies geschieht in einer Java-Klasse, die durch die Annotation @Configuration gekennzeichnet wird und in der Regel auf Config endet. Intern wird diese Klasse mit @Component gekennzeichnet und somit beim Component-Scanning gefunden und ausgewertet:
@Configurationpublic class PersisterConfig {
}
In der Klasse werden die verschiedenen Beans nun durch Bean-Definitionsmethoden zur Verfügung gestellt und diese mit der Annotation @Bean markiert:
@Configurationpublic class PersisterConfig {
@Bean public Persister databasePersister() { return new DatabasePersister(); }}
Die ebenfalls mögliche Konfiguration von Komponenten mit Hilfe von XML ist heute eher nicht mehr üblich, findet man aber noch oft in bestehenden Projekten.
Injizieren der Beans
Eine solchermaßen konfigurierte Bean kann nun wie gewohnt mit Autowired injiziert werden, beispielsweise in einem Service. Der Service kennt optimalerweise den konkreten Persistor nicht, er arbeitet mit dem Interface und ruft die dort definierte persist-Methode auf:
@Servicepublic class PersisterService {
@Autowired private Persister persister;
public void persistData() { persister.persist(); }}
Der Container sucht nun beim Start in den vorhandenen Komponenten nach einer Bean mit dem passenden Typ, sprich Persistor, und findet den konfigurierten DatabasePersister. Durch die Implementierung des Interfaces ist er ein Persister und wird als Kandidat für die Injektion herangezogen.
Bei Problemen ist es manchmal hilfreich, die Beans im Container auszugeben. Die run-Methode der Klasse SpringApplication in der Main-Klasse gibt einen ApplicationContext zurück, der Zugriff auf die konfigurierten Beans bzw. deren Namen über die Methode getBeanDefinitionNames() ermöglicht:
@SpringBootApplicationpublic class DemoApplication {
private static ApplicationContext applicationContext; public static void main(String[] args) { applicationContext = SpringApplication.run(DemoApplication.class, args);> String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames(); for (String beanName : beanDefinitionNames) { System.out.println(beanName); } }}
In der Konsolenausgabe findet man dann, zusammen mit zahlreichen anderen Beans, den konfigurierten Persister:
...
persisterService
personController
persisterConfig
databasePersister
...
Die Qual der Wahl
Spannend wird es, wenn man ein zweites Bean des gleichen Interfaces-Typs in die Konfiguration aufnimmt, etwa den FilesystemPersister. Er implementiert ebenfalls Persister:
public class FilesystemPersister implements Persister {
@Override public void persist() { System.out.println("In FilesystemPersister persist"); }}
Die Konfiguration in PersisterConfig erfolgt analog:
@Configurationpublic class PersisterConfig {
@Bean public Persister databasePersister() { return new DatabasePersister(); }
@Bean public Persister filesystemPersister() { return new FilesystemPersister(); }}
Beim Injizieren der Bean im Service steht der Container nun vor einem Problem: Welchen der beiden Persistoren soll er injizieren? Spring kann das Dilemma nicht alleine lösen. Die Anwendung startet nicht mehr und meldet sich stattdessen mit einer Fehlermeldung:
Field persister in com.example.demo.PersisterService required a single bean, but 2 were found: - databasePersister: defined by method 'databasePersister' in class path resource [com/example/demo/PersisterConfig.class] - filesystemPersister: defined by method 'filesystemPersister' in class path resource [com/example/demo/PersisterConfig.class]
Lösungsmöglichkeiten für das Problem werden erfreulicherweise auch gleich in der Meldung mitgeliefert:
Action:Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
Mit Hilfe der Annotationen @Primary oder @Qualifier lässt sich der Konflikt auflösen. Die Annotation @Primary an einer(!) der beiden Bean-Definitionsmethoden führt dazu, dass die entsprechende Bean bevorzugt wird. Dazu muss natürlich die Konfigurationsklasse im Zugriff sein. Bei der Verwendung von Fremdbibliotheken ist das nicht immer der Fall.
Eine Alternative ist die Annotation @Qualifier, die am Injektionspunkt, also der nutzenden Klasse angebracht werden kann. Durch die Angabe des Methodennamens wird im PersisterService dem FilesystemPersister der Vorzug gegeben:
@Servicepublic class PersisterService {
@Qualifier("filesystemPersister") @Autowired private Persister persister;
public void persistData() { persister.persist(); }}
„Spring Boot“-Tutorial
Bildergalerie mit 20 Bildern
In einer anderen Klasse wird dagegen vielleicht der DatabasePersister präferiert. Man findet dieses Muster oft bei der Konfiguration von mehreren DataSources.
Im nächsten Teil der Spring Boot-Reihe beleuchten wir die verschiedenen Möglichkeiten in Spring Boot, Beans zu injizieren. Spoiler: Die hier benutzte Field-Injection ist gut geeignet für Blog-Artikel, in der Praxis aber nur zweite Wahl.
(ID:47119037)