Kernkonzepte von Spring: Beans und Dependency Injection

In diesem Blog-Post besprechen wir die Basics von Spring:
Spring Beans und wie man diese miteinander vernetzt bzw. referenziert (Dependency Injection).

Den kompletten Code zu diesem Blog-Post findet ihr in GitHub:
https://github.com/elmar-brauch/beans

Spring Bean

Spring Beans sind Java Objekte, die durch den Spring IoC Container instanziiert und verwaltet werden. Der IoC (Inversion of Control) Container erstellt Beans anhand einer Bean Definition, die der Entwickler in Form von Annotationen oder xml Konfiguration bereitstellt.

IoC ist ein Umsetzungsparadigma und bedeutet Steuerungsumkehr. Im Kontext des Spring Frameworks versteht man darunter, dass das Framework die Erstellung des Objektnetzes (Beans) anstelle des Entwicklers übernimmt.  

In vorherigen Blog-Artikel haben wir bereits Projekte mit Spring Boot aufgesetzt, siehe z.B. microservices-mit-spring-boot-erstellen.html. In diesen Projekten verwendeten wir die Annotation @SpringBootApplication, welche eine Aggregation diverser anderer Spring Annotationen ist. Somit sorgt @SpringBootApplication dafür, dass der IoC Container gestartet wird, dass nach Bean Definitionen gesucht wird und dass Beans erzeugt und vernetzt werden.

Bean Definitionen können auf verschiedene Weisen erstellt werden:
  • Mittels @Component, @Service und @Repository Annotationen
  • Mittels @Configuration und @Bean Annotationen
  • Mittels XML Konfiguration, z.B. in spring.xml Dateien
  • Mittels einer beliebigen Kombination der zuvor gelisteten Bean Definitionstechniken

@Component, @Service und @Repository

Wenn wir eine eigene Java-Klasse geschrieben haben, die einen Dienst zur Verfügung stellt. Dann  können wir aus dieser Klasse eine Bean machen, indem wir eine der 3 Annotationen @Component, @Service oder @Repository über die Zeile mit dem Schlüsselwort class schreiben, z.B. so:
    @Service
    public class ItemServiceImpl ...

Schaut man sich die Implementierung der 3 Annotationen an, so sieht man, dass @Repository und @Service jeweils @Component verwenden - sie sind ein Alias für @Component.
Ich verwende die 3 Annotationen, um auszudrücken welche Rolle die Klasse bzw. Bean in meinem Programm spielt:
  • @Repository verwende ich, wenn die Bean Daten speichert und verwaltet. Im Blog-Post keine-ahnung-von-mongodb-dann-nimm.html haben wir gesehen, dass die Annotation @Repository verwendet wurde, um die direkte Interaktion mit der Datenbank zu kapseln.
  • @Service verwende ich, wenn die Bean einen Service bereitstellt, der potentiell von anderen Java-Projekten verwendet werden könnte oder wenn es gut möglich ist, dass sich die Implementierung des Services ändert. Daher sollte auch jeder Service immer ein Interface implementieren.
  • @Component verwende ich, wenn die Bean eine interne Komponente ist, die nur im Java-Projekt selbst verwendet wird und nicht nach außen bereitgestellt werden soll.
Im folgenden Schaubild werden die Services und Komponenten unseres Code-Beispiels gezeigt:
  • Es gibt 2 Services ItemServiceImpl und InMemoryItemStore, beide bekommen im Code die Annontation @Service.
  • Das Interface ItemService wird von unserer mit @Service annotierten Klasse ItemServiceImpl implementiert. Um Interface und Implementierung besser unterscheiden zu können, wird häufig am Klassennamen der Implementierung die Endung "Impl" angehängt.
  • Das Interface ItemPersistenceService wird von der Klasse InMemoryItemStore implementiert. Hier habe ich mich gegen eine gleichnamige Implementierungsklasse (ItemPersistenceServiceImpl) entschieden, da die Wahrscheinlichkeit sehr groß ist, dass der InMemoryItemStore durch eine Klasse ersetzt wird, die in einer Datenbank speichert.
    Hier zeigt sich der große Vorteil eines Interfaces, wenn wir auf eine Datenbank umstellen, so müssen wir lediglich den InMemoryItemStore Service ersetzen - alle anderen Klassen bleiben unverändert, da ItemServiceImpl das Interface ItemPersistenceService nutzt.
  • IdGenerator soll ein Beispiel für eine Komponente sein. Hier wird die Annotation @Component verwendet und auf die Implementierung eines Interfaces wurde verzichtet. 

