Spring Boot: Die Magie verstehen

Teil 1: Auto Configuration begreifen und selbst erstellen
27
Jul

Spring Boot Auto Configuration

Spring Boot hat mich schon immer fasziniert. Kein Herumärgern mehr mit verschiedenen Versionen von Application Servern und unterschiedlichen Umgebungen zwischen lokaler Entwicklung, Test und Produktion. Beschäftigt man sich mit Spring Boot, stößt man schnell auf den Begriff Auto Configuration – ein Begriff, der von den meisten mit „Spring Boot macht das schon irgendwie richtig“ interpretiert wird. Aber was genau ist denn richtig?

Wie entscheidet Spring Boot, dass die Infrastruktur für einen REST Service oder eine Verbindung zu einer MongoDB benötigt wird? Werfen wir einen Blick hinter die Kulissen des Auto-Configuration-Konzepts und betrachten im Anschluss, wie eigene Auto Configurations erstellt werden können.

Artikelserie

Teil 1: Auto Configuration begreifen und selbst erstellen

Teil 2: Externalized Configuration

Typischerweise werden Spring Boot und die Features in Form von Tutorials erklärt, in denen in einer Klasse die komplette Infrastruktur für einen REST Service bereitgestellt wird. Listing 1 zeigt ein solches Beispiel.

Listing 1: Einfaches Spring-Boot-Beispiel

@SpringBootApplication
@RestController
public class SimpleWebApplication {

  @GetMapping("/hello")
  public String getHello() {
    return "Hello World!";
  }

  public static void main(String[] args) {
    SpringApplication.run(SimpleWebApplication.class, args);
  }
}

Natürlich ist es kein guter Stil, den REST-Controller-Code direkt in die Startklasse zu integrieren. Dennoch ist es ein eindrucksvolles Beispiel für die Mächtigkeit und gleichzeitig den Komfort von Spring Boot. Was Spring Boot hierbei leistet, ist durchaus bemerkenswert. Denn basierend auf dem Code in Listing 1 sowie den Abhängigkeiten des Projekts muss Spring Boot die notwendige Infrastruktur erkennen, konfigurieren und bereitstellen. In diesem speziellen Fall einen Web Application Server, das Marshalling und Unmarshalling von REST Requests und nicht zuletzt das Routing auf die korrekten Methoden.

Und an dieser Stelle enden die meisten Tutorials und lassen uns beeindruckt vom Komfort, den Spring Boot uns bietet, zurück. Aber warum funktioniert der obenstehende Code? Dieser Frage gehen wir im Folgenden nach. Allerdings benötigen wir dafür ein gutes Verständnis für ein grundlegendes Konzept: Annotationskomposition.

Verschaffen Sie sich den Zugang zur Java-Welt mit unserem kostenlosen Newsletter!

Annotationskomposition

Spring Boot verwendet an vielen Stellen verschiedenste Annotationen. Nehmen wir das Beispiel aus Listing 1, sehen wir bereits drei Annotationen: @SpringBootApplication, @RestController und @GetMapping. Annotationen scheinen daher ein wichtiger Bestandteil von Spring Boot zu sein. Diese Annotationen basieren auf einem Konzept, das nicht Spring-Boot-spezifisch ist, sondern aus dem Spring Framework selbst stammt: „Meta-Annotationen“ und darauf aufbauend Annotationskomposition.

Beginnen wir mit Meta-Annotationen: dabei handelt es sich um Annotationen, die auf andere Annotationen angewendet werden können. Betrachten wir die @Service-Annotation von Spring, sehen wir, dass @Service mit @Component annotiert ist. Da @Component auf eine Annotation angewendet werden kann, handelt es sich dabei um eine Meta-Annotation.

Was ist nun die Annotationskomposition? Betrachten wir hierfür die Annotation @RestRepository. Bei dieser fällt auf, dass sie mit @Controller und @ResponseBody annotiert wurde. Somit sind @Controller und @ResponseBody beides ebenfalls Meta-Annotationen. Damit ist @RestController eine Komposition aus @Controller und @ResponseBody.

