Verwendung von Spring

IFS-Logo Diese Seite ist ein Teil der IsyFact-Standards. Alle Inhalte der Seite, insbesondere Texte und Grafiken, sind urheberrechtlich geschützt. Alle urheberrechtlichen Nutzungs- und Verwertungsrechte liegen beim Bundesverwaltungsamt.

Creative Commons Namensnennung Die Nutzung ist unter den Lizenzbedingungen der Creative Commons Namensnennung 4.0 International gestattet.

Das Spring-Framework ist ein Java EE Framework, welches im Kern sehr verständlich und leicht zu verwenden ist. In ihm werden die Bestandteile eines Systems als Spring-Beans definiert. Neben seiner Kern-Funktionalität der Verwaltung, Konfiguration und aspektorientierten Erweiterung von Beans bietet Spring viele Funktionalitäten, welche die Entwicklung einer Anwendung erleichtern.

Die folgenden Inhalte beschreiben die Nutzung von Spring für die allgemeine Umsetzung von Backends. Die Nutzung spezifischer Features von Spring sind Bestandteile der folgenden Bausteine:

Alle anderen Funktionalitäten von Spring (Validierung über Spring, Emailing, Thread Pooling, Scripting) werden nicht verwendet.

Mit der Verwendung von Spring verspricht man sich eine geringere Komplexität sowie einen geringeren Entwicklungsaufwand von Backends. Die Verwendung unterliegt den folgenden Zielen.

Einheitlichkeit der Nutzung

Spring soll in Backends einheitlich eingesetzt werden. Hierfür sind geeignete Vorgaben für die Nutzung zu etablieren.

Verständlichkeit der Konfiguration

Die Konfiguration der Spring-Komponenten erfolgt über Annotationen an den Komponenten sowie über Konfigurationsklassen.

Komponentenorientierung wahren

Über Spring werden Komponenten konfiguriert. Es soll nicht möglich sein, direkt auf Implementierungsklassen einer Komponente zuzugreifen.

1. Vorgaben zur Konfiguration mit Spring

Das grundlegende Konzept von Spring ist die Spring-Bean. Die Konfiguration von Spring teilt sich in zwei Teile: die Konfiguration der Spring-Beans sowie die Konfiguration von Spring selbst (innerhalb eines Tomcat Servers). Um die Konfiguration von Spring und der eingesetzten Bausteine einfach zu halten, werden die Autokonfigurationsmechanismen von Spring Boot eingesetzt. Die Spring-Konfiguration des Backends ist nach folgenden Vorgaben zu erstellen.

1.1. Konfiguration von Spring-Beans

Spring ist ein Applikations-Container, welcher sogenannte Spring-Beans instanziiert, per Dependency Injection konfiguriert und bereitstellt. Spring-Beans sind beliebige Java-Klassen. Für diese Klassen kann man benötigte andere Spring-Beans oder Konfigurationsparameter konfigurieren, welche der Klasse daraufhin im Konstruktor oder per set-Methode übergeben werden.

Konfiguriert werden Spring-Beans und ihre Abhängigkeiten durch die von Spring bereitgestellte Annotation @Component bzw. durch deren Spezialisierungen (@Repository, @Service, @Contoller, usw.). Diese werden beim Start des Applikations-Containers gescannt und ausgewertet (Component Scanning). Bei der Verwendung von @SpringBootApplication erfolgt das Scannen dieser Komponenten automatisch im gleichen Paket und den Unterpaketen der Applikationsklasse. Generell gilt, dass jede zentrale und wichtige Klasse als Spring-Bean konfiguriert werden sollte.

Für die Modellierung und Konfiguration der Spring-Beans werden im folgenden Vorgaben aufgestellt.

1.1.1. Querschnittliche Funktionalität als Spring-Beans konfigurieren

Querschnittliche Funktionalität (etwa für das Monitoring, für Fehler- und Nachrichtentexte oder für die Versendung von Mails) sind als Spring-Beans zu konfigurieren. Ebenfalls über Spring-Beans durchzuführen ist die Konfiguration diverser Frameworks, z.B. Hibernate. Die Konfiguration dieser Frameworks wird in ihren Nutzungsvorgaben beschrieben.

1.1.2. Beans standardmäßig als Singletons definieren