Service und Komponenten Beans

Exemplarisch zeige ich hier Ausschnitte aus den Klassen ItemServiceImpl und IdGenerator. Außer der Annotation @Service bzw. @Component wird für die Erzeugung einer Bean nichts weiter benötigt.
Ausschnitt ItemServiceImpl.java:
    import org.springframework.stereotype.Service;

    @Service
    public class ItemServiceImpl implements ItemService {
@Override
public Item createAndPersistItem(String name) 
        ...
    }

Klasse IdGenerator.java:    
    import java.util.UUID;    
    import org.springframework.stereotype.Component;

    @Component
    public class IdGenerator {
public String generateId() {
return UUID.randomUUID().toString();
}
    }

@Configuration und @Bean

Wenn man die Bean Definitionen lieber an einer Stelle im Java Code haben möchte, kann man anstelle von @Component, @Service und @Repository auch die Annotation @Bean verwenden.
Damit @Bean vom Spring IoC Container erkannt wird, muss diese Annotation in einer mit @Configuration annotierten (Konfigurations-)Klasse sein.

Die Beans aus unserem Beispiel könnten also auch so erzeugt werden:

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    import de.bsi.bean.component.IdGenerator;
    import de.bsi.bean.service.InMemoryItemStore;
    import de.bsi.bean.service.ItemPersistenceService;
    import de.bsi.bean.service.ItemService;
    import de.bsi.bean.service.ItemServiceImpl;

    @Configuration
    public class ItemServiceConfig {
     @Bean
public ItemService itemService() {
return new ItemServiceImpl(itemPersistenceService());
}
@Bean(initMethod = "initStore", destroyMethod = "cleanStore")
public ItemPersistenceService itemPersistenceService() {
return new InMemoryItemStore();
}
@Bean(name = "idGenerator_2")
public IdGenerator idGenerator() {
return new IdGenerator();
}
    }

Damit Spring keinen Fehler "Error starting ApplicationContext" loggt und abbricht, müssen wir alle @Service und @Component Annotationen entfernen. Da 2 Beans mit dem gleichen Namen nicht erzeugt werden dürfen.
Alternativ könnten wir auch 2 verschiedene Namen vergeben, so wie es hier gemacht wurde:
@Bean(name = "idGenerator_2") - dann müssen wir aber auch bei der Dependency Injection den Namen und nicht nur das Interface bzw. die Klasse verwenden, siehe nächstes Kapitel.

Eine weitere Besonderheit sieht man in dieser Annotation:
@Bean(initMethod = "initStore", destroyMethod = "cleanStore")
Hier wird eine initMethod definiert, die direkt nach der Instanzierung der Bean ausgeführt wird.
Die
destroyMethod wird am Ende des Lebenszyklus der Bean ausgeführt, z.B. um verwendete Streams zu schließen.
In der zuvor gezeigten Variante mit @Component usw. kann man einzelne Methoden mit @PostConstruct und @PreDestroy annotieren, um den gleichen Effekt zu erzielen.

@Configuration wird häufig eingesetzt, wenn wir Spring Beans konfigurieren wollen deren Implementierung Teil einer 3rd Party Bibliothek ist. Zum Beispiel könnte man in der 3rd Party Bibliothek Spring Data die Id Generierung auf diese Weise verändern. 

Bean Definition in XML