Das Meta-Annotations-Modell (Kasten: „Exkurs: Java-Meta-Annotationen“) ist dabei keineswegs eine Erfindung des Spring Frameworks. Vielmehr ist es ein Konzept, das Java selbst bietet.

Exkurs: Java-Meta-Annotationen

Java setzt bei der Erstellung eigener Annotationen auf das Meta-Annotations-Modell. Das Verhalten und die Möglichkeiten werden durch Meta-Annotationen ausgedrückt. Dabei gibt es verschiedenste, in der Java Runtime Library vordefinierte Meta-Annotationen. Die meistverwendeten Annotationen werden im Folgenden näher beleuchtet.

@Target

Annotationen lassen sich auf unterschiedlichste Elemente anwenden, unter anderem bei einer Typdeklaration (Klassen, Interfaces, Enums), Feldern, Methoden und noch einigen Stellen mehr. Mit der @Target-Annotation wird eingeschränkt, worauf die Annotation angewendet werden kann. Möchte man beispielsweise eine Annotation nur auf Felder beschränken, kann das mittels @Target({ElementType.FIELD}) erreicht werden.

@Retention

Standardmäßig werden Annotationen in der .class-Datei erhalten, jedoch nicht zur Laufzeit geladen. Möchte man die Annotation mittels Reflection auslesen, muss das mittels @Retention(RetentionPolicy.RUNTIME) definiert werden.

@Documented

Vereinfacht ausgedrückt, definiert @Documented, ob die eigene Annotation in der Javadoc-Dokumentation einer annotierten Klasse auftaucht. Die Bedeutung von @Documented geht jedoch darüber hinaus und definiert, dass eine Annotation einen Teil des öffentlichen Kontrakts darstellt – und hat damit denselben Stellenwert wie die Methoden eines Public API.

@Inherited

Standardmäßig werden Annotationen nicht vererbt. Wurde eine Klasse A mit der Annotation @X versehen, gilt die Annotation nicht für eine von A abgeleitete Klasse B. Soll die Annotation @X auch für Unterklassen gültig sein, kann @Inherited das für die Annotation @X bewirken.

Auf den Spuren der Spring-Boot-Magie

Nachdem wir das grundlegende Konzept der Meta-Annotationen betrachtet haben, schauen wir nun tiefer in die Magie von Spring Boot. Kehren wir hierzu zurück zu dem Beispiel aus Listing 1. Dort finden wir die zentrale Annotation von Spring Boot: @SpringBootApplication. Bei @SpringBootApplication handelt es sich um eine Annotationskomposition, die mit mehreren Annotationen meta-annotiert wird. In Listing 2 ist ein Auszug aus dem Quelltext von @SpringBootApplication zu sehen.

Listing 2: @SpringBootApplication

/* ... */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
  @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
  @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
public @interface SpringBootApplication {
  /* ... */
}

Die ersten vier Annotationen sind Meta-Annotationen der Java Runtime Library, deren Bedeutung für die aktuelle Betrachtung zweitrangig sind. Für uns sind an dieser Stelle die drei darauffolgenden Annotationen interessanter: @SpringBootConfiguration, @EnableAutoConfiguration und @ComponentScan. Diese Annotationen sind selbst ebenfalls wiederum meta-annotiert, was zu einer Hierarchie führt, die in Abbildung 1 zu sehen ist.

Abb. 1.: Die Meta-Annotationen-Hierarchie von @SpringBootApplication

 

Mittels @ComponentScan wird definiert, dass im Klassenpfad nach mit @Component annotierten Klassen gesucht werden soll. Über diesen Weg findet Spring beispielsweise mit @Service annotierte Klassen. @SpringBootConfiguration ist mit @Configuration meta-annotiert, was damit auch bedeutet, dass @SpringBootApplication ebenfalls eine Spring-Konfiguration darstellt. Dieses Detail ist der Grund, weshalb es möglich ist, in der mit @SpringBootApplication annotierten Klasse direkt @Bean-Definitionen zu integrieren.