Spring-Beans können entweder als Singletons mit nur einer Instanz, mit einer Instanz pro Aufruf oder mit einer Instanz pro Abhängigkeit (Prototype) erzeugt werden. Die Spring-Beans einer Anwendung sollen zustandslos sein und werden als Singleton-Beans erzeugt. Wo technisch erforderlich können auch andere Scopes verwendet werden.

Eine genaue Beschreibung der Scopes bietet die offizielle Spring-Dokumentation.

1.2. Konfigurationsmöglichkeiten der Applikationsklasse

Der zentrale Ausgangspunkt für die Spring-Konfiguration ist die Applikationsklasse. Diese wird im Package des Backends (<org>.<domäne>.<anwendung>.<backend>) erstellt. Sie kann auf unterschiedliche Weise konfiguriert werden:

Konfiguration mit @SpringBootApplication:

Listing 1. Applikationsklasse mit @SpringBootApplication
package de.beispiel.ifanwendung;

@SpringBootApplication // same as @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan
public class IsyFactApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(IsyFactApplication.class);
    }
}

Diese Annotation ist äquivalent zur gleichzeitigen Nutzung der Annotationen @EnableAutoConfiguration, @ComponentScan und @SpringBootConfiguration. Die automatische Konfiguration, optionale zusätzliche Konfigurationslogik in der Applikationsklasse und das Component Scanning werden aktiviert. Ist eine feinere Kontrolle bei Anwendungen, welche die @SpringBootApplication-Annotation nutzen, gewünscht, können mit exclude-Attributen Komponenten von der automatischen Konfiguration ausgeschlossen werden.

Konfiguration mit @Configuration und @EnableAutoConfiguration:

Listing 2. Applikationsklasse mit expliziter Konfiguration
package de.beispiel.ifanwendung;

@Configuration
@EnableAutoConfiguration
@Import({ CoreConfig.class, PersistenceConfig.class, ServiceConfig.class })
public class IsyFactApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(IsyFactApplication.class);
    }
}

Mit diesem Vorgehen wird explizite Kontrolle über die Konfiguration und das Component Scanning geboten. Dieses Vorgehen hat Vorteile bei der Isolation von Modulen in einem modularen System und um sicherzustellen, dass die Konfigurationen unabhängig bleiben. Alternativ kann zu diesem Zweck auch @SpringBootConfiguration genutzt werden.

Für jede Schicht wird im Package der Schicht (<org>.<domäne>.<anwendung>.<schicht>) parallel zu den Interfaces der Komponentenschnittstellen eine mit @Configuration annotierte Konfigurationsklasse erstellt. Diese wird zusätzlich mit @ComponentScan annotiert, wenn eine Applikationsklasse mit expliziter Konfiguration verwendet wird. Annotationen, die querschnittliche Aspekte konfigurieren (z.B. @EnableTransactionManagement), werden an die Konfigurationsklasse der inhaltlich passenden Schicht geschrieben.

Listing 3. Konfigurationsklasse des Anwendungskerns
package de.beispiel.if2anwendung.core;

@Configuration
@EnableTransactionManagement
@ComponentScan
public class CoreConfig {

    @Bean
    public BeispielBean beispielBean() {
        // ...
    }
}

Die Spring-Beans der Komponenten werden mit der Annotation @Component bzw. deren Spezialisierungen (@Repository, @Service, @Controller etc.) versehen. Externe Spring-Beans, die nicht annotiert werden können, werden in der Konfigurationsklasse der Schicht per @Bean-Methoden konfiguriert.

1.2.1. Empfehlungen zu Konfigurationsmöglichkeiten der Applikationsklasse

  • Grundsätzlich ist für die Applikationsklasse die @SpringBootApplication-Annotation zu verwenden. So wird die Konfiguration vereinfacht und Boiler-Plate-Code minimiert.

  • Die @Configration-Annotation in der Anwendungsklasse soll genutzt werden, wenn die explizite Kontrolle über die Konfiguration und das Component Scanning notwendig ist, zum Beispiel bei der Isolation von Modulen in einem modularen System und um sicherzustellen, dass Konfigurationen unabhängig bleiben.

  • @SpringBootConfiguration ist für Randfälle zu nutzen. Dies ist zum Beispiel der Fall für spezielle Testkonfigurationen bei denen unerwünschte Autokonfigurationen Tests verlangsamen oder verkomplizieren können.