Alles zuvor gezeigten Annotationen kann man auch in XML konfigurieren.
Das würde dann so aussehen:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:c="http://www.springframework.org/schema/c"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean class="de.bsi.bean.service.ItemServiceImpl">
        <constructor-arg ref="itemStore_3"/>
    </bean>
    <bean class="de.bsi.bean.service.InMemoryItemStore"
    name="itemStore_3" 
    init-method="initStore" destroy-method="cleanStore"/>
    <bean class="de.bsi.bean.component.IdGenerator" 
            name="idGenerator_3" />
</beans>

  • Damit eine Bean erzeugt werden kann muss mindestens das Attribut class gesetzt werden.
  • Alle anderen Attribute sind optional bzw. bilden die Bean Definitionen aus dem @Configuration Beispiel nach.
Damit die XML Konfiguration von Spring bzw. Spring Boot ausgewertet wird, muss sie per Annotation bekannt gemacht werden. Das machen wir mit der Annotation @ImportResource z.B. an dieser Stelle:
    
    @SpringBootApplication
    @ComponentScan(useDefaultFilters = false)
    @ImportResource(locations = {"classpath:spring.xml"})
    public class BeanDemoApplication {
public static void main(String[] args) {
    SpringApplication.run(BeanDemoApplication.class, args);
}
    }

  • @ImportResource benötigt noch den Pfad zu den XML Dateien mit den Bean Definitionen. Im locations Attribut kann man eine Liste von Datei-Pfaden angeben.
  • In @ComponentScan habe ich "useDefaultFilters = false" gesetzt, damit der Komponenten-Scan nicht nach Beans sucht, die im Code per Annotation erstellt werden. Wenn man ausschließlich mit XML Bean Definitions arbeitet und keine doppelten Bean Definitionen hat, ist das nicht nötig.
  • Ansonsten entspricht der zuvor gezeigte Code-Ausschnitt dem Spring Boot Standard zum Starten der Applikation.
Bean Definitionen in XML waren die erste Option, die das Spring Framework angeboten hat. Bean Definitionen per Annotation in Java sind die bessere Variante, da der Java Compiler die Typ-Sicherheit prüft. Außerdem sind die beiden Annotations-Varianten kompakter. Daher erstelle ich (freiwillig) keine Bean Definitionen in XML.

Dependency Injection

In unserem Schaubild gibt es außer Klassen und Interfaces auch noch Abhängigkeiten (dargestellt durch  UML Komposition mit @Autowired Beschriftung). Hier noch einmal das Schaubild:


  • Damit die ItemServiceImpl Bean funktioniert, muss sie auf eine ItemPersistenceService Bean zugreifen können.
  • Die InMemoryItemStore Bean benötigt Zugriff auf eine IdGenerator Bean.

Field Injection