Auch wenn die genannten Annotationen interessant sind, so sind wir doch auf der Suche nach der Magie von Spring Boot, die automatisch eine passende Anwendungskonfiguration bereitstellt. Und dabei sticht direkt eine Annotation ins Auge: @EnableAutoConfiguration. Wir sind im Kern der Spring-Boot-Magie angekommen! Natürlich ist es nicht die Annotation selbst, die für die Magie sorgt, sondern ein @Import, der auf AutoConfigurationImportSelector verweist. Üblicherweise wird @Import verwendet, um weitere Spring-Konfigurationen in den Application Context aufzunehmen. Neben mit @Configuration annotierten Klassen darf @Import jedoch auch auf Klassen verweisen, die entweder ImportSelector oder ImportBeanDefinitionRegistrar implementieren. Beides sind Spring-Interfaces, die es ermöglichen, den Aufbau des Application Context zu beeinflussen.

Für das Verständnis von Spring Boot ist der ImportSelector interessant, denn die Klasse AutoConfigurationImportSelector implementiert genau dieses Interface. Ein ImportSelector kann programmatisch entscheiden, welche Spring-Konfigurationen dem Application Context hinzugefügt werden sollen. Dieser Mechanismus wird für das Laden der Auto Configurations verwendet. Was genau Auto Configurations eigentlich sind und wie sie funktionieren, betrachten wir etwas später in diesem Artikel, denn vorher sollten wir herausfinden, woher sie überhaupt stammen.

Der AutoConfigurationImportSelector verwendet hier ein weiteres, weniger bekanntes Spring-Feature: den SpringFactoriesLoader und die damit verbundenen META-INF/spring.factories-Dateien. Bei den spring.factories handelt es sich um Dateien im Properties-Format, mit einem Klassennamen als Key und einer Liste von assoziierten Klassen, jeweils durch ein Komma getrennt. Listing 3 zeigt einen Auszug aus der META-INF/spring.factories von Spring Boot.

Listing 3: Auszug aus META-INF/spring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
# ...

Wichtig ist hierbei, dass es nicht nur eine einzige META-INF/spring.factories gibt, sondern mehrere. Sucht man in der IDE der Wahl nach spring.factories in einem Spring-Boot-basierenden Projekt, wird man mehrere Dateien finden. Die Aufgabe des SpringFactoriesLoader ist es, diese zu aggregieren und transparent zur Laufzeit bereitzustellen. Für den Aufrufer ist es unerheblich, ob die Liste der Klassennamen aus einer Datei stammen oder aus mehreren. Das bildet die Grundlage für die Umsetzung eigener Auto Configurations und Starter.

Versuchen Sie selbst einmal, alle verfügbaren Auto Configurations aufzulisten. Hierfür verwenden wir die gleiche Logik, wie sie auch der AutoConfigurationImportSelector verwendet. Das entsprechende Codebeispiel ist in Listing 4 zu finden.

Listing 4: SpringFactoriesExploration

package de.digitalfrontiers.springboot.springfactories;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.core.io.support.SpringFactoriesLoader;
import java.util.List;

public class SpringFactoriesExplorationApplication {
  public static void main(String[] args) {
    final List<String> candidates = SpringFactoriesLoader
      .loadFactoryNames(EnableAutoConfiguration.class, null);
    candidates.forEach(System.out::println);
  }
}

Auto Configuration

Nachdem wir nun wissen, wie die Auto Configurations gefunden werden, stellt sich natürlich die Frage, was genau eine Auto Configuration eigentlich ist. Die Antwort ist überraschend einfach: Alle Einträge, die in den spring.factories für EnableAutoConfiguration auftauchen, sind selbst normale Spring-Konfigurationen (Klassen, annotiert mit @Configuration). Was Auto Configurations so besonders macht, ist der Zeitpunkt, zu dem sie geladen werden. Der AutoConfigurationImportSelector wird sehr spät im Aufbau des Application Context ausgeführt. So werden @Import-Deklarationen vorher beachtet, und auch der Component Scan findet vorher statt. Damit enthält der Application Context bereits alle Bean-Definitionen der Anwendung, bevor die Auto Configurations betrachtet werden. Das ermöglicht es, die Bean-Deklarationen in den Auto Configurations an Bedingungen zu knüpfen. Diese Bedingungen werden durch @ConditionalOn-Annotationen beschrieben (Kasten: „Übersicht der @ConditionalOn-Annotationen“). Wir betrachten diese Annotationen später in einem konkreten Beispiel.

