Verwendung von Spring
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.
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:
-
Konfiguration der Persistenzschicht: beschrieben in den Nutzungsvorgaben JPA/Hibernate,
-
Konfiguration des Logging: beschrieben in den Nutzungsvorgaben Logging,
-
Bereitstellung von Metriken zur Überwachung: beschrieben im Konzept Überwachung.
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
:
@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
:
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.
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.
<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:
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.
@Component
public class SomeBean {
private final AnotherBean anotherBean;
public SomeBean(AnotherBean anotherBean) {
this.anotherBean = anotherBean;
}
AnotherBean getAnotherBean() {
return anotherBean;
}
}