Diese beiden Abhängigkeiten kann Spring für uns verwalten. Spring injiziert den Beans jeweils die benötigte Bean - das wird als Dependency Injection bezeichnet.
Konkret sieht das z.B. für InMemoryItemStore so aus:
    
  import org.springframework.beans.factory.annotation.Autowired;

  @Service
  public class InMemoryItemStore implements ItemPersistenceService {

    @Autowired private IdGenerator idGenerator;
    ...
 
Die @Autowired Annotation zeigt Spring an, dass die @Service Bean InMemoryItemStore die Injektion der Bean IdGenerator benötigt. Wenn Sping im IoC Container nun eine Bean vom Typ IdGenerator findet, so setzt Spring automatisch im Attribut idGenerator eine Referenz auf diese Bean.
Spring macht das mit Reflection (siehe z.B. Java 7 - Mehr als eine Insel), daher wird auch keine Setter-Methode für idGenerator benötigt.
Diese Form der Dependency Injection wird als Field Injection bezeichnet.

Falls es mehrere Beans gibt, die das gleiche Interface implementieren, müssen wir eine bestimmte Bean über ihren Namen auswählen. Dazu können wir zusätzlich zum @Autowired Tag noch das @Qualifier Tag setzen und übergeben den Namen der Bean als Parameter: 
        
    @Autowired 
    @Qualifier("idGenerator_3")
    private IdGenerator idGenerator;

Für den Fall, dass die benötigte Bean nicht im Spring IoC Container gefunden wird, bleib das mit @Autowird annotierte Attribut (Field) null. Deshalb ist die Constructor Injection der Field Injection vorzuziehen, da sie sicherstellt, dass Beans nur dann erzeugt werden, wenn alle Abhängigkeiten injiziert werden können.

Constructor Injection

Injektion per Konstruktor sehen wir z.B. in der Bean ItemServiceImpl:

  @Service
  public class ItemServiceImpl implements ItemService {
    private ItemPersistenceService persistenceService;
    @Autowired
    public ItemServiceImpl(ItemPersistenceService service) {
      this.persistenceService = service;
    }

Hier befindet sich die @Autowired Annotation direkt am Konstruktor. Die Klasse ItemServiceImpl hat nur diesen einen Konstruktor. Daher kann sie nur dann instanziiert werden, wenn eine Bean gefunden wird, die das Interface ItemPersistenceService implementiert.

Method Injection

Funktioniert analog zu den anderen Formen der Dependency Injection. Hier wird die @Autowired Annotation an eine Methode in der Bean geschrieben:

  @Service
  public class ItemServiceImpl implements ItemService {
    private ItemPersistenceService persistenceService;
    @Autowired
    public void setItemPersistenceService(
        ItemPersistenceService service) {
      this.persistenceService = service;
    } 

Bean Scopes: Singleton versus Prototype

Jede Spring Bean hat einen Gültigkeitsbereich (Scope) in dem sie benutzt wird.
Spring hat drei Arten von Scopes:
  • Singleton
  • Prototype
  • Web Aware Scopes, die hier nicht weiter betrachtet werden, weil sie nur in einem web-aware ApplicationContext verfügbar sind.

Singleton 

Singleton ist der Default Scope von jeder Bean. Alle Beans, die wir bisher in diesem Blog-Post gesehen haben, sind also Singletons.

Singleton ist der Name eines Design Patterns. Zusammengefasst geht es beim Singleton darum, dass eine Singleton Klasse nur ein einziges Mal instanziiert werden kann. An allen anderen Stellen wird dann auf ein und dieselbe Instanz des Singletons zugegriffen. Weitere Infos zum Singleton gibt es z.B. in diesem Buch: Head First: Design Patterns 

Man kann den Singleton Scope auch explizit setzen, das sieht dann so aus:

  import org.springframework.context.annotation.Scope;

  @Component
  @Scope("singleton")
  public class IdGenerator ...

Prototype

Scope Prototype ist die Alternative zum Singleton und muss explizit gesetzt. Das funktioniert analog:
  @Component
  @Scope("prototype")
  public class IdGenerator ...

Jeder Anfrage beim Spring IoC Container nach einer bestimmen Bean mit Scope Prototype wird mit einer Referenz zu einer neuen Instanz dieser Bean beantwortet.
(Beim Singleton wäre die Antwort immer die Referenz auf ein und dieselbe Bean.)

In meinem GitHub-Repository findet ihr in der main-Methode einen kleinen Test des Prototype Scopes:
https://github.com/elmar-brauch/beans 
Dort wird gezeigt, dass jeder Aufruf von 
var idGenerator = context.getBean(IdGenerator.class);
eine neue Instanz in der Variable idGenerator bekommt.

Fazit

In diesem Blog-Post haben wir gelernt was Spring Beans sind. Beans werden in XML oder per Annotation definieret - das Spring Framework erstellt sie dann automatisch.
Wir haben 3 Varianten von Dependency Injection kennengelernt und gesehen wie Spring benötigte Beans bereitstellt bzw. injiziert.
Außerdem haben wir die Bean Scopes Singleton und Prototype gesehen.

Für weitere Details zu Spring verweise ich auf https://spring.io/
und auf dieses sehr umfangreiche Nachschlagewerk zu Spring 5:

Kommentare

Beliebte Posts aus diesem Blog

OpenID Connect mit Spring Boot 3

Authentifizierung in Web-Anwendungen mit Spring Security 6

Reaktive REST-Webservices mit Spring WebFlux