Übersicht der @ConditionalOn-Annotationen

Spring Boot bietet viele verschiedene @ConditionalOn-Annotationen für die unterschiedlichsten Einsatzzwecke an. Im Folgenden sind die gebräuchlichsten Annotationen beschrieben.

@ConditionalOnProperty

Es wird eine Property erwartet, die ggf. auch einen bestimmten Wert hat. Diese Bedingung eignet sich beispielsweise, um Feature-Toggles umzusetzen.

@ConditionalOnBean, @ConditionalOnMissingBean

Mit diesen Annotationen kann ausgedrückt werden, dass eine Bean eines bestimmten Typs im Application Context existiert (@ConditionalOnBean) oder es keine Beans dieses Typs gibt (@ConditionalOnMissingBean). Im Abschnitt „Bedingte Bean-Definitionen“ wird dies näher betrachtet.

@ConditionalOnClass, @ConditionalOnMissingClass

Ähnlich den Annotationen @ConditionalOnBean und @ConditionalOnMissingBean definieren diese Annotationen eine Erwartungshaltung: entweder, dass eine Klasse auf dem Klassenpfad verfügbar ist oder dass sie es nicht ist.

@ConditionalOnJava

Sind bestimmte Konfigurationen oder Beans abhängig von einer bestimmten Java-Runtime-Version, kann das mittels dieser Annotation ausgedrückt werden.

@ConditionalOnResource

Diese Bedingung erwartet, dass Ressourcen verfügbar sind. Dabei sind alle von Spring unterstützen Ressourcenpfade möglich, beispielsweise classpath:/index.html.

@ConditionalOnWebApplication, @ConditionalOnNotWebApplication

Mit diesen Annotationen können Bean-Deklarationen erstellt werden, abhängig davon, ob es sich um eine Webanwendung handelt oder nicht. Bei @ConditionalOnWebApplication kann zusätzlich unterschieden werden, ob es sich um eine Servlet-basierte oder reaktive Webapplikation handelt.

Als ich Spring Boot für mich entdeckt habe, war meine Erwartungshaltung, dass Spring Boot erst die notwendige Infrastruktur bereitstellt, bevor meine Bean-Deklarationen betrachtet werden. Interessanterweise ist das Konzept genau umgekehrt. Erst werden die Applikation und der Application Context geladen und danach schaut Spring Boot (genauer gesagt: schauen die Auto Configurations), was noch benötigt wird.

Erstellen eigener Auto Configurations und Starter

Mit dem grundlegenden Verständnis für Auto Configuration können wir nun mit eigenen Auto Configurations beginnen. Als Beispiel soll die Auto Configuration für den SSH-Server des Apache-MINA-Projekts dienen. Das Ziel ist es, einen SSH-Server in eine Spring-Boot-Anwendung zu integrieren. Sämtliche nun folgenden Beispiele finden sich im zugehörigen GitHub-Projekt.

Beginnen wir mit einer einfachen Anwendung, bei der das Set-up für den SSH-Server statisch umgesetzt wird (Listing 5).

Listing 5: SShServerApplication.java

@SpringBootApplication
public class SshServerApplication {

  @Bean(initMethod = "start", destroyMethod = "stop")
  public SshServer sshServer(ShellFactory shellFactory) {
    final ServerBuilder builder = ServerBuilder.builder();
    final SshServer server = builder.build();
    server.setPort(8022);

    server.setKeyboardInteractiveAuthenticator(
      new DefaultKeyboardInteractiveAuthenticator());
    server.setPasswordAuthenticator(passwordAuthenticator());
    server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());

