Was ist die Magie von Spring Boot?

Ein Blick hinter die Kulissen

Spring Boot wurde entwickelt, um uns bei der Entwicklung von Spring-Anwendungen Arbeit und Entscheidungen abzunehmen. In der Praxis funktioniert dies so gut, dass häufig das Wort Magie verwendet wird. Die Verwendung dieses Wortes deutet jedoch auch darauf hin, dass wir nicht mehr wirklich verstehen, was dort vor sich geht. Genau dies möchte ich mit diesem Artikel angehen und dazu einen Einblick geben, wie Spring Boot funktioniert.

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.

...
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>2.2.0.RELEASE</version>
      <packaging>pom</packaging>
      <scope>import</scope>
    </dependency>
  </dependencies>
  ...
</dependencyManagement>
...
Listing 1: Maven Import einer BOM

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.

...
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.2.0.RELEASE</version>
  <relativePath/>
</parent>
...
Listing 2: Spring Boot POM als Vater definieren

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.

...
<dependencies>
  ...
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
  </dependency>
</dependencies>
...
Listing 3: Hinzufügen der Thymeleaf-Abhängigkeit

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.

@Configuration
public class FooConfiguration {
    @Bean
    public Foo foo() {
        return new Foo();
    }
}
Listing 4: Spring Java Konfiguration

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).

@ConfigurationProperties("foo")
public class FooProperties {
    private boolean enabled;
    private String value;
    ...
}
Listing 5: Eigene Konfigurationswerte mit Spring Boot

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.

Fazit

In diesem Artikel haben wir gesehen, wie Spring Boot mit den vier Techniken:

  • Abhängigkeitsmanagement,
  • Starter und Autokonfigurationen,
  • Web-Server und
  • externe Konfiguration

die Effekte erzeugt, die uns manchmal wie Magie vorkommen. Diese Effekte nehmen uns dabei viel Arbeit ab und sorgen dafür, dass sich alle Spring Boot-Anwendungen bezüglich des Standardsetups ähneln. Gleichzeitig erlaubt Spring Boot es uns, bei Bedarf von diesen Standards abzuweichen und das ein oder andere Konzept auch für unseren eigenen Bedarf zu nutzen.

Zwar haben wir auch in diesem Artikel nur etwas von der Oberfläche angekratzt, ich hoffe aber trotzdem, dass dieser Einblick beim alltäglichen Entwickeln mit Spring Boot hilft und bei Bedarf als Startpunkt für einen tieferen Einstieg dient.

TAGS

Comments

Please accept our cookie agreement to see full comments functionality. Read more