1.3. webAppRootKey konfigurieren

Der Kontextparameter webAppRootKey muss in der web.xml auf den eindeutigen Namen der Webanwendung gesetzt werden.

Listing 4. Konfiguration des webAppRootKey
<context-param>
  <param-name>webAppRootKey</param-name>
  <param-value>allgemeine-anwendung</param-value>
</context-param>

Hintergrund: Spring speichert standardmäßig den Pfad zum Wurzelverzeichnis der Webanwendung im Webserver in der Kontextvariable webapp.root. Wenn mehrere Anwendungen gleichzeitig in einem Tomcat betrieben werden (bspw. in einer Entwicklungsumgebung), wird dieser Parameter durch die verschiedenen Anwendungen überschrieben. Dies kann zu ungewünschten Seiteneffekten führen. Ist der Kontextparameter webAppRootKey wie im obigen Beispiel gesetzt, wird der Pfad statt im Parameter webapp.root im Parameter allgemeine-anwendung abgelegt. Da jede Webanwendung einen eindeutigen Namen besitzt, und damit einen eigenen Kontextparameter verwendet, wird das Überschreiben vermieden.

2. Vorgaben zur direkten Verwendung von Spring

Neben der Konfiguration der Spring-Beans enthält eine Anwendung in der Regel kaum Abhängigkeiten zu Spring. Da die benötigten Objekte per Dependency Injection konfiguriert werden, müssen diese nach ihrer Konfiguration lediglich verwendet werden.

In einigen Fällen existieren jedoch weitere Abhängigkeiten:

  • Spring-Beans werden per Namen aus dem Anwendungskontext ausgelesen.

  • Spring-Beans werden nicht per Dependency Injection, sondern über statische Methoden bereitgestellt. Hierüber entstehen Abhängigkeiten zur Instanziierungsreihenfolge der Spring-Beans durch Spring.

Für diese Aspekte werden im folgenden Vorgaben aufgestellt.

2.1. Keine Spring-Beans per Namen auslesen

Über den Anwendungskontext könnten Spring-Beans explizit per Namen ausgelesen werden. Dies ist mit einer Ausnahme verboten: Die Namen von Spring-Beans sollen nicht im Anwendungscode verwendet werden. Die Ausnahme gilt für den Zugriff von einem Anwendungskontext auf einen anderen (in Zusammenhang mit dem DispatcherServlet). In diesem Fall ist ein explizites Auslesen nicht zu vermeiden. Auszulesen ist in diesem Fall keine Komponente des Anwendungskerns, sondern eine weitere Schnittstellen-Bean, welche nur für diesen Zweck verwendet wird.

3. Vorgaben zur aspektorientierte Programmierung in Spring

Es ist möglich, für Spring-Beans Funktionalität in Form von Aspekten zu definieren. Ihr Einsatz kann über Pointcuts konfiguriert werden. Pointcuts definieren (etwa über reguläre Ausdrücke) Klassen und Methoden, welche um den Aspekt erweitert werden.

Zu intensive Nutzung kann leicht zu einem schwer durchschaubaren Programmfluss führen. Deshalb soll AOP nur für folgende Bereichen genutzt werden: die Steuerung von Transaktionen, die Überwachung und die Berechtigungsprüfung.

Explizit nicht benutzt werden soll AOP für die Fehlerbehandlung.

Die Verwendung von AOP für andere Zwecke ist nur in begründeten Ausnahmefällen erlaubt.

3.1. AOP für Transaktionssteuerung verwenden

Für die Transaktionssteuerung ist Spring-AOP mit den dafür vorgesehenen Klassen von Spring einzusetzen. Die Umsetzung geschieht im Anwendungskern oder in der Serviceschicht. Zusammengefasst gilt:

  • Instrumentiert werden alle Schnittstellenmethoden des Anwendungskerns.

  • Für jeden Aufruf des Anwendungskerns wird eine Transaktion gestartet.

  • Falls kein Fehler auftritt, wird die Transaktion abgeschlossen (Commit), sonst zurückgerollt (Rollback).