    server.setShellFactory(shellFactory);

    return server;
  }

  private PasswordAuthenticator passwordAuthenticator() {
    return (username, password, session) ->
      "admin".equals(username) && "password".equals(password);
  }

  @Bean
  public ShellFactory shellFactory() {
    return new NoopShellFactory();
  }

  public static void main(String[] args) throws Exception {
    SpringApplication.run(SshServerApplication.class, args);
  }
}

Die Anwendung ist einfach strukturiert. Es handelt sich bereits um eine Spring-Boot-Applikation mit der Deklaration von zwei Beans. Die eine Bean ist der SshServer selbst. Hierbei wird der Server mit einem Port konfiguriert, die Authentifizierung initialisiert und eine sogenannte ShellFactory gesetzt. Die ShellFactory stellt dem Anwender nach einem erfolgreichen SSH-Verbindungsaufbau eine interaktive Shell bereit. Wie diese Shell konkret umgesetzt wird, ist nicht von MINA vorgegeben. Da die Entwicklung eines vollständigen Bash-Äquivalents den Rahmen dieses Artikels sprengen würde, verwenden wir in diesem Beispiel eine NoopShellFactory (Listing 6), die beim Aufruf schlicht „noop“ ausgibt und die Verbindung schließt.

Listing 6: NoopShellFactory

public class NoopShellFactory implements ShellFactory {
  @Override
  public Command createShell(ChannelSession channel) throws IOException {
    return new AbstractCommandSupport("noop-shell", channel.getExecutorService()) {
      @Override
      public void run() {
        final var out = new PrintWriter(getOutputStream());
        out.println("noop");
        out.flush();
        getExitCallback().onExit(0);
      }
    };
  }
}

Möchten wir nun eine weitere Applikation mit einem SSH-Server ausstatten, haben wir unterschiedliche Möglichkeiten: entweder das Kopieren des Codes in die neue Applikation oder das Extrahieren der Logik und ihre wiederverwendbare Gestaltung. Natürlich möchten wir nicht den Code kopieren, sondern entscheiden uns für eine wiederverwendbare Auto Configuration.

Der erste Schritt ist, die Bean-Deklarationen in eine eigene Spring-Konfiguration zu extrahieren. Basierend auf dem Beispiel aus Listing 5 resultiert das in einer mit @Configuration annotierte Klasse, die wir MinaSSHDAutoConfiguration nennen. Diese neue Klasse darf jedoch weder im Scanbereich des Component-Scanning sein noch mittels @Import importiert werden. In unserem Beispiel befindet sich die Spring-Applikation aus Listing 5 im Package de.digitalfrontiers.springboot.example. Somit darf die MinaSSHDAutoConfiguration weder in diesem noch in einem darunterliegenden Package liegen. Nehmen wir jedoch de.digitalfrontiers.springboot.mina.autoconfigure als Package-Namen, ist die MinaSSHDAutoConfiguration außerhalb des Component-Scanbereichs.

Damit Spring Boot die Auto Configuration findet, muss sie noch in META-INF/spring.factories eingetragen werden:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  de.digitalfrontiers.springboot.mina.autoconfigure.MinaSSHDAutoConfiguration

Bedingte Bean-Definitionen

Wir haben nun erreicht, dass die Konfiguration von SshServer auch von anderen Applikationen verwendet werden kann. Allerdings ist die Konfiguration für alle Applikationen immer identisch, was sicher nicht ausreichend ist. Benötigt eine Anwendung beispielsweise eine spezielle ShellFactory, um Monitoringinformationen über SSH abrufbar zu machen, muss es möglich sein, die vorkonfigurierte NoopShellFactory zu ersetzen. Dabei genügt es nicht, eine weitere Bean vom Typ ShellFactory zu definieren, da dann zwei ShellFactory-Beans existieren und nicht entschieden werden kann, welche davon verwendet werden soll. Entfernt man die NoopShellFactory-Deklaration in unserer MinaSSHDAutoConfiguration, muss immer eine ShellFactory im Application Context der Applikation definiert sein, selbst wenn für die Applikation die Standard-NoopShellFactory völlig ausreichen würde. Die Idee bei Auto Configurations ist, dass diese mit sinnvollen Defaults beginnen und beiseitetreten, wenn die Defaults nicht mehr genügen. Hier ist das die NoopShellFactory. Diese soll als Default verwendet werden, es sei denn, es wird explizit eine Bean vom Typ ShellFactory deklariert. Das erreicht man mit den @ConditionalOn-Annotationen, in diesem Fall mit @ConditionalOnMissingBean. Listing 7 enthält die resultierende MinaSSHDAutoConfiguration.

