Das Wort „Magie“ verwenden wir in der Regel, wenn etwas passiert und wir das, was wir sehen, nicht direkt erklären können. Bei Zaubertricks wird deswegen in der Regel von den beiden Bausteinen „Methode“ und „Effekt“ gesprochen. In der Softwareentwicklung ist das nicht anders. Gerade mächtige Frameworks, die dem Nutzer viel Arbeit abnehmen, wirken, wenn wir die Implementierungsdetails nicht kennen, magisch.
Wir können dies als gegeben akzeptieren und kümmern uns nicht um die Details. Das birgt zwei Gefahren. Erstens können wir in die Situation kommen, Aufgaben nicht mehr so zu lösen, wie vom Framework gedacht. Zweitens stehen wir bei auftretenden Problemen vor einem großen Berg, den wir nur zu einem kleinen Teil verstehen. Die Alternative besteht demnach darin, die Details, zumindest zu einem Teil, zu verstehen und somit das Verständnis für das Framework zu erhöhen.
In diesem Artikel schauen wir uns deshalb vier Effekte von Spring Boot an und, im Gegensatz zu Zaubertricks, betrachten wir auch, mit welcher Methode diese erzeugt werden.
Abhängigkeitsmanagement
Praktisch jede Anwendung besteht neben dem eigenen Code auch aus einer Vielzahl von genutzten Frameworks und Bibliotheken. Schon das Spring Framework besteht aus mehreren Modulen. Hinzu kommen dann noch beispielsweise eine Template-Engine wie Thymeleaf, Hibernate für die Persistenz und weitere.
Vor noch nicht allzu langer Zeit mussten wir all diese Abhängigkeiten manuell herunterladen und verwalten. Diese Aufgabe nehmen uns heute Build-Tools wie Maven oder Gradle ab. Allerdings müssen wir uns auch weiterhin bei jeder Abhängigkeit für eine Version entscheiden. Das Problem besteht nun darin, dass alle Abhängigkeiten zum Schluss erfolgreich zusammenspielen und damit zueinanderpassen oder sich zumindest nicht stören.
Damit wir uns darum nicht selbst kümmern müssen, stellt Spring Boot hier für viele gängige Bibliotheken bereits eine vorgefertigte Liste zur Verfügung. Wir als Endnutzer können uns dann darauf verlassen, dass keine Konflikte auftreten.
Um diese Liste mit Maven zu nutzen, wird das Konzept der
„bill of materials“ (BOM) angewandt. Hierzu definieren wir diese BOM
innerhalb der dependencyManagement
-Sektion der pom.xml mit dem Scope
import
(s. Listing 1). Anschließend müssen wir selbst beim Einbinden von in
der BOM definierten Abhängigkeiten keine Versionsnummer mehr angeben, und auch
bei transitiven Abhängigkeiten werden die dort definierten Versionen genutzt.
Möchten wir noch ein wenig mehr Komfort von Spring Boot erhalten, können wir auch eine dort bereitgestellte POM als Vater definieren (s. Listing 2). Neben den Abhängigkeiten werden nun auch einige Maven Plug-ins vorkonfiguriert.
Um den gleichen Effekt für Gradle zu erzeugen, muss das Spring Boot Gradle Plugin genutzt werden.
Starter und Autokonfigurationen
Angenommen, wir möchten in unserer Anwendung serverseitig HTML generieren und haben uns für die Template-Engine Thymeleaf entschieden. Dank des Abhängigkeitsmanagements müssen wir uns zwar nicht mehr um die Version kümmern. Allerdings reicht es nicht aus, nur eine Abhängigkeit zu definieren, wir müssen Thymeleaf innerhalb unserer Anwendung noch einbinden und dazu konfigurieren.
Hier kommt der zweite Effekt von Spring Boot ins Spiel. Um Thymeleaf
einzubinden, müssen wir nur die passende Abhängigkeit definieren (s. Listing 3).
Die Integration passiert nun automatisch, ohne dass wir im Standardfall etwas
Zusätzliches tun müssen. Wollen wir beispielsweise die View mit dem Namen
index
rendern, müssen wir diese per Konvention in der Datei
src/main/resources/templates/index.html beschreiben.
Um diesen Effekt zu erzeugen, werden die beiden Methoden Starter und Autokonfigurationen genutzt. Ein Starter wird dazu genutzt, die eigene Spring Boot-Anwendung um, primär technische, Funktionalität, wie das Hinzufügen einer Template-Engine, zu erweitern. Hierzu definiert ein solcher Starter alle Abhängigkeiten, die benötigt werden, und besteht selbst aus keiner einzigen Zeile Code oder Konfiguration. Fügen wir nun diesen Starter als Abhängigkeit in unserer Anwendung hinzu, erhalten wir auch dessen Abhängigkeiten. Um den vollen Effekt zu erzeugen, brauchen wir allerdings mit Autokonfiguration noch eine zweite Methode.
Spring basiert im Kern auf dem Prinzip der Dependency Injection (DI),
und bringt hierzu seinen eigenen Inversion of Control (IoC)-Container mit. Seit
Spring 3 ist es möglich, Konfiguration für diesen Container direkt in Java zu
definieren. Hierzu müssen wir eine Klasse mit @Configuration
annotieren und
können anschließend mit @Bean
annotierte Methoden zur Konfiguration nutzen
(s. Listing 4). Wird nun innerhalb unserer Anwendung eine Instanz der Klasse
Foo
benötigt, können wir uns diese per DI injizieren lassen, und zur Erzeugung
wird die Methode foo
aufgerufen.
Mittels Autokonfigurationen ist es nun zusätzlich möglich, dass eine so
definierte Konfiguration nur unter bestimmten Bedingungen ausgewertet wird.
Hierzu wird die Annotation @Conditional
verwendet. Bevor Spring eine Klasse
oder Methode auswertet, die mit dieser Annotation markiert wurde, wird die an
der Annotation definierte Condition
ausgewertet. Nur wenn diese Auswertung
erfolgreich war, wird die Klasse oder Methode weiter berücksichtigt.
@Conditional
und Condition
sind dabei bewusst sehr generisch gehalten, um
darauf aufbauend eigene Abstraktionen zu ermöglichen. Für diverse
Standardprobleme gibt es deshalb bereits weitere fertige Bedingungen und
spezifischere Annotationen, wie beispielsweise @ConditionalOnClass
oder
@ConditionalOnMissingBean
.
Für viele Standardfälle liefert Spring Boot nun bereits fertige Autokonfigurationen mit. Damit diese beim Start der Anwendung gefunden werden, müssen sie zusätzlich mit einem Eintrag in der Datei META-INF/spring.factories bekannt gemacht werden.
Für unseren Anwendungsfall, die Integration von Thymeleaf, wird dabei die
Autokonfiguration ThymeleafAutoConfiguration
mitgeliefert. Diese wird
angezogen, sobald die beiden Klassen TemplateMode
und SpringTemplateEngine
zur Laufzeit vorhanden sind, und fügt anschließend, basierend auf weiteren
Bedingungen, bis zu acht Klassen zum IoC-Container hinzu.
Wichtig hierbei ist, dass viele der mit @Bean
annotierten Methoden zusätzlich
mit @ConditionalOnMissingBean
annotiert sind. Autokonfigurationen werden
grundsätzlich erst nach unseren eigenen Konfigurationen ausgewertet. Dies
ermöglicht es uns, sollte uns die Deklaration einer Bean innerhalb der
Autokonfiguration nicht gefallen, eine eigene Bean vom selben Typ und mit
demselben Namen zu deklarieren und damit den Standardfall zu überschreiben.
In Summe beinhaltet Spring Boot so bereits über 50 offizielle Starter, die von uns genutzt werden können. Diese kümmern sich vor allem um Zugriff auf Datenquellen und diverse Aspekte rund um die Webentwicklung. Autokonfigurationen sind aber nicht alleine Spring Boot selbst vorbehalten, sondern es gibt zusätzliche, von der jeweiligen Community gepflegte Starter.
Sollten auch diese nicht ausreichen, können wir auch eigene entwickeln und anderen Nutzern zur Verfügung stellen. Für diesen Fall gibt es zusätzlich speziellen Testsupport, um zum Beispiel innerhalb von auf JUnit basierten Tests diverse Bedingungen zu „simulieren“ und somit die eigenen Autokonfigurationen ausführlich zu testen.
Web-Server
Klassischerweise wurden Java-Webanwendungen in einen Servlet-Container wie Tomcat oder Jetty deployt. Hierzu haben wir eine WAR-Datei mit unserer Anwendung erzeugt und diese in einen speziellen Ordner des Servers kopiert oder über dessen Web-Interface hochgeladen.
Servlet-Container sind allerdings dafür konzipiert, mehrere Anwendungen parallel zu betreiben, und sollten deshalb den Betriebsaufwand reduzieren. Dies hat den Nachteil, nur eine geringe Isolation zwischen den verschiedenen Anwendungen zu ermöglichen. Hat zum Beispiel eine der Anwendungen ein Speicherleck, führt dies häufig dazu, dass auch die anderen Anwendungen Probleme bekommen. Deswegen hat es sich häufig ergeben, dass pro Anwendung ein eigener Servlet-Container verwendet wird. Wenn nun allerdings jede Anwendung in einen eigenen Servlet-Container deployt wird, dann brauchen wir auch einen Großteil der von diesem zur Verfügung gestellten Features nicht mehr.
Da Servlet-Container auch in Java geschrieben sind, entstand somit die Idee,
diesen direkt als Abhängigkeit mit in die eigene Anwendung zu ziehen und dort in
der main
-Methode zu konfigurieren und starten. Genau dies ermöglicht uns
Spring Boot auch. Sobald wir, direkt oder indirekt, die Abhängigkeit
spring-boot-starter-web
anziehen, wird per Autokonfiguration dafür gesorgt,
dass beim Starten unserer Anwendung intern ein Tomcat konfiguriert und gestartet
wird. Diesen können wir anschließend, im Standard auf Port 8080, erreichen.
Der Schritt, die Anwendung in einen Servlet-Container zu deployen, entfällt
somit und es reicht, unsere Anwendung, auch in Produktion, über die
main
-Methode zu starten.
Neben Tomcat kann auch einer der beiden Servlet-Container Jetty oder Undertow genutzt werden. Wie dies funktioniert, kann an der passenden Stelle in der Dokumentation nachgelesen werden.
Externe Konfiguration
Der vierte und letzte hier vorgestellte Effekt besteht in den
Konfigurationsmöglichkeiten, die uns Spring Boot anbietet. Wollen wir
beispielsweise unsere Anwendung nicht mehr auf dem Port 8080 betreiben, können
wir die Umgebungsvariable SERVER_PORT
, das Argument --server.port
oder das
Property server.port
in der Datei src/main/resources/application.properties
auf einen anderen Wert setzen. Beim Start wird nun dieser Port genutzt.
Insgesamt wird beim Start an 17 definierten Stellen nach Konfigurationswerten gesucht. Ich persönlich merke mir die Reihenfolge damit, dass der beim Start am explizitesten gesetzte Wert gewinnt. Das heißt, die Übergabe als Argument ist expliziter als die Umgebungsvariable und diese ist wiederum explizier als die sich in der JAR-Datei befindliche Konfigurationsdatei.
Im Standard stellt uns Spring Boot bereits
über 1000 Konfigurationswerte zur Verfügung, um die durch
Autokonfigurationen definierten Klassen von außen ändern zu können. Allerdings
ist auch dieses Konzept nicht nur für Spring Boot, sondern auch für uns
verwendbar. Um aus unserer Anwendung auf Konfigurationswerte zuzugreifen, gibt
es zwei Möglichkeiten. Der „klassische“ Weg besteht darin, sich diese Werte,
genau wie andere Abhängigkeiten, injizieren zu lassen. Hierzu nutzen wir einen
Ausdruck in der Spring Expression Language (SpEL) innerhalb der
@Value
-Annotation. Alternativ lässt sich für eigene Konfigurationswerte auch
eine eigene Klasse definieren, die für jeden Wert ein Feld besitzt
(s. Listing 5).
Der Name des zu setzenden Wertes setzt sich aus dem in der
@ConfigurationProperties
definierten Präfix und dem Namen des Felds zusammen.
Als Typ lassen sich neben primitiven auch komplexe Typen nutzen. Zudem kann eine
solche Klasse auch mittels Bean Validation nach dem Start
validiert werden, und fehlerhafte oder fehlende Werte verhindern sofort den
Start der Anwendung.
Der letzte Clou besteht darin, unsere Konfigurationswerte mit
Metadaten zu versehen. Nutzen wir @Value
, muss hierzu manuell,
oder mittels IDE-Unterstützung, eine JSON-Datei erweitert werden. Nutzen wir
eigene Klassen, werden diese Metadaten automatisch per Build-Tool-Plug-in
erzeugt. Der Vorteil dieser Metadaten besteht darin, dass wir anschließend
innerhalb unserer IDE durch Codecompletion unterstützt werden.