3.2. AOP für Berechtigungsprüfungen verwenden

Die Berechtigungsprüfung wird über Spring-AOP mit den vom Baustein Security angebotenen Annotationen umgesetzt.

3.3. AOP nicht für das Logging von Exceptions verwenden

Sämtliche in einem Backend geworfenen und nicht behandelten Ausnahmen müssen inklusive ihrer Stack-Traces geloggt werden. Geloggt wird dies in den Methoden der Schnittstellen-Beans der Serviceschicht. Hierfür soll Spring-AOP nicht verwendet werden.

Schnittstellen-Beans transformieren die Geschäftsobjekte des Anwendungskerns in Transportobjekte der Schnittstelle. Die Stack-Traces der Exceptions werden dabei nicht übertragen, da diese internen Informationen dem Aufrufer keinen Mehrwert bieten. Für das Logging des Backends selbst sind sie jedoch wertvoll. Statt AOP ist das in der Serviceschicht beheimatete Konstrukt der Exception-Fassade zu verwenden.

4. Vorgaben zur Dependency Injection

Um im Anwendungskern das Single-Responsibility-Prinzip umzusetzen, wird auf das Entwurfsmuster Dependency Injection zurückgegriffen. Dabei werden die Verantwortlichkeiten für den Aufbau des Abhängigkeitsnetzes zwischen den Objekten aus den einzelnen Klassen in eine zentrale Komponente überführt. Anders als bei der herkömmlichen Vorgehensweise in der objektorientierten Programmierung ist bei der Dependency Injection nicht jedes Objekt selbst dafür zuständig, seine Abhängigkeiten (benötigte Objekte und Ressourcen) zu verwalten.

Die verschiedenen Verfahren der Dependency Injection sowie ihre Einsatzszenarien verdeutlicht folgende Übersicht:

Tabelle 1. Vergleich der Verfahren zur Dependency Injection
DI-Methode Einsatzszenario Bemerkung

Constructor-Injection

Für alle Spring-Beans, welche keine dynamische Anpassung zur Laufzeit benötigen.

Kann in Produktiv-Code eingesetzt werden und ist der Standard von Spring.

Field-Injection

Vermeidung von Boilerplate-Code

Darf nicht in Produktiv-Code verwendet werden.

Method-Injection (Setter-Injection)

Flexibles Austauschen von Spring-Beans zur Laufzeit

Nicht immutable. Sollte nur eingesetzt werden, wenn diese Eigenschaft benötigt wird.

Da die Constructor Injection diverse Vorteile gegenüber den beiden anderen Techniken liefert, empfiehlt die IsyFact die Verwendung von Constructor Injection.

4.1. Verwendung von Constructor Injection

Bei der Constructor Injection werden alle Abhängigkeiten einer Klasse über die Konstruktoren von außen injiziert. Dadurch werden automatisch auch die benötigten Abhängigkeiten definiert, welche der Erzeuger des Objektes zur Verfügung stellen muss. Dieses Vorgehen hat den Vorteil, dass alle benötigten Abhängigkeiten in der Initialisierungsphase des Objektes zur Verfügung stehen. Zusätzlich werden durch dieses Verfahren Überprüfungen auf null und die Behandlung von nicht aufgelösten Abhängigkeiten unnötig, da die Abhängigkeiten vorhanden sein müssen.

Constructor Injector hilft ebenfalls bei dem Identifizieren von zu vielen Abhängigkeiten zu einem Objekt. Wenn ein Konstruktor zu viele Argumente aufweist, kann dies ein Zeichen für eine zu große Verantwortlichkeit des Objektes sein. Ist dies der Fall, sollte an dieser Stelle über ein Refactoring nachgedacht werden.

Ein weiterer Vorteil von Constructor Injection ist, dass die injizierten Abhängigkeiten während der Laufzeit nicht veränderbar sind und so Nebenläufigkeiten und Seiteneffekte vermieden werden.

Listing 5. Verwendung von Constructor Injection
@Component
public class SomeBean {

  private final AnotherBean anotherBean;

  public SomeBean(AnotherBean anotherBean) {
    this.anotherBean = anotherBean;
  }

  AnotherBean getAnotherBean() {
    return anotherBean;
  }

}