Listing 7: MinaSSHDAutoConfiguration

@Configuration
@ConditionalOnClass(SshServer.class)
public class MinaSSHDAutoConfiguration {

  @Bean(initMethod = "start", destroyMethod = "stop")
  @ConditionalOnMissingBean
  public SshServer sshServer(ShellFactory shellFactory, PasswordAuthenticator passwordAuthenticator) {
    final ServerBuilder builder = ServerBuilder.builder();
    final SshServer server = builder.build();
    server.setPort(8022);

    server.setKeyboardInteractiveAuthenticator(
      new DefaultKeyboardInteractiveAuthenticator());
    server.setPasswordAuthenticator(passwordAuthenticator);
    server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());

    server.setShellFactory(shellFactory);

    return server;
  }

  @Bean
  @ConditionalOnMissingBean
  public PasswordAuthenticator passwordAuthenticator() {
    return (username, password, session) ->
      "admin".equals(username) && "password".equals(password);
  }

  @Bean
  @ConditionalOnMissingBean
  public ShellFactory shellFactory() {
    return new NoopShellFactory();
  }
}

Eine mit @ConditionalOnMissingBean annotierte @Bean-Deklaration wird nur dann berücksichtigt, wenn im Application Context nicht bereits eine Deklaration existiert. Konkret bedeutet das, dass Anwendungen standardmäßig einen SSH-Server enthalten, der die NoopShellFactory verwendet. Anwendungen, die jedoch eine spezifische Implementierung der ShellFactory verwenden möchten, müssen lediglich eine Bean vom Typ ShellFactory deklarieren.

Dem aufmerksamen Leser ist vermutlich die zusätzliche Annotation @ConditionalOnClass in der Klassendeklaration der Auto Configuration nicht entgangen. Diese Annotation drückt aus, dass die gesamte MinaSSHDAutoConfiguration nur dann anwendbar ist, wenn die Klasse SshServer auf dem Klassenpfad existiert. Kann SshServer nicht gefunden werden, wird die komplette Auto Configuration ignoriert. Damit ergeben sich interessante Möglichkeiten für die Strukturierung der Auto Configuration sowie möglicher Starter.

Auto Configuration und Starter

Bisher haben wir die Starter und deren Bedeutung noch nicht betrachtet. Nachdem wir aber die erste eigene Auto Configuration erstellt haben, ist nun genau der richtige Zeitpunkt, das nachzuholen.

Auto Configurations sorgen für die Konfiguration von Bibliotheken, wenn sich diese auf dem Klassenpfad befinden. Ein Starter sorgt für genau diesen Klassenpfad, indem er das Dependency-Management übernimmt. Wir benötigen die Dependency zur eigentlichen Library (in unserem Beispiel sshd-mina) und die Bibliothek, in der sich unsere jüngst erstellte MinaSSHDAutoConfiguration befindet. Die Spring-Boot-Dokumentation empfiehlt hierbei eine Aufteilung in zwei Module: Eins ist der jeweilige Starter, das zweite enthält die Auto Configuration. In Abbildung 2 findet sich die exemplarische Umsetzung.

Abb. 2: Starter und Auto-Configure-Abhängigkeiten

 

