39,90 €
Ihr umfassender Leitfaden für Spring Boot 3 - Spring Boot von Null auf Hundert - Neuerungen in Spring Boot 3 und Migration von Spring Boot 2 - Beispiele für verschiedenste Anwendungsfälle Entdecken Sie die Leistungsfähigkeit von Spring Boot 3 mit diesem umfassenden Leitfaden. Von den Grundlagen zu Spring Boot 3 über fortgeschrittene Themen wie zum Beispiel reaktive Programmierung bis zu eigenen Erweiterungen von Spring Boot. Was Sie in diesem Buch erwartet: Universelle Zugänglichkeit: Egal, ob Sie gerade erst anfangen oder bereits Erfahrung mit Spring Boot haben, dieses Buch führt Sie durch die Grundlagen bis hin zu fortgeschrittenen Konzepten. Praxisorientierte Lernweise: Nutzen Sie zahlreiche praxisnahe Beispiele, um das Gelernte sofort anzuwenden. Die klare Darstellung und Anwendung in realen Szenarien machen den Lernprozess effektiv und eingängig. Neuerungen in Version 3: Entdecken Sie die spezifischen Features von Spring Boot 3. Die Autoren gehen detailliert auf die neuesten Entwicklungen ein und zeigen, wie Sie das volle Potenzial dieser Version ausschöpfen können. Ob Sie ein erfahrener Entwickler sind oder gerade erst in die Welt von Spring Boot eintauchen – dieses Buch vermittelt Ihnen das notwendige Wissen, um moderne und effiziente Java-Anwendungen zu entwickeln.
Das E-Book können Sie in Legimi-Apps oder einer beliebigen App lesen, die das folgende Format unterstützen:
Seitenzahl: 506
François Fernandès ist Senior Solution Architect bei Digital Frontiers und hat fast 20 Jahre Erfahrung in der Java-Softwareentwicklung und dem Java-Ökosystem. Seine Erfahrungen reichen dabei von der Entwicklung von Java-Desktop-Anwendungen über verteilte Applikationen bis hin zu Softwarearchitekturen in hybriden Cloud-Umgebungen. Den ersten Kontakt mit Spring hatte François mit dem Spring Framework 2.0. Seither begleiten ihn das Spring Framework, Spring Boot sowie diverse weitere Spring-Projekte in den unterschiedlichsten Einsätzen.
Tom Hombergs ist Principal Developer bei Atlassian in Sydney und dort für den Java Tech Stack verantwortlich, der von hunderten Teams weltweit zur Entwicklung der Atlassian-Produkte genutzt wird. Spring Boot ist ein zentraler Bestandteil dieses Tech Stacks. Er arbeitet seit vielen Jahren mit Spring und Spring Boot und trieb bei Atlassian unter anderem die unternehmensweite Migration von Spring Boot 2 zu Spring Boot 3 für über 800 (Micro-)Services voran. Tom ist Autor von »Get Your Hands Dirty on Clean Architecture« (deutsche Ausgabe: »Clean Architecture Praxisbuch«) und »Stratospheric – from Zero to Production with Spring Boot and AWS« und schreibt gelegentlich auf reflectoring.io über Themen der Softwareentwicklung.
Benedikt Jerat ist Senior Consultant bei Digital Frontiers. Seine Schwerpunkte liegen auf der Softwareentwicklung im Umfeld von JVM-Sprachen wie Java, Kotlin und Scala, wobei er besonders auf Spring und Spring Boot setzt. In seinen Projekten fokussiert er sich auf die funktionale Programmierung und die Prinzipien des Software Craftsmanship. Benedikt ist Autor zahlreicher Blog-Posts und Artikel in Fachzeitschriften und regelmäßiger Speaker auf Konferenzen zu aktuellen Themen.
Florian Pfleiderer ist Co-Founder und Senior Consultant bei Digital Frontiers. In dieser Rolle beschäftigt er sich vor allem mit den Themen Architektur, Microservices und Software Craftsmanship und unterstützt seine Kunden auf ihrem Weg in die Cloud. Das Spring Framework benutzte er zum ersten Mal vor über zehn Jahren, bei Spring Boot war er Nutzer der ersten Stunde. Trotz oder gerade wegen einiger Ausflüge in andere Ökosysteme kehrt er immer wieder gerne zu Spring Boot zurück und empfiehlt es seinen Kunden mit Begeisterung und Überzeugung. Außerhalb von Projekten gibt er sein Wissen regelmäßig auf Konferenzen oder in Fachzeitschriften weiter.
Copyright und Urheberrechte:
Die durch die dpunkt.verlag GmbH vertriebenen digitalen Inhalte sind urheberrechtlich geschützt. Der Nutzer verpflichtet sich, die Urheberrechte anzuerkennen und einzuhalten. Es werden keine Urheber-, Nutzungs- und sonstigen Schutzrechte an den Inhalten auf den Nutzer übertragen. Der Nutzer ist nur berechtigt, den abgerufenen Inhalt zu eigenen Zwecken zu nutzen. Er ist nicht berechtigt, den Inhalt im Internet, in Intranets, in Extranets oder sonst wie Dritten zur Verwertung zur Verfügung zu stellen. Eine öffentliche Wiedergabe oder sonstige Weiterveröffentlichung und eine gewerbliche Vervielfältigung der Inhalte wird ausdrücklich ausgeschlossen. Der Nutzer darf Urheberrechtsvermerke, Markenzeichen und andere Rechtsvorbehalte im abgerufenen Inhalt nicht entfernen.
François Fernandès · Tom Hombergs ·Benedikt Jerat · Florian Pfleiderer
So geht moderne Java-Entwicklung –Konzepte und Anwendungen
François Fernandès · Tom Hombergs · Benedikt Jerat · Florian Pfleiderer
Lektorat: Dr. Benjamin Ziech
Projektkoordinierung: Julia Griebel
Copy-Editing: Annette Schwarz, Ditzingen
Satz: inpunkt[w]o, Wilnsdorf (www.inpunktwo.de)
Herstellung: Stefanie Weidner
Umschlaggestaltung: Eva Hepper, Silke Braun
Bibliografische Information der Deutschen Nationalbibliothek
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie;
detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
ISBN:
978-3-86490-994-8
978-3-98890-125-5
ePub
978-3-98890-126-2
1. Auflage 2024
Copyright © 2024 dpunkt.verlag GmbH
Wieblinger Weg 17
69123 Heidelberg
Schreiben Sie uns:
Falls Sie Anregungen, Wünsche und Kommentare haben, lassen Sie es uns wissen: [email protected].
Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen.
Es wird darauf hingewiesen, dass die im Buch verwendeten Soft- und Hardware-Bezeichnungen sowie Markennamen und Produktbezeichnungen der jeweiligen Firmen im Allgemeinen warenzeichen-, marken- oder patentrechtlichem Schutz unterliegen.
Alle Angaben und Programme in diesem Buch wurden mit größter Sorgfalt kontrolliert. Weder Autoren noch Verlag können jedoch für Schäden haftbar gemacht werden, die in Zusammenhang mit der Verwendung dieses Buches stehen.
1Einleitung
Teil IGrundlagen
2Hallo, Spring Boot
3Spring-Grundlagen
4Spring-Boot-Grundlagen
5Konfiguration
6Build Management mit Spring Boot
7Einführung ins Testen
8Troubleshooting einer Spring-Boot-Anwendung
Teil IIAnwendungsfälle
9Einen REST-Service entwickeln
10Das Reactor-Framework verwenden
11Eine GraphQL-API entwickeln
12Integration einer SPA mit Spring Boot
13Ein serverbasiertes Web-Frontend entwickeln
14Eine Datenbank anbinden
15Eine CLI-Anwendung entwickeln
16Architektur-Governance mit Spring Boot
Teil IIIReferenz
17Testing
18Spring Reactive
19Spring Web MVC
20HTTP-Clients mit Spring
21GraphQL
22Spring Boot Developer Tools
23Events
24Caching
25Messaging
26Spring Data
27Spring Cloud Config
28Spring Security
29Observability
30Docker-Images mit Spring Boot
31Native Images mit Spring Boot
32Spring Boot erweitern
33Coordinated Restore at Checkpoint (CRaC)
34Migration von Spring Boot 2 zu Spring Boot 3
35Ausblick
1Einleitung
1.1Warum Spring Boot?
1.2Für wen ist dieses Buch?
1.3Aufbau des Buchs
1.4Codebeispiele
Teil IGrundlagen
2Hallo, Spring Boot
2.1Einleitung
2.2JDK installieren
2.3Kickstart mit dem Spring Initializr
2.4Projektstruktur
2.5Die Anwendung bauen
2.6Die Anwendung starten
2.7Einen REST-Controller bauen
3Spring-Grundlagen
3.1Dependency Injection und Inversion of Control
3.1.1Inversion of Control
3.1.2Dependency Injection
3.2Der Spring Application Context
3.3Application Context mit XML konfigurieren
3.4Java-Konfiguration im Detail
3.4.1@Configuration und @Bean
3.4.2@Component und @ComponentScan
3.4.3@Configuration und @ComponentScan kombinieren
3.5Was haben wir vom Spring Container?
4Spring-Boot-Grundlagen
4.1Bootstrapping
4.2Den Application Context beeinflussen
4.3Embedded Webserver
4.4Dependency Management
4.5Integrationen
4.6Produktionsbetrieb
5Konfiguration
5.1Warum Konfiguration?
5.2Konfigurationsparameter
5.2.1Konfigurationsparameter definieren
5.2.2Parameter als String injizieren
5.2.3Parameter typsicher injizieren
5.2.4Parameter validieren
5.2.5Default-Werte definieren
5.3Profile
5.3.1Konfigurationsparameter für ein Profil definieren
5.3.2Ein Profil aktivieren
5.3.3Konfigurationswerte aus unterschiedlichen Quellen kombinieren
5.4Das Environment-Bean
5.5Best Practices für Konfigurationsmanagement
5.5.1Konfigurationsdateien im JAR
5.5.2Secrets als Umgebungsvariablen definieren
5.5.3Umgebungsvariablen explizit definieren
5.5.4Profile nur für Umgebungen
5.5.5Konfiguration von Default-Werten
6Build Management mit Spring Boot
6.1Überblick
6.2Gradle oder Maven?
6.3Spring Boot »Fat JAR«
6.4Das Gradle Plugin
6.5Das Maven Plugin
6.6Dependency Management mit Spring Boot
6.6.1Spring-Boot-BOM mit Gradle konsumieren
6.6.2Spring-Boot-BOM mit Maven konsumieren
7Einführung ins Testen
7.1Testen – wieso, weshalb, warum?
7.2Spring Boot Starter Test
7.3Unit-Tests
7.4SpringExtension (JUnit 5)
7.5Integrationstests mit Spring
7.5.1@SpringBootTest
7.5.2Ausblick auf Slice-Annotationen
8Troubleshooting einer Spring-Boot-Anwendung
8.1Spring-Boot-Magie
8.2Troubleshooting-Werkzeuge
8.2.1Lokal reproduzieren
8.2.2Debug-Modus
8.2.3Actuator-Endpoints
8.2.4Logging
8.2.5Hooks
8.3Troubleshooting-Anwendungsfälle
8.3.1Welche Beans stehen mir zur Verfügung?
8.3.2Wo kommt eine Bean her?
8.3.3Warum ist meine Bean nicht im Application Context?
8.3.4Warum ist eine Bean doppelt im Application Context?
8.3.5Wie ist meine Anwendung konfiguriert?
8.3.6Welche Konfigurationsparameter werden (nicht) genutzt?
8.3.7Welche Endpoints stellt meine Anwendung zur Verfügung?
8.3.8Warum startet meine Anwendung nicht?
Teil IIAnwendungsfälle
9Einen REST-Service entwickeln
9.1Was ist REST?
9.2Codebeispiel
9.3Endpoints
9.4Request Body und Parameter
9.5Fehlerbehandlung
10Das Reactor-Framework verwenden
10.1Was ist das Reactor-Framework?
10.2Codebeispiel
10.3Andere reaktive Operatoren
11Eine GraphQL-API entwickeln
11.1GraphQL in Kürze
11.2Die Beispielanwendung
11.3Erstellung der Anwendung
11.4Abbildung des Datenmodells in GraphQL
11.5Implementierung des Controllers
11.5.1Query Mapping
11.5.2Schema Mapping
11.5.3Mutation Mapping
11.6Implementierung einer Subscription
12Integration einer SPA mit Spring Boot
12.1Herausforderungen von SPAs
12.1.1Handhabung von URLs
12.1.2Integration unterschiedlicher Toolings
12.2Mögliche Varianten für ein Deployment
12.2.1Integration in das Spring-Boot-JAR
12.2.2Unabhängige Deployments des Frontends und Backends
12.3Ein Spring Boot Backend mit React Frontend
12.3.1Verzeichnisstruktur
12.3.2Konfiguration des Builds
12.3.3Integration einer REST-API in das Frontend
12.3.4Unterstützung von Deep-Links mit der History-API
13Ein serverbasiertes Web-Frontend entwickeln
13.1Warum serverseitig?
13.2Die Beispielanwendung
13.3»Hello World« mit Thymeleaf
13.4Formulardaten verarbeiten
13.5Interaktivität mit HTMX
13.6Spring Boot Developer Tools
13.7Weitere Ressourcen
14Eine Datenbank anbinden
14.1Datenbanken
14.2Codebeispiel
14.3Spring Data Repositories
14.4Transaktionen
15Eine CLI-Anwendung entwickeln
15.1Eine ADR-Management-Anwendung
15.2Erstellen des Projekts
15.2.1Die ADR-API
15.3Ein erstes Kommando
15.4Registrieren von Kommandos
15.4.1Programmatische Registrierung von Kommandos
15.4.2Kommandos mit Annotationen definieren
15.4.3AvailabilityProvider
15.4.4Anzeige eines eigenen Prompts
15.5Ausführung im interaktiven oder Kommando-Modus
15.6Alternativen zu Spring Shell
16Architektur-Governance mit Spring Boot
16.1Warum Architektur-Governance?
16.2Komponentenbasierte Architektur
16.3Komponentenbasierte Architektur mit Spring Boot
16.4Governance mit ArchUnit
16.5Spring Modulith
Teil IIIReferenz
17Testing
17.1Überblick
17.2Unit Testing
17.2.1Unit Testing ohne Spring
17.2.2Unit Testing Utilities im Spring Framework
17.3Integration Testing
17.3.1Spring TestContext Framework
17.3.2SpringExtension und SpringRunner
17.3.3TestContextManager und ContextConfiguration
17.4Wichtige Testfunktionalitäten im Überblick
17.4.1@SpringBootTest
17.4.2Context Initializers
17.4.3@TestPropertySource
17.4.4@ActiveProfiles
17.4.5@TestConfiguration
17.4.6@TestExecutionListeners
17.5Testen von einzelnen Schichten mit Test Slices
17.5.1Was sind Test Slices?
17.5.2@WebMvcTest
17.5.3@DataJpaTest
17.5.4@JsonTest
17.5.5Weitere Test Slices
18Spring Reactive
18.1Warum Reactive?
18.2Grundlagen
18.2.1Reactive Streams und Project Reactor
18.2.2Reaktive Datentypen: Mono und Flux
18.2.3Reactive Chaining
18.3Spring WebFlux
18.3.1Annotierte Controller
18.3.2Funktionale Endpoints
18.3.3WebClient statt RestTemplate
18.4Testen von Reactive Streams
18.4.1StepVerifier
18.4.2WebFluxTest
19Spring Web MVC
19.1REST
19.1.1RestController
19.1.2Request-Daten auslesen
19.1.3Reactive RestController
19.2JSON-Mapping mit Jackson
19.2.1Jackson Annotations
19.2.2Custom Serializer
19.3Error Handling
19.4API-Dokumentation mit Spring REST Docs
19.4.1Setup für REST Docs
19.4.2JUnit-Test zur Generierung
19.4.3Nutzen von Snippets
19.5HATEOAS
19.5.1Wieso überhaupt HATEOAS?
19.5.2Repräsentation von Ressourcen
19.5.3Hypermedia Links & Relations
19.5.4Komfortable Links mit dem WebMvcLinkBuilder
19.5.5Affordances
19.5.6Integration mit RestTemplate und WebClient
20HTTP-Clients mit Spring
20.1RestTemplate
20.2RestClient
20.3WebClient
20.4HTTP-Interface
21GraphQL
21.1Einführung
21.2GraphQL in Kürze
21.2.1Das GraphQL-Schema
21.2.2Die Query Language
21.2.3Ein GraphQL-Schema kann noch mehr
21.3Ablauf eines GraphQL-Requests
21.3.1Transportmethoden
21.4Erstellung einer Spring-Boot-Anwendung mit GraphQL-API
21.4.1Verwenden des Spring Initializr
21.4.2Manuelles Hinzufügen der notwendigen Abhängigkeiten
21.4.3Interaktives Testen mit GraphiQL
21.5Implementierung einer GraphQL-API
21.5.1Definieren eines GraphQL-Schemas
21.5.2GraphQL Controller in Spring Boot
21.5.3Optimierte Abfragen mit Batches
21.5.4Definition eigener Skalare
21.6Fehlerbehandlung
22Spring Boot Developer Tools
22.1Warum Developer Tools?
22.2Developer Tools aktivieren
22.3Restart
22.4Live Reload
22.5Einschränkungen der Developer Tools
23Events
23.1Lose Kopplung
23.2Events versenden
23.3Events empfangen
23.3.1ApplicationListener
23.3.2@EventListener
23.4Synchron oder asynchron?
23.5Spring Boot ApplicationEvents
24Caching
24.1Cache-Configuration
24.2Caching-Annotationen
24.3Cache-Implementierungen
24.3.1EhCache
24.3.2Caffeine
25Messaging
25.1Messaging im Überblick
25.2JMS
25.2.1Mit einer JMS-API verbinden
25.2.2Nachrichten senden
25.2.3Nachrichten empfangen
25.2.4Message Converter
25.3AMQP
25.3.1Das AMQP-Protokoll
25.3.2Konfiguration
25.3.3Nachrichten versenden
25.3.4Nachrichten empfangen
25.4Kafka
25.4.1Konfiguration
25.4.2Nachrichten versenden und empfangen
25.5Ausblick: Spring Cloud Stream
25.5.1Konzepte
25.5.2Nachrichten senden und empfangen
25.5.3Vorteile
26Spring Data
26.1Überblick über Spring Data
26.2Spring Data Repositories
26.2.1Das Repository-Interface
26.2.2Query-Methoden
26.3Die DataSource Bean
26.4JPA
26.4.1Welchen Mehrwert bietet Spring Data JPA?
26.4.2JPA-Repositories und -Entities
26.4.3Datenbankinitialisierung mit JPA und Hibernate
26.5R2DBC
26.5.1Datenbankverbindung über ConnectionFactory
26.5.2Datenbankzugriff
26.6NoSQL mit Spring Data MongoDB
26.6.1MongoDatabaseFactory und MongoTemplate
26.6.2Spring Data Repository für MongoDB
26.6.3Integrationstests mit @DataMongoTest
26.7Schema-Migration mit Flyway
27Spring Cloud Config
27.1Warum Spring Cloud Config?
27.2Der Spring Cloud Config-Server
27.3Environment Repository
27.4Spring Cloud Config-Client
28Spring Security
28.1Konzepte
28.1.1Authentication
28.1.2Authorization
28.1.3Auto-Configuration
28.2Absicherung von Methoden
28.3Absicherung von HTTP-Pfaden
28.4Benutzerverwaltung
28.5Security Testing
28.6OAuth
28.6.1Was ist OAuth?
28.6.2OAuth Resource-Server
28.6.3OAuth-Client
29Observability
29.1Warum Observability?
29.2Admin-Endpoints mit Actuator
29.2.1Anwendungs-Metadaten ausgeben
29.2.2Einen eigenen Actuator-Endpoint entwickeln
29.3Logging
29.3.1SLF4J
29.3.2Logging via application.yml konfigurieren
29.3.3Logback und Log4J direkt konfigurieren
29.3.4Kombination der Logging-Konfigurationen
29.4Metriken
29.4.1Standardmetriken exportieren
29.4.2Metriken an ein Observability-Produkt exportieren
29.4.3Eigene Metriken exportieren
29.4.4Tags
29.4.5Metriken programmatisch anpassen
29.4.6Histogramme und Perzentile
29.5Tracing
29.5.1Traces programmatisch auswerten
29.5.2Traces in Log-Events ausgeben
29.5.3Traces exportieren
29.5.4Clients instrumentieren
29.5.5Baggage
30Docker-Images mit Spring Boot
30.1Warum Docker?
30.2Einfaches Docker-Image mit Spring Boot
30.3Optimierte Docker-Images
30.4Optimiertes Docker-Image mit Spring Boot
30.5Docker abstrahieren mit Buildpacks
31Native Images mit Spring Boot
31.1Warum Native?
31.2Was ist ein Native Image?
31.3Anwendungsfälle für Native Images
31.4Ahead-of-Time-Optimierung mit Spring Boot
31.5Ein natives Image erstellen
31.6Ein natives Image testen
31.7Reachability-Metadaten erstellen
32Spring Boot erweitern
32.1Cross-Cutting Concerns
32.2@Configuration und @Import
32.3@Enable…-Annotationen
32.4@AutoConfiguration
32.5Bedingte @Configuration
32.6Testen von @AutoConfigurations
32.7Starter
32.8Fortgeschrittene Erweiterungspunkte
32.8.1FactoryBean
32.8.2BeanPostProcessor
32.8.3BeanDefinitionRegistryPostProcessor
32.8.4EnvironmentPostProcessor
33Coordinated Restore at Checkpoint (CRaC)
33.1Warum CRaC?
33.2Checkpoint und Restore
33.3Checkpoint und Restore mit Spring Boot
33.4Automatische Checkpoints
33.5Checkpoints in Docker
33.6CRaC vs. GraalVM
34Migration von Spring Boot 2 zu Spring Boot 3
34.1Überblick
34.2Schritt 1: Bibliotheken analysieren und aktualisieren
34.3Schritt 2: Auf Java 17 aktualisieren
34.4Schritt 3: Das Spring-Boot-Upgrade vorbereiten
34.4.1WebSecurityConfigurerAdapter durch WebSecurityFilterChain ersetzen
34.4.2@AutoConfiguration
34.4.3@LocalServerPort
34.4.4@EnableWebFluxSecurity
34.5Schritt 4: Spring Boot aktualisieren
34.5.1Spring Boot auf 3.x aktualisieren
34.5.2Bibliotheken aktualisieren
34.5.3javax durch jakarta ersetzen
34.5.4Spring Cloud Sleuth durch Micrometer ersetzen
34.5.5@ConstructorBinding
34.5.6HttpStatusCode
34.5.7Konfigurationsparameter
35Ausblick
Index
Das Spring Framework und Spring Boot sind aus dem Java-Ökosystem nicht mehr wegzudenken. Ohne Zweifel führt Spring Boot das Feld der Java-basierten Application-Frameworks an. In einer Umfrage von Jetbrains aus dem Jahr 2022 gaben 67% der befragten Java-Entwickler an, Spring Boot zu nutzen, und 41% nutzten Spring MVC (beide Technologien werden in diesem Buch behandelt)1. Das Framework auf Platz 3 kommt nur auf einen einstelligen Prozentwert, und 23% der Befragten gaben an, gar kein Web-Framework zu nutzen, was die Prominenz von Spring nur verdeutlicht.
Softwareunternehmen wie Netflix und Atlassian setzen auf Spring Boot, um die Softwareentwicklung über Hunderte von Teams hinweg zu standardisieren!
Die führende Rolle von Spring Boot kommt nicht von ungefähr. Spring Boot basiert auf dem Spring Framework, das seit Anfang der 2000er Jahre kontinuierlich und sorgsam weiterentwickelt wird. Die Idee, ein auf Spring basierendes »Boot«-Framework zu entwickeln, das es uns vereinfacht, eine Spring-Anwendung zu konfigurieren, zu integrieren und zu betreiben, war nur eine Frage der Zeit.
Spring Boot macht uns als Entwickler produktiver, weil es uns viel Arbeit abnimmt. Es setzt dabei stark auf Konventionen, sodass wir nicht jede Kleinigkeit selbst konfigurieren müssen und sofort loslegen können. Diese große Stärke von Spring Boot wird aber teilweise auch als Hindernis empfunden, wenn man mit diesen Konventionen nicht vertraut ist oder nicht weiß, wo man sie nachschlagen kann.
Genau dieses Hindernis möchten wir mit diesem Buch aus dem Weg räumen.
Dieses Buch richtet sich sowohl an Java-Entwickler, die noch keine Erfahrung mit Spring oder Spring Boot haben, als auch an solche, die bereits Spring Boot 2 gearbeitet haben und einige Neuerungen in Spring Boot 3 kennenlernen oder ihr allgemeines Spring-Boot-Wissen auffrischen möchten.
Es bietet sich an, grundlegende Java-Kenntnisse (oder allgemeine Kenntnisse in objektorientierter Programmierung) mitzubringen, andernfalls sind die Konzepte und Codebeispiele in diesem Buch schwer nachzuvollziehen.
Da Spring Boot eine Fülle von Integrationen mit anderen Systemen anbietet, von denen einige in diesem Buch behandelt werden, eignet sich das Buch ebenfalls als Nachschlagewerk für Architekten, die Spring und Spring Boot für den Einsatz in einem Projekt evaluieren möchten.
Auch wenn dieses Buch die wichtigsten Features und Konzepte erläutert, erhebt es keinen Anspruch auf Vollständigkeit. Das Spring-Ökosystem ist mittlerweile so umfangreich, dass ein Buch mit einer vollständigen Abdeckung unpraktikabel wäre und vermutlich nur als Monitorstütze verwendet werden würde. Daher konzentrieren wir uns in diesem Buch auf die wichtigen Aspekte und verweisen an vielen Stellen für bestimmte Details auf die sehr gute Referenzdokumentation der jeweiligen Spring-Projekte. Es geht nicht darum, jeden Konfigurationsparameter auswendig zu lernen, sondern darum, die Konzepte zu verstehen, um sie dann bei Bedarf anwenden (und nachschlagen) zu können.
Dieses Buch ist in drei Teile aufgeteilt.
In Teil I werden die Grundlagen von Spring und Spring Boot beschrieben. Dieser Teil ist insbesondere für Leser geeignet, die sich noch nicht viel mit Spring und Spring Boot beschäftigt haben. Leser, die bereits Erfahrung mit beiden Frameworks gesammelt haben, mögen Teil I gegebenenfalls ganz oder teilweise überspringen. Die Konzepte aus Teil I sind für das Verständnis vieler Kapitel aus den Teilen II und III erforderlich.
Teil II ist gedacht als Nachschlagewerk für bestimmte Anwendungsfälle. Jedes Kapitel in Teil II betrachtet einen konkreten Anwendungsfall (zum Beispiel die Entwicklung einer REST-API), mit dem man jederzeit in einem Softwareprojekt konfrontiert sein kann. Die Kapitel geben jeweils einen Überblick darüber, wie man den entsprechenden Anwendungsfall mit Spring Boot lösen kann.
Die Kapitel in Teil III sind Referenzkapitel, die im Detail auf Spring-Boot-Features eingehen. Hier lernt der Leser alles, was notwendig ist, um diese Features in einer Anwendung zu nutzen.
Mit Ausnahme von Teil I bauen die Kapitel nicht aufeinander auf, sodass die Kapitel größtenteils unabhängig voneinander gelesen werden können. Wir laden dazu ein, der Neugier zu folgen und diejenigen Kapitel zuerst zu lesen, die gerade das Interesse wecken oder im aktuellen Projekt von Bedeutung sind. Sollte ein Kapitel doch mal ein Konzept aus einem anderen Kapitel erfordern, findet sich ein Verweis im Text.
Ein Codebeispiel sagt mehr als tausend Worte. Das haben wir uns mit diesem Buch zu Herzen genommen. Die Konzepte von Spring und Spring Boot werden mit weit über 300 Codebeispielen veranschaulicht, um sie in Aktion zu sehen.
Noch viel mehr als ein Codebeispiel sagt jedoch eine lauffähige Anwendung. Deshalb haben wir für viele der Kapitel jeweils eine Spring-Boot-Anwendung entwickelt und in einem GitHub-Repository zur Verfügung gestellt. Wir empfehlen, dieses Repository zu klonen und mit den darin enthaltenen Spring-Boot-Anwendungen beim Lesen der Kapitel herumzuspielen. Am besten lernt man doch beim Spielen mit Code!
Das Repository ist unter der URL https://github.com/dxfrontiers/spring-boot-3-buch frei verfügbar.
Damit alle Codebeispiele aus diesem Buch und dem Repository lauffähig sind, empfehlen wir die Installation von JDK 21 und die Nutzung der aktuellen Versionen von Spring Boot 3.x und Spring 6.x. Die meisten Beispiele werden auch mit etwas älteren Versionen funktionieren, aber einige Features sind erst mit späteren Versionen verfügbar.
Im ersten Kapitel bauen wir direkt eine lauffähige »Hello World«-Anwendung, um mit einem Erfolgserlebnis zu starten.
Dieses Buch ist ein Praxisbuch. In fast allen Kapiteln werden wir anhand von Codebeispielen die verschiedenen Aspekte einer Spring-Boot-Anwendung diskutieren. In den meisten Kapiteln sind diese Codebeispiele einer Beispielanwendung entnommen, deren Code auf GitHub öffentlich verfügbar ist. Wir laden dazu ein, den Beispielcode beim Lesen des Buches in der lokalen IDE durchzustöbern.
In diesem Sinne beginnen wir dieses Buch auch mit einem praktischen »Hello World«-Beispiel. Leser, die bereits Erfahrung mit Spring Boot haben, können dieses Kapitel guten Gewissens überspringen. Wenn nicht, laden wir dazu ein, den Beispielen in diesem Kapitel zu folgen und eine erste lauffähige Spring-Boot-Anwendung zu bauen.
In diesem Kapitel werden wir nicht jedes kleine Detail erläutern. Aber keine Angst, wir verweisen stets auf spätere Kapitel, in denen die Details vertieft werden.
Um eine Spring-Boot-Anwendung zu entwickeln, müssen wir ein JDK (Java Development Kit) auf unserem Rechner installieren. Spring Boot 3 benötigt ein JDK der Version 17 oder höher. Wenn Sie bereits ein solches JDK installiert haben, können Sie mit dem nächsten Abschnitt fortfahren.
Wir empfehlen zur Verwaltung des JDK einen Runtime Manager wie asdf1, jEnv2 oder SDKMan3. Diese Tools machen es einfach, zwischen verschiedenen Versionen eines JDK hin- und herzuwechseln. SDKMan und asdf automatisieren darüber hinaus auch das Herunterladen von verschiedenen JDK-Versionen. Wir können mit einem einzigen Kommandozeilenbefehl ein bestimmtes JDK herunterladen oder bestimmen, welches der bereits heruntergeladenen JDKs wir gerade benutzen möchten.
Zur Installation der Runtime Manager verweisen wir auf die Websites der Tools. Im Folgenden zeigen wir, wie wir mit asdf ein JDK installieren, das wir für alle Beispiele in diesem Buch benutzen können.
Vorausgesetzt, asdf ist erfolgreich installiert, müssen wir zunächst das Java-Plugin installieren (asdf unterstützt nämlich auch Runtimes anderer Programmiersprachen):
> asdf plugin add java
Dann können wir zum Beispiel mit dem folgenden Befehl ein JDK installieren:
> asdf install java latest:corretto
Hier installieren wir die aktuelle Version des Amazon-Corretto-JDK. Wir können uns auch eine Liste aller anderen JDKs anzeigen lassen und dann eines von denen installieren:
> asdf list all java | grep \\-21\\.
Diese Liste ist sehr lang, sodass wir sie hier mithilfe von grep auf die JDKs der Version 21 filtern.
Nun, da wir ein JDK installiert haben (oder mehrere), müssen wir unsere Umgebung noch so konfigurieren, dass dieses JDK auch überall genutzt wird:
> asdf local java latest:corretto
Der Befehl java --version sollte nun die erwartete Version des JDK ausgeben.
Nun können wir unser Beispielprojekt erstellen. Das geht am einfachsten mit dem Spring Initializr, der unter https://start.spring.io/ verfügbar ist. Hier können wir einige Parameter, wie zum Beispiel die Java-Version, auswählen und uns eine ZIP-Datei mit einem Projektskelett erstellen lassen. Wir werden den Spring Initializr im Rest dieses Buches noch häufig verwenden, um Beispielprojekte für unterschiedliche Anwendungsfälle zu generieren.
Für unser »Hello World«-Beispielprojekt wählen wir die folgenden Parameter:
Project
: Gradle-Kotlin
Language
: Java
Spring Boot
: aktuelle 3.x-Version (Standardeinstellung)
Dependencies
: Spring Web
Alle anderen Einstellungen belassen wir mit ihren Standardwerten. Über den Button »Add Dependencies« können wir eine Auswahl bestimmter Integrationen und Features auswählen, die unserem Projekt dann als Dependencies hinzugefügt werden. Wir wählen zu Beginn nur die »Spring Web«-Dependency, um eine einfache Webanwendung zu entwickeln.
Mit einem Klick auf den »Generate«-Button lassen wir uns das Projekt erstellen und laden die ZIP-Datei herunter, die wir auf unserer Festplatte entpacken. Dann laden wir den Ordner als Projekt in unsere IDE und schauen uns im Projekt um.
Die zentrale Datei im Projektordner ist die Datei build.gradle.kts, unser Gradle-Build-Skript. Es ist in der Kotlin-DSL geschrieben, weil wir bei Erstellung des Projekts »Gradle-Kotlin« ausgewählt haben. Wir können auch »Gradle-Groovy« wählen, um das Skript in Groovy zu erstellen, oder »Maven«, um Maven als Build-Tool zu nutzen und eine pom.xml-Datei zu erzeugen.
Im Build-Skript finden wir eine Dependency zum Modul spring-boot-starter-web, das uns alle Features mitbringt, um eine Webanwendung zu entwickeln. Wir finden außerdem eine Dependency zum Modul spring-boot-starter-test, das uns einige Testwerkzeuge zur Verfügung stellt, die wir später im Testing-Kapitel noch genauer betrachten.
Das Build-Skript konfiguriert darüber hinaus die Plugins org.spring-framework.boot und io.spring.dependency-management, über die wir in Kapitel 6 noch mehr lernen.
Wir finden des Weiteren die Dateien gradlew und gradlew.bat und den Ordner gradle, die alle zum Gradle Wrapper4 gehören. Die gradlew-Skripte können wir nutzen, um Gradle aufzurufen, ohne Gradle lokal installieren zu müssen. Sie prüfen, ob eine Gradle-Installation verfügbar ist, und laden sie, falls erforderlich, herunter. So können wir unser Projekt auf unterschiedlichen Rechnern laufen lassen, ohne auf jedem Rechner Gradle installieren zu müssen. Wir werden unser Projekt später mit ./gradlew build bauen.
Im src-Ordner finden wir genau drei Dateien:
DemoApplication.java
: Diese Klasse ist der Einstiegspunkt zu unserer Spring-Boot-Anwendung. Sie ist mit der Annotation
@SpringBootApplication
versehen, die wir in
Kapitel 4
genauer untersuchen. Wird die
main()
-Methode dieser Klasse aufgerufen, startet unsere Anwendung.
application.properties
: Dies ist die zentrale Konfigurationsdatei für unsere Anwendung. Diese Datei ist leer, weil unsere Anwendung aktuell mit der Standardkonfiguration von Spring Boot zufrieden ist. Wir schauen uns die Konfiguration einer Spring-Boot-Anwendung im Detail in
Kapitel 5
an.
DemoApplicationTests.java
: Diese Klasse ist mit der Annotation
@SpringBootTest
versehen. Diese sorgt dafür, dass unsere Anwendung für jede
@Test
-Methode gestartet wird. Die leere Test-Methode
contextLoads()
hat augenscheinlich keinen Effekt, da unsere Anwendung aber für diesen Test gestartet wird, prüft dieser leere Test implizit, dass unsere Anwendung fehlerfrei starten kann. Schleicht sich zum Beispiel ein Konfigurationsfehler ein, schlägt dieser Test fehl. Er gibt uns also eine gewisse Sicherheit, obwohl die Test-Methode leer ist. Wir beschäftigen uns mit den Details des Testing mit Spring Boot in
Kapitel 7
.
Das sind auch schon alle Dateien unserer Anwendung. Mehr braucht unsere Anwendung nicht, um zu starten.
Was macht man als Erstes, wenn man ein neues Projekt lokal untersucht? Richtig, man führt den Build-Prozess aus, um Vertrauen in das Projekt zu gewinnen. Erst wenn der Build-Prozess problemlos läuft, sollten wir damit anfangen, Änderungen am Projekt vorzunehmen.
In unserem Fall nutzen wir Gradle als Build-Werkzeug, und der Build-Befehl lautet wie folgt:
> ./gradlew build
Mit diesem Befehl starten wir den Gradle Wrapper, der bei Bedarf Gradle herunterlädt, den Code kompiliert, die Tests ausführt und ein Artefakt erzeugt, das unsere Anwendung beinhaltet. Der Build-Prozess sollte mit einem befriedigenden BUILD SUCCESSFUL quittiert werden, wenn alles wie erwartet funktioniert.
Gradle legt während des Builds den Ordner build an, der alle Ergebnisse des Build-Prozesses beinhaltet. Im Ordner build/libs finden wir eine Datei demo-0.0.1-SNAPSHOT.jar (der Name kann variieren, je nachdem, was man bei der Initialisierung des Projekts auf start.spring.io eingegeben hat). Diese Datei ist ein sogenanntes »Fat JAR«, also eine JAR-Datei, die alles beinhaltet, was unsere Anwendung braucht. Wir diskutieren die Build-Prozesse und Fat JARs noch im Detail in Kapitel 6.
Nachdem wir sichergestellt haben, dass unsere Anwendung kompiliert, können wir nun etwas mit ihr herumspielen.
Wir können unsere Anwendung auf verschiedene Wege starten, um sie auszuprobieren.
Nachdem wir die Anwendung mit ./gradlew build gebaut haben, können wir die erzeugte JAR-Datei direkt mit dem Befehl java -jar <DATEINAME> starten. Diese Art, die Anwendung zu starten, ist einem echten Produktivszenario am ähnlichsten. Die JAR-Datei würde dabei auf eine Zielmaschine kopiert (am besten verpackt in einem Docker-Image, mehr dazu in Kapitel 30) und dort dann einfach gestartet.
In der täglichen Arbeit an unserer Anwendung wäre das allerdings unpraktisch, da wir zuvor immer den Build ausführen und auf deren Fertigstellung warten müssten.
Wir können unsere Anwendung auch direkt aus unserer IDE heraus starten. Wenn wir zum Beispiel in IntelliJ zu unserer Application-Klasse navigieren, sehen wir dort einen grünen »Play«-Button. Dieser startet die Anwendung, und wir können die Ausgabe direkt in der IDE-Konsole verfolgen.
Ein dritter Weg, die Anwendung zu starten, ist es, das Spring-Boot-Plugin von Gradle zu nutzen. Wir können einfach den Kommandozeilenbefehl ./gradlew bootRun ausführen, und die Anwendung wird gestartet. Der Vorteil dieses Vorgehens ist es, dass wir etwaige Konfigurationsparameter unserer Anwendung in der build.gradle.kts-Datei hinterlegen können, sodass alle Entwickler die Anwendung in derselben Konfiguration starten können, ohne immer manuell die Parameter eingeben zu müssen. Aktuell hat unsere Anwendung aber noch keinerlei Konfigurationsparameter, deshalb ist es egal, wie wir sie starten. Das Kapitel 5 befasst sich noch im Detail mit der Konfiguration einer Spring-Boot-Anwendung.
Egal, wie wir die Anwendung starten, das Ergebnis ist aktuell noch nicht sehr beeindruckend. Das Einzige, was wir sehen, sind einige Log-Ausgaben in der Konsole, die hoffentlich mit einer Meldung wie dieser hier enden:
Started DemoApplication in 1.352 seconds
Die Anwendung läuft also. Sie tut aber noch nichts. Das möchten wir nun ändern.
Das einfachste Element, das wir unserer Anwendung hinzufügen können, um ihr etwas Funktionalität zu geben, ist ein REST-Controller. Da wir das Modul spring-boot-starter-web unserem Projekt schon als Dependency hinzugefügt haben, haben wir bereits das Spring Web MVC Framework im Classpath, das alles mitbringt, was wir dafür brauchen.
Das MVC in »Spring Web MVC« steht für »Model, View, Controller«, ein beliebtes Pattern für Anwendungen, die eine Benutzeroberfläche anbieten. Der Controller nimmt Anfragen entgegen, erzeugt ein Model (oder lädt es aus einer Datenbank) und erzeugt eine View, die das Model für den Benutzer aufbereitet und zur Anzeige bringt. Diese View wird dem Benutzer dann angezeigt, in unserem Fall im Browser.
Ein einfacher »Hello World«-Controller sieht so aus:
Listing 2–1Ein einfacher Web-Controller
@RestController
public class HelloController {
@GetMapping("/hello/{name}")
public String hello(
@PathVariable("name") String name
) {
return String.format("Hello %s", name);
}
}
Wir können diese Klasse einfach neben der DemoApplication-Klasse ablegen und Spring Boot greift sie automatisch auf.
Mit der Annotation @RestController markieren wir die Klasse als REST-Controller. Der Controller bietet also eine REST-Schnittstelle an, die Webanfragen entgegennimmt und ein Textformat als Antwort zurückgibt (üblicherweise JSON). Die »View« aus MVC besteht in diesem Fall also einfach aus Text und nicht aus einer Benutzeroberfläche. Wie wir eine HTML-Benutzeroberfläche mit Spring MVC entwickeln, lernen wir in Kapitel 13.
Mit der Annotation @GetMapping an der hello()-Methode teilen wir Spring MVC mit, dass Anfragen über ein HTTP GET an den Pfad /hello/{name} an diese Methode geroutet werden sollen. Spring Boot startet automatisch einen Webserver, wenn wir Spring MVC benutzen, der HTTP-Anfragen entgegennimmt und an das Framework zur weiteren Verarbeitung übergibt. In der Parameterliste der hello()-Methode nutzen wir die Annotation @PathVariable ("name"), um den Wert der Variable {name} im Pfad in den Methodenparameter name zu injizieren.
In der Methode nutzen wir die name-Variable dann, um einen String zusammenzubauen und an den Aufrufer zurückzugeben. Diesen String können wir als das Model aus MVC betrachten, das in diesem Fall auch gleichzeitig die View ist, weil wir es ungefiltert an den Aufrufer zurückgeben.
Wir können die Anwendung nun starten und im Browser zum Beispiel http://localhost:8080/hello/bob aufrufen; wir sollten dann den String »Hello Bob« im Browser sehen. Spring Boot startet den Webserver standardmäßig auf Port 8080, das ist aber konfigurierbar.
Damit haben wir bereits eine erste funktionsfähige Spring-Boot-Anwendung gebaut! Die Annotationen, die wir benutzt haben, mögen vielleicht noch etwas magisch erscheinen, aber keine Angst: Die Annotationen müssen nicht auswendig gelernt werden. Im Rest des Buches werden wir diese Magie entzaubern und Ihnen Ressourcen an die Hand geben, die es Ihnen erleichtern, die richtigen Annotationen für Ihren Anwendungsfall zu finden. Für die Arbeit mit REST-Controllern tun wir das konkret in Kapitel 9.
Spring Boot basiert auf dem Spring Framework. Um Spring Boot zu verstehen, müssen wir die Grundlagen des Spring Frameworks verstehen, die wir in diesem Kapitel diskutieren.
Spring ist im Kern ein Dependency Injection Framework. Es bietet mittlerweile zwar eine Menge anderer Features, die das Entwicklerleben vereinfachen, aber diese bauen meist auf dem Dependency Injection Framework auf.
Dependency Injection wird oft mit Inversion of Control gleichgesetzt. Wir möchten die beiden Begriffe hier kurz erläuternd in das Spring Framework einordnen und diskutieren, wie Dependency Injection mit dem Spring Framework funktioniert.
Das Konzept von Inversion of Control (Kontrollumkehr) ist es, die Kontrolle über den Aufruf von Programmcode an ein Framework abzugeben. Dies kann zum Beispiel in Form einer Funktion geschehen, die wir selbst programmieren, dann aber an ein Framework übergeben, das sie anschließend zum richtigen Zeitpunkt aufruft. Diese Funktion nennen wir »Callback«.
Ein Beispiel für einen Callback ist eine Funktion, die in einer Serveranwendung ausgeführt werden soll, wenn eine bestimmte URL aufgerufen wird. Wir programmieren die Funktion, aber wir rufen sie nicht selbst auf. Stattdessen übergeben wir die Funktion an ein Framework, das auf HTTP-Anfragen auf einem bestimmten Port horcht, die Anfrage analysiert und nach bestimmten Parametern dann an eine der registrierten Callback-Funktionen weiterleitet. Das Spring-Web-MVC-Projekt basiert auf genau diesem Mechanismus. Wir werden uns später noch mit Web MVC auseinandersetzen.
Dependency Injection ist eine konkrete Ausprägung von Inversion of Control. Wie der Name andeutet, geht es bei Dependency Injection um Abhängigkeiten. Eine Klasse A ist abhängig von einer anderen Klasse B, wenn die Klasse A eine Methode von B aufruft. In Programmcode wird diese Abhängigkeit häufig in der Form eines Attributs in A vom Typ B ausgedrückt:
Listing 3–1Der Service erzeugt sein eigenes UserDatabase-Objekt.
In diesem Beispiel benötigt der GreetingService ein Objekt vom Typ UserDatabase, um seine Arbeit zu machen. Wenn wir ein Objekt vom Typ GreetingService instanziieren, instanziiert es automatisch ein Objekt vom Typ UserDatabase.
Die Klasse GreetingService ist also selbst dafür verantwortlich, die Abhängigkeit zu UserDatabase aufzulösen. Dies ist aus mehreren Gründen problematisch.
Zunächst erzeugt diese Lösung eine sehr starke Kopplung der beiden Klassen. Der GreetingService muss wissen, wie ein UserDatabase-Objekt erzeugt wird. Was wäre, wenn die Erzeugung eines UserDatabase-Objekts nicht so einfach ist? Um eine Datenbankverbindung zu öffnen, werden üblicherweise einige Parameter benötigt:
Listing 3–2Der Service braucht Parameter, um selbst ein UserDatabase-Objekt zu erzeugen.
Der GreetingService erzeugt immer noch seine eigene Instanz vom Typ UserDatabase, aber er muss plötzlich wissen, welche Parameter eine Datenbankverbindung benötigt. Die Kopplung zwischen GreetingService und UserDatabase ist soeben noch stärker geworden. Wir möchten diese Details im GreetingService gar nicht sehen!
Was, wenn andere Klassen in unserer Anwendung auch ein UserDatabase-Objekt benötigen? Wir möchten nicht, dass jede Klasse wissen muss, wie man ein UserDatabase-Objekt erzeugt!
Durch die starke Kopplung werden Details der Klasse UserDatabase über die ganze Codebase verteilt. Eine Änderung an UserDatabase würde also viele Änderungen an anderen Stellen im Code nach sich ziehen.
Dies erschwert nicht nur die Entwicklung des Anwendungscodes, sondern auch das Schreiben von Tests. Wenn wir die Klasse GreetingService testen möchten, brauchen wir in diesem Beispiel die URL und den Port einer echten Datenbank. Wenn wir ungültige Verbindungsparameter übergeben, funktioniert die Methode greet() nicht mehr!
Um die starke Kopplung zwischen den Klassen aufzuheben, ändern wir den Code, sodass wir die Abhängigkeit in den Konstruktor »injizieren« können:
Listing 3–3Der Service bekommt das UserDatabase-Objekt im Konstruktor »injiziert«.
Es gibt immer noch eine Kopplung zwischen GreetingService und UserDatabase, aber diese ist viel loser als vorher, denn GreetingService muss nun nicht mehr wissen, wie ein UserDatabase-Objekt erzeugt wird. Die Kopplung ist auf das erforderliche Minimum reduziert. Dieses Pattern wird »Constructor Injection« genannt, da wir die Abhängigkeiten einer Klasse in Form von Konstruktorparametern übergeben.
In einem Test können wir nun ein Mock-Objekt vom Typ UserDatabase erzeugen (zum Beispiel mit einer Mock-Bibliothek wie Mockito; mehr dazu in Kap. 7) und dieses an den GreetingService übergeben. Da wir das Verhalten des Mocks kontrollieren, brauchen wir keine Verbindung zu einer echten Datenbank mehr, um die Klasse GreetingService zu testen.
Im Anwendungscode instanziieren wir die Klasse UserDatabase nur einmal und übergeben diese Instanz an alle Klassen, die sie benötigen. Anders ausgedrückt »injizieren« wir die Abhängigkeit zu UserDatabase in die Konstruktoren der anderen Klassen.
Dieses »Injizieren« von Abhängigkeiten kann in einer echten Anwendung mit mehreren hundert Klassen sehr umständlich werden, da wir alle Klassen in der richtigen Reihenfolge instanziieren und die Abhängigkeiten zwischen ihnen explizit ausprogrammieren müssen. Die Folge ist sehr viel »Boilerplate«-Code, der sich häufig ändert und uns von der eigentlichen Entwicklung abhält.
Genau hier kommt Dependency Injection ins Spiel. Ein Dependency Injection Framework wie Spring übernimmt die Aufgabe, den Großteil der Klassen unserer Anwendung zu instanziieren, sodass wir uns nicht mehr darum kümmern müssen. Hier wird deutlich, dass Dependency Injection eine Ausprägung von Inversion of Control ist, denn wir geben die Kontrolle über die Instanziierung unserer Objekte an das Dependency Injection Framework ab.
Die Aufgabenteilung zwischen Spring und uns als Entwicklern sieht in etwa so aus:
Wir programmieren die Klassen
GreetingService
und
UserDatabase
.
Wir drücken die Abhängigkeit zwischen den beiden Klassen durch einen Parameter vom Typ
UserDatabase
im Konstruktor von
GreetingService
aus.
Wir teilen Spring mit, dass es die Kontrolle über die Klassen
GreetingService
und
UserDatabase
übernehmen soll.
Spring instanziiert die Klassen in der richtigen Reihenfolge, um Abhängigkeiten aufzulösen, und erzeugt ein Objektnetzwerk mit einem Objekt für jede übergebene Klasse.
Wenn wir ein Objekt vom Typ
GreetingService
oder
UserDatabase
benötigen, fragen wir Spring nach diesem Objekt.
In einer echten Anwendung verwaltet Spring natürlich nicht nur zwei Objekte, sondern ein komplexes Netzwerk von hunderten oder tausenden Objekten. Dieses Netzwerk wird in Spring »Application Context« genannt, da es den Kern unserer Anwendung ausmacht.
Wie der Application Context funktioniert, schauen wir uns im nächsten Abschnitt an.
Die Objekte im Application Context werden »Beans« genannt. Wenn man nicht aus der Java-Welt kommt, ist der Begriff »Bean« (Bohne) vermutlich eher verwirrend. Spring ist ein Framework für die Programmiersprache Java. Java ist der Name einer Insel in Indonesien, auf der Kaffee angebaut wird. Die Kaffee-Art wird ebenfalls »Java« genannt. Und Kaffee wird aus Kaffeebohnen hergestellt. In der Java Community hat man sich deshalb dafür entschieden, bestimmte Objekte in Java (der Programmiersprache) »Beans« zu nennen. Ziemlich weit hergeholt, aber der Begriff hat sich eingebürgert.
Der Application Context ist also im Grunde nur ein Netzwerk von Java-Objekten, genannt »Beans«. Spring instanziiert diese Beans für uns und löst die Abhängigkeiten zwischen den Beans über Constructor Injection auf.
Woher weiß Spring aber, welche Beans es für uns erzeugen und in seinem Application Context verwalten soll? Hier kommt der Begriff »Konfiguration« ins Spiel.
Eine Konfiguration im Kontext von Spring ist eine Definition der Beans, die wir für unsere Anwendung benötigen. Im einfachsten Fall ist dies eine Liste von Klassen. Spring nimmt diese Klassen, instanziiert sie und nimmt die entstandenen Objekte (Beans) in den Application Context auf.
Wenn die Instanziierung der Klassen nicht möglich ist (zum Beispiel, wenn ein Bean-Konstruktor eine andere Bean erwartet, die aber nicht Teil der Konfiguration ist), bricht Spring die Erstellung des Application Context mit einer Exception ab.
Dies ist einer der Vorteile, den Spring bietet: Eine fehlerhafte Konfiguration führt in den meisten Fällen dazu, dass die Anwendung gar nicht erst startet und somit keinen Schaden zur Laufzeit anrichten kann.
Es gibt mehrere Wege, eine Spring-Konfiguration zu erstellen. In den meisten Anwendungsfällen ist es komfortabel und praktisch, die Konfiguration in Java zu programmieren. In Anwendungsfällen, wo der Quellcode komplett frei von Abhängigkeiten zum Spring Framework sein soll, kann aber auch eine Konfiguration mit XML sinnvoll sein.
Zu den Anfängen von Spring musste der Application Context mit XML konfiguriert werden. Die Konfiguration mit XML ermöglicht es, die Konfiguration und den Code komplett voneinander zu trennen. Der Code muss nicht wissen, dass er von Spring verwaltet wird.
Eine Beispielkonfiguration in XML sieht so aus:
Listing 3–4Beans mit XML definieren
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="userDatabase"
class="de.springboot3.xml.UserDatabase"/>
<bean id="greetingService"
class="de.springboot3.xml.GreetingService">
<constructor-arg ref="userDatabase"/>
</bean>
</beans>
In dieser Konfiguration werden die Beans userDatabase und greetingService definiert. Jede Bean-Deklaration ist eine Anleitung für Spring, wie diese Bean instanziiert wird.
Die Klasse UserDatabase hat einen Default-Konstruktor ohne Parameter, weshalb es ausreicht, Spring den Namen der Klasse zu nennen. Die Klasse GreetingService hat einen Konstruktorparameter vom Typ UserDatabase, weshalb wir mit dem constructor-ref-Element auf die zuvor deklarierte userDatabase-Bean verweisen.
Mit dieser XML-Deklaration können wir nun einen Application Context erzeugen:
Listing 3–5Einen Application Context aus einer XML-Datei erzeugen
public class XmlConfigMain {
public static void main(String[] args) {
new ClassPathXmlApplicationContext(
"application-context.xml");
applicationContext.getBean(
GreetingService.class);
System.out.println(greetingService.greet(1));
}
}
Wir übergeben dem Konstruktor von ClassPathXmlApplicationContext die XML-Konfiguration, und Spring erzeugt daraus ein ApplicationContext-Objekt für uns.
Dieser ApplicationContext ist nun unser IoC-Container, und wir können ihn zum Beispiel nach einer Bean vom Typ GreetingService fragen.
Die Konfiguration per XML sieht in diesem Beispiel noch recht überschaubar aus, kann aber in größeren Anwendungen sehr umfangreich werden. Für uns als Java-Entwickler wäre es doch angenehm, wenn wir eine solch umfangreiche Konfiguration in Java selbst verwalten und die Vorteile des Java-Compilers und der IDE für uns nutzen könnten.
Spätestens seit dem Erfolg von Spring Boot wird die XML-Konfiguration meist nur noch in Sonderfällen und Legacy-Anwendungen genutzt, und die Konfiguration mit Java ist zum Standard geworden. Deshalb werden wir uns diesen Weg, auch »Java-Config« genannt, genauer anschauen.
Der Kern einer Java-Konfiguration ist eine Java-Klasse, die mit der Spring-Annotation @Configuration annotiert ist:
Listing 3–6Eine einfache @Configuration-Klasse
@Configuration
public class GreetingConfiguration {
@Bean
UserDatabase userDatabase() {
return new UserDatabase();
}
@Bean
GreetingService greetingService(
UserDatabase userDatabase) {
return new GreetingService(userDatabase);
}
}
Diese Konfiguration ist äquivalent zu der XML-Konfiguration aus dem letzten Abschnitt. Mit der Annotation @Configuration teilen wir Spring mit, dass diese Klasse einen Teil des Application Context beisteuert. Ohne diese Annotation wird Spring nicht aktiv.
Eine Konfigurationsklasse kann dann Factory-Methoden wie hier userDatabase() und greetingService() deklarieren, die jeweils ein Objekt erzeugen. Mit der Annotation @Bean markieren wir solche Factory-Methoden. Spring findet diese Methoden und ruft sie auf, um einen Application Context zu erzeugen.
Abhängigkeiten zwischen Beans, wie hier die Abhängigkeit von GreetingService zu UserDatabase, werden über Parameter der Factory-Methoden aufgelöst. In unserem Fall wird Spring zuerst die Methode userDatabase() aufrufen, um eine UserDatabase-Bean zu erzeugen, und diese dann in die Methode greetingService() übergeben, um eine GreetingService-Bean zu erzeugen.
Mithilfe der Klasse AnnotationConfigApplicationContext können wir dann einen ApplicationContext erzeugen:
Listing 3–7Einen Application Context aus einer Java-Config erzeugen
public class JavaConfigMain {
public static void main(String[] args) {
new AnnotationConfigApplicationContext(
GreetingConfiguration.class);
applicationContext.getBean(
GreetingService.class);
System.out.println(greetingService.greet(1));
}
}
Der Konstruktor von AnnotationConfigApplicationContext erlaubt uns auch, mehrere Konfigurationsklassen zu übergeben anstatt nur eine einzige. Dies ist hilfreich für größere Anwendungen, denn wir können die Konfiguration von vielen Beans in mehrere Konfigurationsklassen aufteilen, um die Übersicht zu bewahren.
Die Konfiguration Hunderter oder gar Tausender von Beans via Java für eine große Anwendung kann sehr mühselig werden. Um dies zu vereinfachen, bietet Spring die Möglichkeit, im Java-Classpath nach Beans zu »scannen«.
Dieser Scan wird mit der Annotation @ComponentScan aktiviert:
Listing 3–8Scannen nach Beans im Package de.springboot3
@Configuration
@ComponentScan("de.springboot3")
public class GreetingScanConfiguration {
}
Wie zuvor erstellen wir eine Konfigurationsklasse (annotiert mit @Configuration). Hier definieren wir die Beans aber nicht selbst in Form von Factory-Methoden annotiert mit der @Bean-Annotation, sondern fügen die neue Annotation @ComponentScan hinzu.
Mit dieser Annotation weisen wir Spring an, im Package de.springboot3 nach Beans zu scannen. Findet der Scan eine Klasse, die mit der Annotation @Component versehen ist, wird aus dieser Klasse eine Bean erzeugt (das heißt, die Klasse wird instanziiert und dem Spring Application Context hinzugefügt).
Wir müssen also einfach alle Klassen, für die Spring eine Bean erzeugen soll, wie folgt mit der @Component-Annotation versehen:
Listing 3–9Mit @Component annotierte Klassen werden zu Beans.
@Component
public class GreetingService {
// …
}
@Component
public class UserDatabase {
// ...
}
Abhängigkeiten zwischen den Beans werden wie zuvor durch Konstruktorparameter ausgedrückt, und Spring löst diese automatisch auf – dieser Mechanismus wird häufig auch »Autowiring« genannt.
@Bean vs. @Component
Die Annotationen @Bean und @Component drücken ein ähnliches Konzept aus: Beide markieren ein Bean, das dem Application Context von Spring hinzugefügt werden soll. Diese Ähnlichkeit kann insbesondere zu Anfang verwirren.
Der Hauptunterschied beider Ansätze ist, dass wir mit der @Bean-Annotation selbst die Kontrolle über die Instanziierung der Beans haben und mit der @Component-Annotation Spring die Instanziierung überlassen. @Bean ist nur an Methoden erlaubt und @Component nur an Klassen.
Der Java-Compiler hilft uns hier etwas, denn er meckert, wenn wir eine @Bean-Annotation an einer Klasse benutzen oder eine @Component-Annotation an einer Methode. Wir können sie also nicht verwechseln. Wir können aber sehr wohl Methoden und Klassen annotieren, die von Spring gar nicht erkannt werden!
Spring wertet die @Bean-Annotation nur innerhalb einer @Configuration-Klasse und die @Component-Annotation nur an Klassen aus, die von einem Component-Scan gefunden werden!
Spring schreibt uns nicht vor, wie wir die Beans unserer Anwendung konfigurieren. Wir können sie per XML oder per Java-Config konfigurieren oder gar beide Varianten miteinander kombinieren. Wir können auch explizite Bean-Definitionen per @Bean-Methode und einen Scan per @ComponentScan kombinieren:
Listing 3–10Mischen von Component-Scan und Java-Config
@Configuration
@ComponentScan("de.springboot3.java.mixed")
class MixedConfiguration {
@Bean
GreetingService greetingService(
UserDatabase userDatabase) {
return new GreetingService(userDatabase);
}
}
// no @Component annotation!
class GreetingService {…}
@Component
class UserDatabase {...}
In dieser Konfiguration erzeugt Spring eine Bean vom Typ UserDatabase, da die Klasse mit @Component annotiert ist und ein @ComponentScan konfiguriert ist. Die Bean vom Typ GreetingService hingegen wird durch die explizite @Bean-annotierte Factory-Methode definiert.
Modulare Konfiguration
Die Konfiguration einer größeren Anwendung mit hunderten Beans kann schnell unübersichtlich werden.
Die explizite Konfiguration per @Bean-Annotation hat den Vorteil, dass die Konfiguration der Beans in einigen wenigen @Configuration-Klassen gebündelt ist und so leicht zu begreifen ist.
Die implizite Konfiguration per @ComponentScan und @Component hat den Vorteil, dass wir nicht jede Bean selbst definieren müssen, ist dafür aber über sehr viele @Component-Annotationen und somit über die gesamte Codebase verteilt und schwieriger zu begreifen.
Ein bewährtes Prinzip ist es, die Spring-Konfiguration entlang der Architektur der Anwendung zu gestalten. Jedes Modul der Anwendung sollte in seinem eigenen Package liegen und seine eigene @Configuration-Klasse besitzen. In dieser @Configuration-Klasse können wir dann entweder einen @ComponentScan für das Modulpackage konfigurieren oder eine explizite Konfiguration per @Bean-Methoden. Um die Module zu einer Gesamtanwendung zusammenzuführen, erstellen wir eine übergeordnete @Configuration-Klasse, die einen @ComponentScan für das Hauptpackage definiert. Dieser Scan wird alle @Configuration-Klassen in diesem und den untergeordneten Packages aufgreifen.
Wir wissen nun, dass Spring uns einen IoC-Container bietet, der Objekte (Beans) für uns instanziiert und verwaltet. Das erspart uns die Arbeit, den Lebenszyklus dieser Objekte selbst zu verwalten.
Aber das ist erst der Anfang. Da Spring die Kontrolle über alle Beans hat, kann Spring eine Menge anderer Aufgaben für uns übernehmen. Zum Beispiel kann Spring die Aufrufe von Bean-Methoden abfangen, um zum Beispiel eine Datenbanktransaktion zu starten oder zu committen. Wir können Spring auch als Event-Bus nutzen. Dafür senden wir ein Event an den Spring-Container, und Spring leitet das Event an alle interessierten Beans weiter.
Diesen und vielen anderen Features werden wir im Rest des Buches auf den Grund gehen. Die Grundlage all dieser Features ist das Spring-Programmiermodell, dessen Kern wir in diesem Kapitel bereits kennengelernt haben. Dieses Programmiermodell ist eine Mischung aus Annotationen, Konventionen und Interfaces, die wir zu einer Gesamtanwendung kombinieren können.
Spring Boot baut auf dem Spring Framework auf und bietet eine Menge an zusätzlichen Funktionen und Integrationen. Etwas vereinfacht kann man sagen, dass das Spring Framework sich auf Funktionen rund um den Application Context fokussiert, während Spring Boot darauf aufbauend Funktionen bereitstellt, die man in vielen Anwendungen im Produktivbetrieb benötigt oder die das Entwicklerleben vereinfachen. In diesem Kapitel geben wir einen Überblick über die Kernfunktionen von Spring Boot, die wir in späteren Kapiteln noch genauer betrachten werden.
Der Name »Spring Boot« stammt von der Kernfunktionalität von Spring Boot: dem Bootstrapping. Mit »Bootstrapping« ist in diesem Fall der Prozess gemeint, einen Haufen Quellcode in eine Anwendung zu überführen und den Benutzern zugänglich zu machen.
Wir haben im vorherigen Kapitel bereits gesehen, wie wir mithilfe von Spring (ohne Spring Boot) einen ApplicationContext erzeugen und starten können. Wie aber machen wir aus diesem ApplicationContext eine richtige Anwendung? Genau hierbei hilft Spring Boot.
Anstatt dass wir manuell einen ApplicationContext erzeugen, lassen wir Spring Boot dies für uns tun. Wir stellen lediglich eine main()-Methode zur Verfügung, die als Spring-Boot-Anwendung deklariert ist:
Listing 4–1Code, um eine Spring-Boot-Anwendung zu starten
@SpringBootApplication
public class SpringBootBasicsApplication {
public static void main(String[] args) {
SpringApplication.run(
SpringBootBasicsApplication.class, args);
}
}
In der main()-Methode rufen wir die statische Methode SpringApplication.run() auf und reichen eine Klasse hinein, die mit der Annotation @SpringBootApplication versehen ist. Üblicherweise ist dies dieselbe Klasse, die auch die main()-Methode beinhaltet, wie im Beispiel oben.
Damit wir die Klassen @SpringBootApplication und SpringApplication zur Verfügung haben, müssen wir das Modul org.springframework.boot:spring-boot-starter als Dependency in unserer pom.xml- oder build.gradle.kts-Datei deklarieren.
Sobald wir die main-Methode zum Beispiel in unserer IDE starten, sehen wir folgende Log-Ausgabe:
Listing 4–2Log-Ausgabe beim Starten einer Spring-Boot-Anwendung
2023-07-03T11:48:41.624+10:00 INFO 44185 ---
[main] d.s.s.SpringBootBasicsApplication:
Starting SpringBootBasicsApplication using Java 18 with PID 44185
2023-07-03T11:48:41.629+10:00 INFO 44185 ---
[main] d.s.s.SpringBootBasicsApplication:
No active profile set, falling back to 1 default profile: "default"
2023-07-03T11:48:42.482+10:00 INFO 44185 ---
[main] d.s.s.SpringBootBasicsApplication:
Started SpringBootBasicsApplication in 1.288 seconds (process running for 1.87)
Process finished with exit code 0
Dieser Log-Auszug zeigt uns bereits einige interessante Dinge:
Wir nutzen Spring Boot in Version 3.0.1.
Wir nutzen Java 18.
Spring Boot startet im
default
-Profil, weil wir kein bestimmtes Profil definiert haben. Das bedeutet, dass Spring Boot das Konzept von »Profilen« unterstützt! Was genau ein Profil ist und wie wir es nutzen können, sehen wir später in
Kapitel 5
.
Es hat 1,288 Sekunden gedauert, die Spring-Boot-Anwendung zu starten.
Spring Boot hat das Logging für uns konfiguriert, sodass unsere Log-Ausgaben mit Datum, Log-Level, Prozess-ID, Thread-Name usw. formatiert sind.
Spring Boot hat also automatisch bereits einiges getan, das uns hilft, eine Anwendung zu starten, und wir werden im Rest des Buchs noch viele weitere Dinge lernen, die Spring Boot für uns tun kann.
Nach der oben gezeigten Log-Ausgabe beendet sich unsere Beispielanwendung allerdings direkt wieder, ohne irgendetwas getan zu haben, und gibt den Rückgabewert 0 an die Kommandozeile zurück, der angibt, dass die Anwendung erfolgreich beendet wurde. Das ist wenig hilfreich, deshalb schauen wir uns einige weitere Features von Spring Boot an, die uns helfen, eine echte Anwendung zu bauen.
In Kapitel 3 haben wir bereits gesehen, wie man einen ApplicationContext beeinflussen kann. Genau wie in einer Spring-Anwendung ist der ApplicationContext das Herz einer Spring-Boot-Anwendung, und wir können ihn genauso beeinflussen, wie wir es aus Spring gewohnt sind (mit einigen Extras, die wir in Kapitel 32 lernen werden).
Wenn wir uns den Quellcode der Annotation @SpringBootApplication anschauen, sehen wir Folgendes:
Listing 4–3Meta-Annotationen an der Annotation SpringBootApplication
Wir erkennen die @ComponentScan-Annotation aus dem letzten Kapitel! Zusätzlich sehen wir die Annotation der @SpringBootConfiguration, deren Quellcode wie folgt aussieht:
Listing 4–4Meta-Annotationen an der Annotation SpringBootConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
...
}
Hier erkennen wir die Annotation @Configuration wieder!
Annotationen an Annotationen nennt man Meta-Annotationen. Wenn Spring die Annotationen von Klassen untersucht, ist das Framework schlau genug, auch die Meta-Annotationen (und Meta-Meta-Annotationen usw.) auszuwerten. Für uns bedeutet das, dass unsere Hauptklasse SpringBootBasicsApplication sowohl mit @ComponentScan als auch mit @Configuration (meta-)annotiert ist.
Da unsere Hauptklasse mit @ComponentScan annotiert ist, wertet Spring automatisch alle @Component- und @Configuration-Annotationen im Package der Klasse und allen Packages darunter aus, instanziiert Objekte der annotierten Klassen und fügt sie dem ApplicationContext hinzu, wie in Kapitel 3 beschrieben.
Außerdem behandelt Spring unsere Hauptklasse als @Configuration-Klasse, sodass wir @Bean-annotierte Factory-Methoden direkt hier hinzufügen können, wenn wir dies möchten.
Um ein »Hello World« zu implementieren, könnten wir unsere Anwendung also zum Beispiel wie folgt erweitern:
Listing 4–5Eine minimale, lauffähige Spring-Boot-Anwendung
@SpringBootApplication
public class SpringBootBasicsApplication {
public static void main(String[] args) {
SpringApplication.run(
SpringBootBasicsApplication.class, args);
}
@Bean
public HelloBean helloBean() {
return new HelloBean();
}
static class HelloBean {
public HelloBean() {
System.out.println("Hello!");
}
}
}
Wir erzeugen eine Bean vom Typ HelloBean, die im Konstruktor eine Nachricht ausgibt. Starten wir die Anwendung erneut, sehen wir diese Log-Ausgabe, bevor die Anwendung stoppt.
Bisher haben wir gesehen, wie wir Spring Boot nutzen können, um eine Anwendung zu entwickeln, die eine einzige Aktion durchführt (in unserem Fall »Hello!« auf der Kommandozeile auszugeben). In Kapitel 15 werden wir sehen, wie wir dies nutzen können, um vollumfängliche Kommandozeilen-Programme zu entwickeln.
Viele Anwendungen, die wir heutzutage entwickeln, sind aber Webanwendungen und keine Kommandozeilen-Programme, weshalb wir uns als Nächstes den Embedded-Webserver-Features von Spring Boot widmen.
Traditionell wurden Java-basierte Webanwendungen auf Application-Servern wie Tomcat oder JBoss deployt. Die Anwendung wurde dabei als WAR-Datei (Web Archive) verpackt und auf den Server kopiert, der das Archiv dann auspackte, einige Metadaten aus Deskriptor-XML-Dateien auslas und HTTP-Requests an die von der Anwendung definierten Endpoints weiterleitete.
Spring Boot hat das traditionelle Deployment von Webanwendungen auf den Kopf gestellt. Anstatt eine WAR-Datei zu erzeugen und auf einen Server zu kopieren, erzeugen wir mit Spring Boot standardmäßig eine »runnable« JAR-Datei (auch »Fat JAR« genannt), die unseren Anwendungscode, alle Dependencies und einen Webserver beinhaltet. Starten wir diese JAR-Datei mit dem Befehl java -jar app.jar, so startet Spring Boot automatisch den Webserver, der dann HTTP-Requests entgegennimmt und an unsere Anwendung weiterleitet.
Möchten wir eine Webanwendung entwickeln, müssen wir lediglich das Modul org.springframework.boot:spring-boot-starter-web als Depedency in unserer pom.xml- oder build.gradle.kts-Datei ergänzen.
Wenn wir die Anwendung dann starten, sehen wir die folgende Log-Ausgabe zusätzlich zu den Log-Ausgaben von zuvor:
Listing 4–6Meldung nach einem erfolgreichen Start des Embedded-Tomcat-Servers
Tomcat started on port(s): 8080 (http) with context path ''
Spring Boot hat automatisch einen Tomcat-Server gestartet, der auf Port 8080 horcht, um HTTP-Requests zu empfangen. Unsere Anwendung stoppt nun nicht mehr direkt nach dem Start, sondern wartet auf HTTP-Requests so lange, bis sie beendet wird (oder abstürzt).
Der Embedded Webserver macht die Auslieferung einer Spring-Boot-Webanwendung sehr einfach. Wir können die JAR-Datei zum Beispiel in ein Docker-Image verpacken und mit dem Befehl java -jar app.jar starten. Mit dem Profilkonzept, das wir bereits in der Log-Ausgabe oben gesehen haben, macht Spring Boot es uns auch sehr einfach, die Anwendung für verschiedene Umgebungen (development, staging, production, …) zu konfigurieren. Hierfür müssen wir lediglich eine Datei wie application-<profile>.yml mit in die JAR-Datei packen (oder neben der JAR-Datei ablegen) und das Profil aktivieren. Dies werden wir im Detail noch in Kapitel 5 kennenlernen.
Ein weiteres Kernfeature von Spring Boot ist das Dependency Management.
Da wir in unserer Anwendung nicht jedes Rad neu erfinden möchten, nutzen wir häufig eine große Anzahl an Bibliotheken, die wir in unserer pom.xml- oder build.gradle.kts-Datei als Dependencies definieren. Jede dieser Libraries existiert in Versionen, die mit unserem Code (und den anderen Bibliotheken) kompatibel sind, und oft in Versionen, die veraltet (oder zu neu) und mit unserer Anwendung nicht kompatibel sind. Wir müssen die Versionen all unserer Dependencies verwalten, sodass sie stets miteinander kompatibel bleiben.
Hier schafft Spring Boot Abhilfe. Spring Boot bietet uns eine sogenannte »Bill of Materials« (BOM), die eine Menge an Libraries und deren Versionen definiert, sodass sie stets miteinander und mit der aktuellen Spring-Boot-Version kompatibel sind. Diese BOM beinhaltet natürlich nicht alle Bibliotheken, die es gibt, aber alle Bibliotheken, die von Spring Boot selbst oder von einer der offiziellen Integrationen zwischen Spring Boot und anderen Produkten (wie zum Beispiel Datenbanken) benötigt werden.
Um die Spring-Boot-BOM zu nutzen, müssen wir lediglich das Modul org.springframework.boot:spring-boot-dependencies in derselben Version wie Spring Boot in unsere pom.xml- oder build.gradle.kts-Datei importieren. Nutzen wir das Spring-Boot-Plugin für Maven oder Gradle, erledigt das Plugin dies automatisch für uns. Wenn wir dann eine Dependency zu einer Bibliothek deklarieren, können wir die Version der Bibliothek einfach weglassen und sie wird automatisch in der Version geladen, die in der Spring-Boot-BOM definiert ist. In Kapitel 6 gehen wir in die Details des Maven- und Gradle-Plugins für Spring Boot.
Spring Boot bringt einige Integrationen mit häufig genutzten Bibliotheken mit, sodass wir diese Integrationen nur noch konfigurieren, aber nicht mehr selbst programmieren müssen.
Ein Beispiel hierfür ist die Integration mit einer Datenbank. Fügen wir beispielsweise das Modul org.postgresql:postgresql in unserer pom.xml- oder build.gradle.kts-Datei hinzu (ohne Versionsnummer, denn dank Spring Boots Dependency Management wird automatisch die aktuelle kompatible Version genutzt), erkennt Spring Boot den Postgre SQL-Treiber im Classpath und stellt automatisch ein DataSource-Objekt im ApplicationContext zur Verfügung, das wir dann nutzen können, um auf die Datenbank zuzugreifen. Wir müssen Spring Boot lediglich noch die URL, den Benutzernamen und das Passwort der Datenbank mitteilen, indem wir einige Konfigurationsparameter in der zentralen Konfigurationsdatei von Spring Boot namens application.yml hinzufügen.
Das von Spring Boot zur Verfügung gestellte DataSource-Objekt kann von uns direkt genutzt werden. Es wird aber auch von weiteren Integrationen genutzt. Fügen wir beispielsweise eine Dependency zu Spring Data JPA oder Spring Data JDBC hinzu, erkennt Spring Boot dies ebenfalls und stellt diesen das DataSource-Objekt zur Verfügung. Wir müssen die DataSource dann nicht mehr direkt benutzen, sondern können die Abstraktionen von Spring Data nutzen, um auf die Datenbank zuzugreifen, was uns das Leben deutlich vereinfacht.
Dieses Beispiel der Datenbankintegration folgt einem typischen Muster für Spring-Boot-Integrationen. Spring Boot erkennt eine Dependency im Classpath und konfiguriert automatisch einige Beans im Application Context, die dann von uns (oder anderen Integrationen) genutzt werden können. Benötigt die Integration noch weitere Konfigurationsparameter, können wir diese in der Datei application.yml zur Verfügung stellen. Müssen wir die von der Integration zur Verfügung gestellten Beans darüber hinaus noch beeinflussen, können wir die Beans im Application Context »überschreiben«, indem wir sie selbst als @Bean oder @Component definieren.
Die Integration mit einer Datenbank wird in so vielen Anwendungen benötigt, dass Spring Boot sie im Kern bereits mitliefert. Andere Integrationen (wie zum Beispiel mit einem Cache oder einem Messaging-System) werden nicht in jeder Anwendung benötigt, sodass wir sie erst mit einem sogenannten »Starter« aktivieren müssen. Möchten wir in unserer Anwendung beispielsweise einen Redis-Cache benutzen, müssen wir das Modul org.springframework.boot:spring-boot-starter-data-redis hinzufügen, das dann die Integration mit Redis aktiviert. Ein »Starter« ist also eine kleine Bibliothek, die uns den »Start« mit einem bestimmten Feature oder einer Integration vereinfacht. Wir setzen uns mit Startern in Kapitel 32 noch näher auseinander.
Neben dem Bootstrapping und dem Embedded Webserver bringt Spring Boot noch einige Features mit, die es uns deutlich erleichtern, eine Anwendung in Produktion zu betreiben.
Eines der wichtigsten Features für den Produktionsbetrieb ist Logging. Wir haben bereits die automatische Konfiguration der Log-Ausgaben gesehen, sodass zum Beispiel das Datum und der Name des Threads in jeder Logzeile auftauchen, was uns das Debugging erleichtert. Spring Boot setzt hierfür auf den De-facto-Standard SLF4J, sodass wir lediglich ein Logger-Objekt definieren müssen und dieses dann für Log-Ausgaben nutzen können. Wir schauen uns das Thema Logging noch detaillierter in Kapitel 29 an.
Auch das Profilfeature haben wir bereits gesehen. Wir können Konfigurationsparameter definieren, die je nach Profil einen unterschiedlichen Wert besitzen. Beim Start der Anwendung können wir dann angeben, welche Profile (also welche Konfigurationswerte) aktiviert werden sollen. So können wir dieselbe Anwendung in unterschiedlichen Umgebungen betreiben und müssen lediglich die Konfiguration pro Umgebung anpassen. Dieses Feature ist aus der modernen Softwareentwicklung nicht mehr wegzudenken, da es eine ganze Klasse von umgebungsspezifischen Bugs verhindert. Wir gehen auf Profile und Konfigurationsparameter in Kapitel 5 ein.
Mit dem sogenannten »Actuator«-Modul bringt Spring Boot auch ein Modul mit, das uns Einblicke in eine laufende Anwendung gibt1. Sobald wir das Modul als Dependency hinzufügen, werden diverse Metriken wie freier/belegter Hauptspeicher und freie/belegte Rechenkapazität über den Endpoint/actuator zugänglich gemacht. Von dort können diese Metriken dann zum Beispiel von Observability-Tools abgefragt und in Dashboards verfügbar gemacht werden, um immer einen Blick auf die Produktionsumgebung zu haben. Wir schauen uns das Thema Observability und Actuator im Detail in Kapitel 29 an.
Dies war nur ein Ausschnitt der Features, die Spring Boot mitbringt. Spring Boot bietet eine Fülle von anderen Features, die uns die Entwicklung und den Betrieb einer Anwendung erleichtern und die wir im Rest dieses Buches beschreiben.
Serveranwendungen besitzen oft eine Vielzahl an Konfigurationsparametern, die wir in eine Konfigurationsdatei oder in Umgebungsvariablen auslagern möchten. In diesem Kapitel schauen wir uns an, wie wir eine solche externalisierte Konfiguration mit Spring Boot umsetzen können.
Serveranwendungen sind häufig auf externe Services angewiesen. Sie müssen beispielsweise eine Verbindung zu einer Datenbank, einem Cache oder einem Messaging-System aufbauen oder sie sprechen zur Laufzeit mit einem anderen (Micro-)Service. Damit diese Verbindungen erfolgreich aufgebaut werden können, muss die Anwendung wissen, wie sie diese externen Services erreichen kann. Üblicherweise muss hierfür die URL des externen Service hinterlegt werden und ein Passwort oder Token, um sich gegenüber diesem Service zu authentifizieren.