Die Spring-Boot-Anwendung definiert damit eine Abhängigkeit auf den mina-sshd-spring-boot-starter. Und ganz im Spirit von Spring Boot ist das das Einzige, was der Anwendungsentwickler machen muss, um von der Auto Configuration und dem Dependency-Management zu profitieren. Der Starter hat Abhängigkeiten zu allen für diesen Starter relevanten Libraries (hier: sshd-mina) sowie die passenden autoconfigure-Module. Diese Struktur erlaubt eine klare Trennung der Verantwortlichkeiten: Der Starter ist für das Dependency-Management zuständig, das autoconfigure-Modul für die Bereitstellung der Auto Configuration.

Dabei fällt einerseits auf, dass die Abhängigkeit des Moduls mina-autoconfigure zu sshd-mina als optional angegeben wurde, andererseits auch, dass der Starter den Zusatz sshd im Modulnamen enthält, was bei mina-autoconfigure jedoch nicht der Fall ist. Letzteres ist nicht etwa ein Tippfehler, sondern wurde ganz bewusst gewählt.

Mina bietet eine Reihe von verschiedenen Libraries, die auf der Mina-Kernbibliothek aufbauen. Nehmen wir als Beispiel die Unterstützung für SFTP, die durch Mina angeboten wird. Da diese ebenfalls auf dem SshServer basiert, ist es sinnvoll, die entsprechende Auto Configuration in demselben Modul zu implementieren wie die MinaSSHDAutoConfiguration (in mina-autoconfigure). Die Abhängigkeit zu der notwendigen Bibliothek wird dann ebenfalls als optional deklariert und schlussendlich ein passender Starter bereitgestellt. Es ergibt sich eine Modulstruktur wie in Abbildung 3 zu sehen.

Abb. 3: Neue Modulstruktur mit Unterstützung für SFTP

 

Damit kann ein Entwickler nun entscheiden, ob er nur SSH benötigt oder doch auch SFTP – und das primär durch das Setzen einer Dependency. Welche Auto Configuration aktiviert wird, entscheidet sich damit primär über den Klassenpfad und mittels der Annotation @ConditionalOnClass.

Fazit

Das Auto-Configuration-Konzept ist so einfach wie genial. Hat man einmal begonnen, in der Struktur von Auto Configuration und Starter zu denken und zu entwickeln, bieten sich viele Möglichkeiten. Kopierter Code (von dem wir alle wissen, dass er viel zu häufig existiert) kann reduziert und vereinheitlicht werden. Unternehmenskonventionen können in Starter und Auto Configuration umgesetzt werden und nicht mehr nur in länglichen, selten aktualisierten Verfahrensanweisungen.

Ausblick

Es endet aber nicht nur bei den Auto Configurations und Startern. Jeder, der mit Spring Boot entwickelt hat, kennt die application.yaml oder application.properties. Mit ihnen können Parameter wie beispielsweise Ports konfiguriert werden. Einen Port haben wir auch in unserem SSH-Server-Codebeispiel gesetzt. Wäre es nicht extrem hilfreich, wenn man diesen Port mittels eines Eintrags in der application.yaml einfach ändern könnte? Genau hierfür hat Spring Boot ein weiteres Konzept, das mit Features wie Validierung und Type Safety aufwartet, genannt „Externalized Configuration“. Dieses Konzept werden wir im nächsten Teil der Reihe genauer beleuchten.

Nun haben wir unseren eigenen Starter und Auto Configurations erstellt, und es wäre wirklich genial, wenn man den Spring Initializr dazu bringen könnte, die eigenen Starter anzubieten. Und typisch für das Spring-Ökosystem: Auch das ist möglich! Wie genau, sehen wir im zweiten Teil dieser Artikelserie.

Alle News der Java-Welt:
Alle News der Java-Welt:

Behind the Tracks

Agile & Culture
Teamwork & Methoden

Data Access & Machine Learning
Speicherung, Processing & mehr

Clouds, Kubernets & Serverless
Alles rund um Cloud

Core Java & JVM Languages
Ausblicke & Best Practices

DevOps & Continuous Delivery
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Web Development & JavaScript
JS & Webtechnologien

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Software-Architektur
Best Practices

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management