Spring Boot Aufbau-Kurs – Tag 1 von 10
Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting

🗺️ Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| → 1 | Auto-Configuration & Custom Starter | 👉 DU BIST HIER! |
| 2 | Spring Data JPA Basics | 📜 Kommt als nächstes |
| 3 | JPA Relationships & Queries | 🔒 Noch nicht freigeschaltet |
| 4 | Spring Security – Part 1 | 🔒 Noch nicht freigeschaltet |
| 5 | Spring Security – Part 2 | 🔒 Noch nicht freigeschaltet |
| 6 | Caching & Serialisierung | 🔒 Noch nicht freigeschaltet |
| 7 | Messaging & Email | 🔒 Noch nicht freigeschaltet |
| 8 | Testing & Dokumentation | 🔒 Noch nicht freigeschaltet |
| 9 | Spring Boot Actuator | 🔒 Noch nicht freigeschaltet |
| 10 | Template Engines & Microservices | 🔒 Noch nicht freigeschaltet |
Modul: Spring Boot Aufbau-Kurs (10 Arbeitstage)
Gesamt-Dauer: 10 Arbeitstage (je 8 Stunden)
Dein Ziel: Die Spring Boot „Magie“ verstehen & deinen eigenen Starter bauen
📋 Voraussetzungen für diesen Tag
Du brauchst:
- ✅ Java SE Grundlagen (40 Tage) abgeschlossen
- ✅ Java Web Basic + Aufbau (20 Tage) abgeschlossen
- ✅ Spring Boot Basic (10 Tage) abgeschlossen
- ✅ Du verstehst REST APIs, Dependency Injection und Maven
Tag verpasst oder später eingestiegen?
Kein Problem! Dieser Blogbeitrag deckt genau den Stoff von Tag 1 ab. Du kannst das komplette Projekt unten herunterladen und es in deinem eigenen Tempo durcharbeiten!
⚡ Kurze Zusammenfassung – Das Wichtigste in 30 Sekunden
Heute lüften wir das Geheimnis hinter Spring Boot’s „Magie“: Du baust einen eigenen Custom Starter (greeting-spring-boot-starter), der automatisch einen Service bereitstellt, der über application.properties konfigurierbar ist – genau wie spring-boot-starter-web oder andere offizielle Starter!
Du lernst heute:
- ✅
@ConfigurationPropertiesfür typsichere Konfiguration - ✅
@AutoConfigurationmit Conditional-Annotations - ✅
spring.factoriesfür die Registrierung deines Starters - ✅ Die drei Säulen jedes Starters: Dependencies, Auto-Configuration, Properties
Am Ende des Tages: Du verstehst wie Spring Boot „denkt“ und kannst eigene wiederverwendbare Starter für deine Projekte bauen!
💻 Was du heute baust:
Heute bauen wir gemeinsam einen greeting-spring-boot-starter – einen vollwertigen Custom Starter, der automatisch einen GreetingService bereitstellt. Dabei lüften wir das Geheimnis, wie Spring Boot seine „Magie“ macht: Properties aus application.properties werden automatisch gebunden, Beans werden conditional erstellt, und alles funktioniert ohne dass du etwas konfigurieren musst!
🎯 Dein Lernpfad heute:
Du arbeitest heute in mehreren aufbauenden Schwierigkeitsstufen. Arbeite in deinem eigenen Tempo durch die Schritte:
🟢 GRUNDLAGEN (Schritte 1-7)
Was du lernst:
- Das Mysterium hinter Spring Boot verstehen
- Multi-Modul Projekt-Struktur für einen Custom Starter aufbauen
- Service Interface und Implementierung erstellen
@ConfigurationPropertiesfür typsichere Konfiguration nutzen@AutoConfigurationund Conditionals verstehen- Den Starter via
AutoConfiguration.importsregistrieren - Unit Tests für Properties und Service schreiben
Ziel: Du hast einen funktionierenden, getesteten Custom Starter.
Zeitaufwand: Ca. 5-6 Stunden
🟡 PROFESSIONALS (Schritte 8-10)
Was du lernst:
- ApplicationContextRunner – der professionelle Weg Auto-Configuration zu testen
- Den Starter in einem Demo-Projekt einsetzen
- Debug-Modus nutzen um Auto-Configuration zu analysieren
- REST-Controller mit MockMvc testen
Ziel: Du weißt wie du Starter professionell testest und in Production einsetzt.
Zeitaufwand: Ca. 2-3 Stunden
🔵 BONUS: Advanced Features (Schritt 11)
Was du baust:
- Eigene Implementierung überschreiben (Überschreibbarkeit)
- Nested Properties für komplexe Konfigurationen
- Advanced Conditional-Szenarien
Ziel: Du beherrschst fortgeschrittene Starter-Features für Enterprise-Anwendungen.
Zeitaufwand: Ca. 1-2 Stunden (optional)
💡 Tipp: Die Grundlagen (Schritte 1-7) sind essenziell – ohne sie verstehst du nicht, wie Spring Boot funktioniert. Tests sind keine Option, sondern Pflicht! Professional (🟡) zeigt dir die Best Practices für produktionsreifen Code. Bonus (🔵) ist perfekt wenn du eigene komplexe Starter entwickeln möchtest.
🟢 GRUNDLAGEN
Schritt 1: Das Mysterium verstehen
Hi! 👋
Elyndra hier – und heute tauchen wir gemeinsam in die Tiefen von Spring Boot ein.
Kennst du das Gefühl? Du fügst spring-boot-starter-web zu deinem Projekt hinzu und plötzlich:
- Tomcat startet wie von Geisterhand
- Jackson serialisiert JSON automatisch
- Error Pages funktionieren einfach
- Alles läuft – ohne eine Zeile Konfiguration!
„Das ist doch Magie!“ hörte ich letzte Woche von Nova. Und weißt du was? Sie hatte recht – es fühlt sich wie Magie an. Aber wie bei guter Magie steckt dahinter ein elegantes System, das wir heute entschlüsseln.
Die drei Säulen der Spring Boot „Magie“
Jeder Starter basiert auf drei Prinzipien:
1. Dependencies – Die benötigten Libraries werden mitgebracht
Wenn du spring-boot-starter-web hinzufügst, bekommst du automatisch Tomcat, Jackson, Spring MVC und alles was du brauchst. Du musst nicht jede Dependency einzeln hinzufügen!
2. Auto-Configuration – Beans werden automatisch erstellt
Spring Boot analysiert deinen Classpath und erstellt automatisch die richtigen Beans. Tomcat da? → Webserver wird konfiguriert. Jackson da? → JSON-Serialisierung läuft.
3. Properties – Konfiguration erfolgt deklarativ über application.properties
Statt Java-Code zu schreiben, schreibst du einfach server.port=8080 und Spring Boot versteht es.
Das ist wie ein gut organisiertes Werkzeugset: Alles was du brauchst ist dabei, nichts fehlt, nichts ist zu viel.
Unser Lernprojekt: greeting-spring-boot-starter
Heute bauen wir einen Starter, der so funktioniert:
java
// Du schreibst nur:
@Autowired
private GreetingService greetingService;
String greeting = greetingService.greet("Anna");
// Output: "Hello, Anna!"
Konfigurierbar über application.properties:
properties
greeting.message=Guten Tag greeting.uppercase=true greeting.prefix=[VIP] # Output wird: "[VIP] GUTEN TAG, ANNA!"
Das Schöne: Wir bauen das Schritt für Schritt nach und du verstehst dabei, wie Spring Boot „denkt“.
Schritt 2: Multi-Modul Projekt-Struktur aufsetzen

Lass uns strukturiert vorgehen – wie beim Refactoring von Legacy-Code: Erst die Architektur verstehen, dann implementieren.
Wichtig: Wir nutzen ein Multi-Modul Maven Projekt. Das hat mehrere Vorteile:
- Saubere Trennung zwischen Starter und Demo
- Gemeinsames Dependency Management
- Einfacher zu verteilen und zu testen
Projekt-Struktur
greeting-spring-boot-starter-project/
├── pom.xml # Parent POM
├── greeting-spring-boot-starter/ # Der Starter
│ ├── pom.xml
│ └── src/
│ ├── main/java/com/javafleet/greeting/
│ │ ├── GreetingService.java
│ │ ├── DefaultGreetingService.java
│ │ ├── GreetingProperties.java
│ │ └── GreetingAutoConfiguration.java
│ ├── main/resources/META-INF/spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ └── test/java/com/javafleet/greeting/
│ ├── GreetingPropertiesTest.java
│ ├── DefaultGreetingServiceTest.java
│ └── GreetingAutoConfigurationTest.java # ← DAS FEHLT OFT!
│
└── greeting-spring-boot-starter-demo/ # Demo-Anwendung
├── pom.xml
└── src/
├── main/java/com/javafleet/demo/
│ ├── DemoApplication.java
│ └── GreetingController.java
├── main/resources/
│ └── application.properties
└── test/java/com/javafleet/demo/
├── DemoApplicationTest.java
└── GreetingControllerTest.java
Warum diese Struktur? Jede Klasse hat eine klare Verantwortung – Single Responsibility Principle in Aktion!
Parent POM (pom.xml im Root)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.javafleet</groupId>
<artifactId>greeting-spring-boot-starter-project</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<name>Greeting Spring Boot Starter - Parent</name>
<description>Multi-Module Project: Custom Starter mit Demo und Tests</description>
<modules>
<module>greeting-spring-boot-starter</module>
<module>greeting-spring-boot-starter-demo</module>
</modules>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.2.5</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Boot BOM - Alle Versionen zentral verwaltet! -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
Was macht dieser code?
Das Parent POM definiert die gemeinsame Konfiguration für alle Module. Der <dependencyManagement>-Block importiert das Spring Boot BOM (Bill of Materials), wodurch alle Spring-Versionen zentral verwaltet werden. Du musst in den Child-POMs keine Versionen mehr angeben!
In der Praxis bedeutet das:
Statt in jedem Modul <version>3.2.5</version> zu schreiben, lässt du die Version einfach weg. Maven holt sie automatisch aus dem BOM. Das verhindert Versionskonflikte!
Starter POM (greeting-spring-boot-starter/pom.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.javafleet</groupId>
<artifactId>greeting-spring-boot-starter-project</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>greeting-spring-boot-starter</artifactId>
<packaging>jar</packaging>
<name>Greeting Spring Boot Starter</name>
<description>Auto-configures a customizable GreetingService</description>
<dependencies>
<!-- Spring Boot Auto-Configuration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- Configuration Properties Processor für IDE Auto-Complete -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- ==================== TEST DEPENDENCIES ==================== -->
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- AssertJ für fluent Assertions -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot Test - enthält ApplicationContextRunner! -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Context für Integration Tests -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Was macht dieses code?
Das Starter-POM definiert die Dependencies für unseren Starter. Beachte: Keine Versionen! Die kommen aus dem Parent-BOM.
In der Praxis bedeutet das:
Der spring-boot-configuration-processor ist optional – er wird nur beim Kompilieren gebraucht um IDE-Metadaten zu generieren, aber nicht zur Laufzeit. Die Test-Dependencies (scope=test) werden nur für Tests geladen.
Wichtig für Spring Boot 3:
- ✅ Minimum Java 17
- ✅ Spring Boot 3.2.x oder höher
- ✅
spring-boot-configuration-processorfür IDE Auto-Complete - ✅
spring-boot-testfür ApplicationContextRunner – das ist der Schlüssel!
Schritt 3: Das Service Interface
Beginnen wir mit dem einfachsten Teil – dem Interface. Das ist wie das Fundament eines Hauses: solide und klar definiert.
GreetingService.java
java
package com.javafleet.greeting;
/**
* Service für Begrüßungen.
*
* <p>Dieses Interface definiert den Vertrag für Greeting-Implementierungen.
* Der Spring Boot Starter stellt automatisch eine Default-Implementierung bereit,
* die über {@code application.properties} konfigurierbar ist.</p>
*
* <h2>Verwendung:</h2>
* <pre>{@code
* @Autowired
* private GreetingService greetingService;
*
* String message = greetingService.greet("World");
* // Output: "Hello, World!"
* }</pre>
*
* @author Java Fleet Systems Consulting
* @since 1.0.0
* @see DefaultGreetingService
* @see GreetingProperties
*/
public interface GreetingService {
/**
* Erstellt eine Begrüßung für den angegebenen Namen.
*
* @param name Der zu begrüßende Name (darf nicht {@code null} sein)
* @return Die formatierte Begrüßung, niemals {@code null}
* @throws IllegalArgumentException wenn {@code name} null oder leer ist
*/
String greet(String name);
}
Was macht dieses code?
Das Interface definiert den Vertrag für unseren Service. Jede Implementierung muss die Methode greet(String name) bereitstellen.
In der Praxis bedeutet das:
Durch das Interface können User später ihre eigene Implementierung bereitstellen (Überschreibbarkeit). Das Javadoc dokumentiert das erwartete Verhalten inklusive Exception-Handling.
Schritt 4: Configuration Properties
Jetzt kommt der wichtigste Teil: Wie binden wir application.properties an Java-Klassen?
GreetingProperties.java
java
package com.javafleet.greeting;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration Properties für den Greeting Starter.
*
* <p>Alle Properties mit dem Prefix {@code greeting} werden automatisch
* an diese Klasse gebunden.</p>
*
* <h2>Beispiel application.properties:</h2>
* <pre>
* greeting.message=Hello
* greeting.uppercase=true
* greeting.prefix=[VIP]
* greeting.enabled=true
* greeting.format.emoji=👋
* greeting.format.separator=,
* </pre>
*
* @author Java Fleet Systems Consulting
* @since 1.0.0
*/
@ConfigurationProperties(prefix = "greeting")
public class GreetingProperties {
/**
* Die Begrüßungs-Message.
* Standard: "Hello"
*/
private String message = "Hello";
/**
* Soll die Ausgabe in Großbuchstaben sein?
* Standard: false
*/
private boolean uppercase = false;
/**
* Optionaler Prefix vor der Message.
*/
private String prefix = "";
/**
* Aktiviert/deaktiviert den Greeting Service.
* Standard: true
*/
private boolean enabled = true;
/**
* Format-Optionen für erweiterte Anpassung.
*/
private Format format = new Format();
// ==================== GETTER & SETTER ====================
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public boolean isUppercase() {
return uppercase;
}
public void setUppercase(boolean uppercase) {
this.uppercase = uppercase;
}
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Format getFormat() {
return format;
}
public void setFormat(Format format) {
this.format = format;
}
// ==================== NESTED CLASS ====================
/**
* Nested Configuration für Format-Optionen.
* Konfigurierbar über {@code greeting.format.*}
*/
public static class Format {
/**
* Optionales Emoji das der Begrüßung hinzugefügt wird.
* Beispiel: "👋" → "👋 Hello, World! 👋"
*/
private String emoji = "";
/**
* Separator zwischen Message und Name.
* Standard: ", "
*/
private String separator = ", ";
/**
* Suffix nach dem Namen.
* Standard: "!"
*/
private String suffix = "!";
public String getEmoji() {
return emoji;
}
public void setEmoji(String emoji) {
this.emoji = emoji;
}
public String getSeparator() {
return separator;
}
public void setSeparator(String separator) {
this.separator = separator;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
}
Was macht dieses code?
Die Annotation @ConfigurationProperties(prefix = "greeting") sagt Spring Boot: „Suche in application.properties nach Properties die mit greeting. beginnen und binde sie automatisch an die Felder dieser Klasse.“
In der Praxis bedeutet das:
properties
# In application.properties greeting.message=Guten Tag greeting.format.emoji=👋
Spring Boot macht automatisch:
java
properties.setMessage("Guten Tag");
properties.getFormat().setEmoji("👋");
Die Nested Class Format ermöglicht hierarchische Properties wie greeting.format.emoji.
Schritt 5: Die Standard-Implementierung
Jetzt implementieren wir das Interface mit einer konkreten Klasse.
DefaultGreetingService.java
java
package com.javafleet.greeting;
/**
* Standard-Implementierung des {@link GreetingService}.
*
* <p>Diese Klasse wird automatisch von der {@link GreetingAutoConfiguration}
* als Spring Bean erstellt, sofern keine andere Implementierung vorhanden ist.</p>
*
* @author Java Fleet Systems Consulting
* @since 1.0.0
*/
public class DefaultGreetingService implements GreetingService {
private final GreetingProperties properties;
/**
* Constructor Injection - Best Practice in Spring!
*
* @param properties Die Konfiguration aus application.properties
* @throws IllegalArgumentException wenn properties null ist
*/
public DefaultGreetingService(GreetingProperties properties) {
if (properties == null) {
throw new IllegalArgumentException("GreetingProperties must not be null");
}
this.properties = properties;
}
@Override
public String greet(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name must not be null or empty");
}
StringBuilder greeting = new StringBuilder();
// 1. Prefix hinzufügen (falls konfiguriert)
String prefix = properties.getPrefix();
if (prefix != null && !prefix.isEmpty()) {
greeting.append(prefix).append(" ");
}
// 2. Message hinzufügen
greeting.append(properties.getMessage());
// 3. Separator und Name
greeting.append(properties.getFormat().getSeparator());
greeting.append(name.trim());
// 4. Suffix
greeting.append(properties.getFormat().getSuffix());
// 5. Uppercase (vor Emoji, damit Emoji nicht betroffen ist)
String result = greeting.toString();
if (properties.isUppercase()) {
result = result.toUpperCase();
}
// 6. Emoji hinzufügen (falls konfiguriert)
String emoji = properties.getFormat().getEmoji();
if (emoji != null && !emoji.isEmpty()) {
result = emoji + " " + result + " " + emoji;
}
return result;
}
/**
* Gibt die aktuellen Properties zurück (für Debugging).
*/
public GreetingProperties getProperties() {
return properties;
}
}
Was macht dieses code?
Die Implementierung baut die Begrüßung Schritt für Schritt zusammen: Prefix → Message → Separator → Name → Suffix → Uppercase → Emoji.
In der Praxis bedeutet das:
Die Input-Validierung am Anfang verhindert NullPointerException und gibt aussagekräftige Fehlermeldungen. Das ist defensive Programmierung – wichtig für Libraries die andere nutzen!
Schritt 6: Auto-Configuration
Jetzt kommt der spannendste Teil: Die Auto-Configuration! Hier entscheiden wir, wann und wie unser Service automatisch erstellt wird.
GreetingAutoConfiguration.java
java
package com.javafleet.greeting;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* Auto-Configuration für den Greeting Spring Boot Starter.
*
* <p>Diese Klasse wird automatisch von Spring Boot geladen wenn:
* <ul>
* <li>Der Starter im Classpath ist</li>
* <li>Die {@link GreetingService} Klasse verfügbar ist</li>
* <li>{@code greeting.enabled} nicht explizit auf {@code false} gesetzt ist</li>
* </ul>
* </p>
*
* @author Java Fleet Systems Consulting
* @since 1.0.0
*/
@AutoConfiguration
@ConditionalOnClass(GreetingService.class)
@EnableConfigurationProperties(GreetingProperties.class)
public class GreetingAutoConfiguration {
/**
* Erstellt den {@link GreetingService} Bean.
*
* <p><b>@ConditionalOnMissingBean:</b> Nur erstellen wenn der User nicht
* bereits eine eigene Implementierung bereitgestellt hat!</p>
*
* <p><b>@ConditionalOnProperty:</b> Nur erstellen wenn nicht explizit
* disabled. {@code matchIfMissing = true} bedeutet: Default ist aktiviert!</p>
*/
@Bean
@ConditionalOnMissingBean(GreetingService.class)
@ConditionalOnProperty(
prefix = "greeting",
name = "enabled",
havingValue = "true",
matchIfMissing = true
)
public GreetingService greetingService(GreetingProperties properties) {
return new DefaultGreetingService(properties);
}
}

Was macht dieses code?
Die Conditional-Annotations steuern wann der Bean erstellt wird:
@AutoConfiguration– Markiert als Auto-Configuration (Spring Boot 3+)@ConditionalOnClass– Nur laden wenn die Klasse im Classpath ist@ConditionalOnMissingBean– Nur erstellen wenn keiner existiert (Überschreibbarkeit!)@ConditionalOnProperty– Abhängig von Property-Werten
In der Praxis bedeutet das:
matchIfMissing = true folgt dem Spring Boot Prinzip „Convention over Configuration“ – der Starter funktioniert out-of-the-box. Aber mit greeting.enabled=false kann man ihn jederzeit ausschalten.
AutoConfiguration.imports erstellen
Erstelle die Datei:src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
Inhalt:
com.javafleet.greeting.GreetingAutoConfiguration
Das war’s! Diese eine Zeile registriert unseren Starter bei Spring Boot.
Schritt 7: Unit Tests schreiben – PFLICHT, keine Option!
Jetzt kommt der Teil, den viele Tutorials vergessen – und das ist ein Fehler!
Ein Starter ohne Tests ist wie ein Haus ohne Fundament. Klar, es steht vielleicht. Aber beim ersten Sturm?
GreetingPropertiesTest.java
java
package com.javafleet.greeting;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit Tests für {@link GreetingProperties}.
*/
@DisplayName("GreetingProperties")
class GreetingPropertiesTest {
@Nested
@DisplayName("Default Values")
class DefaultValues {
@Test
@DisplayName("message sollte 'Hello' sein")
void messageShouldDefaultToHello() {
GreetingProperties props = new GreetingProperties();
assertThat(props.getMessage()).isEqualTo("Hello");
}
@Test
@DisplayName("uppercase sollte false sein")
void uppercaseShouldDefaultToFalse() {
GreetingProperties props = new GreetingProperties();
assertThat(props.isUppercase()).isFalse();
}
@Test
@DisplayName("prefix sollte leer sein")
void prefixShouldDefaultToEmpty() {
GreetingProperties props = new GreetingProperties();
assertThat(props.getPrefix()).isEmpty();
}
@Test
@DisplayName("enabled sollte true sein")
void enabledShouldDefaultToTrue() {
GreetingProperties props = new GreetingProperties();
assertThat(props.isEnabled()).isTrue();
}
@Test
@DisplayName("format sollte nicht null sein")
void formatShouldNotBeNull() {
GreetingProperties props = new GreetingProperties();
assertThat(props.getFormat()).isNotNull();
}
}
@Nested
@DisplayName("Format Default Values")
class FormatDefaultValues {
@Test
@DisplayName("separator sollte ', ' sein")
void separatorShouldDefaultToCommaSpace() {
GreetingProperties.Format format = new GreetingProperties.Format();
assertThat(format.getSeparator()).isEqualTo(", ");
}
@Test
@DisplayName("suffix sollte '!' sein")
void suffixShouldDefaultToExclamation() {
GreetingProperties.Format format = new GreetingProperties.Format();
assertThat(format.getSuffix()).isEqualTo("!");
}
@Test
@DisplayName("emoji sollte leer sein")
void emojiShouldDefaultToEmpty() {
GreetingProperties.Format format = new GreetingProperties.Format();
assertThat(format.getEmoji()).isEmpty();
}
}
@Nested
@DisplayName("Setter & Getter")
class SettersAndGetters {
@Test
@DisplayName("setMessage sollte Wert speichern")
void setMessageShouldStoreValue() {
GreetingProperties props = new GreetingProperties();
props.setMessage("Guten Tag");
assertThat(props.getMessage()).isEqualTo("Guten Tag");
}
@Test
@DisplayName("Format-Setter sollte Werte speichern")
void formatSettersShouldStoreValues() {
GreetingProperties.Format format = new GreetingProperties.Format();
format.setEmoji("👋");
format.setSeparator(" -> ");
assertThat(format.getEmoji()).isEqualTo("👋");
assertThat(format.getSeparator()).isEqualTo(" -> ");
}
}
}
Was macht dieses code?
Die Tests prüfen alle Default-Werte und Getter/Setter. Mit @Nested gruppieren wir logisch zusammengehörige Tests.
In der Praxis bedeutet das:
Wenn du später einen Default-Wert änderst, schlägt der Test fehl. Das ist gewollt! So vergisst du nicht, die Dokumentation und andere abhängige Stellen anzupassen.
DefaultGreetingServiceTest.java
java
package com.javafleet.greeting;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit Tests für {@link DefaultGreetingService}.
*/
@DisplayName("DefaultGreetingService")
class DefaultGreetingServiceTest {
private GreetingProperties properties;
private DefaultGreetingService service;
@BeforeEach
void setUp() {
properties = new GreetingProperties();
service = new DefaultGreetingService(properties);
}
@Nested
@DisplayName("Constructor")
class ConstructorTests {
@Test
@DisplayName("sollte Exception werfen bei null Properties")
void shouldThrowExceptionForNullProperties() {
assertThatThrownBy(() -> new DefaultGreetingService(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("must not be null");
}
}
@Nested
@DisplayName("greet() mit Default-Properties")
class GreetWithDefaults {
@Test
@DisplayName("sollte Standard-Begrüßung zurückgeben")
void shouldReturnDefaultGreeting() {
String result = service.greet("Franz");
assertThat(result).isEqualTo("Hello, Franz!");
}
@Test
@DisplayName("sollte Whitespace im Namen trimmen")
void shouldTrimWhitespaceInName() {
assertThat(service.greet(" Franz ")).isEqualTo("Hello, Franz!");
}
}
@Nested
@DisplayName("greet() mit custom Properties")
class GreetWithCustomProperties {
@Test
@DisplayName("sollte custom Message verwenden")
void shouldUseCustomMessage() {
properties.setMessage("Guten Tag");
assertThat(service.greet("Franz")).isEqualTo("Guten Tag, Franz!");
}
@Test
@DisplayName("sollte Prefix voranstellen")
void shouldPrependPrefix() {
properties.setPrefix("[VIP]");
assertThat(service.greet("Franz")).isEqualTo("[VIP] Hello, Franz!");
}
@Test
@DisplayName("sollte Uppercase anwenden")
void shouldApplyUppercase() {
properties.setUppercase(true);
assertThat(service.greet("Franz")).isEqualTo("HELLO, FRANZ!");
}
@Test
@DisplayName("sollte Emoji hinzufügen")
void shouldAddEmoji() {
properties.getFormat().setEmoji("👋");
assertThat(service.greet("Franz")).isEqualTo("👋 Hello, Franz! 👋");
}
@Test
@DisplayName("sollte alle Optionen kombinieren")
void shouldCombineAllOptions() {
properties.setMessage("Willkommen");
properties.setPrefix("[PREMIUM]");
properties.setUppercase(true);
properties.getFormat().setEmoji("🎉");
String result = service.greet("Franz");
assertThat(result).isEqualTo("🎉 [PREMIUM] WILLKOMMEN, FRANZ! 🎉");
}
}
@Nested
@DisplayName("greet() Edge Cases")
class GreetEdgeCases {
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
@DisplayName("sollte Exception werfen für ungültige Namen")
void shouldThrowExceptionForInvalidNames(String invalidName) {
assertThatThrownBy(() -> service.greet(invalidName))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Name must not be null or empty");
}
@Test
@DisplayName("sollte Namen mit Sonderzeichen verarbeiten")
void shouldHandleSpecialCharactersInName() {
assertThat(service.greet("O'Brien")).isEqualTo("Hello, O'Brien!");
assertThat(service.greet("名前")).isEqualTo("Hello, 名前!");
}
}
}
Was macht dieses code?
Wir testen alle Kombinationen von Properties und Edge Cases. @ParameterizedTest testet mehrere ungültige Inputs mit einem Test.
In der Praxis bedeutet das:
Diese Tests garantieren, dass dein Service auch mit unerwarteten Inputs korrekt reagiert. Das ist besonders wichtig für Libraries!
🟡 PROFESSIONALS
Schritt 8: ApplicationContextRunner – Der professionelle Weg
Jetzt kommt das Herzstück des professionellen Starter-Testings!
Der ApplicationContextRunner ist ein spezielles Test-Utility von Spring Boot um Auto-Configurations zu testen – ohne einen echten Application Context zu starten!
Warum ist das wichtig?
- 🚀 Schnell: Kein Spring Context Startup (Millisekunden statt Sekunden)
- 🔒 Isoliert: Jeder Test hat seinen eigenen „Mini-Context“
- 🎛️ Flexibel: Properties können pro Test gesetzt werden
- ✅ Best Practice: So testen die Spring-Boot-Entwickler selbst!
GreetingAutoConfigurationTest.java
java
package com.javafleet.greeting;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration Tests für {@link GreetingAutoConfiguration}.
*
* <p>Verwendet {@link ApplicationContextRunner} – den empfohlenen Weg
* um Spring Boot Auto-Configuration zu testen!</p>
*/
@DisplayName("GreetingAutoConfiguration")
class GreetingAutoConfigurationTest {
/**
* ApplicationContextRunner mit unserer Auto-Configuration.
* Wird als Basis für alle Tests verwendet.
*/
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GreetingAutoConfiguration.class));
@Nested
@DisplayName("Bean-Erstellung")
class BeanCreation {
@Test
@DisplayName("sollte GreetingService Bean erstellen")
void shouldCreateGreetingServiceBean() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(GreetingService.class);
assertThat(context).hasSingleBean(DefaultGreetingService.class);
});
}
@Test
@DisplayName("sollte GreetingProperties Bean erstellen")
void shouldCreateGreetingPropertiesBean() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(GreetingProperties.class);
});
}
@Test
@DisplayName("GreetingService sollte funktionieren")
void greetingServiceShouldWork() {
contextRunner.run(context -> {
GreetingService service = context.getBean(GreetingService.class);
assertThat(service.greet("Test")).isEqualTo("Hello, Test!");
});
}
}
@Nested
@DisplayName("Property Binding")
class PropertyBinding {
@Test
@DisplayName("sollte greeting.message binden")
void shouldBindMessage() {
contextRunner
.withPropertyValues("greeting.message=Guten Tag")
.run(context -> {
GreetingService service = context.getBean(GreetingService.class);
assertThat(service.greet("Franz")).isEqualTo("Guten Tag, Franz!");
});
}
@Test
@DisplayName("sollte greeting.prefix binden")
void shouldBindPrefix() {
contextRunner
.withPropertyValues("greeting.prefix=[VIP]")
.run(context -> {
GreetingService service = context.getBean(GreetingService.class);
assertThat(service.greet("Franz")).isEqualTo("[VIP] Hello, Franz!");
});
}
@Test
@DisplayName("sollte greeting.uppercase binden")
void shouldBindUppercase() {
contextRunner
.withPropertyValues("greeting.uppercase=true")
.run(context -> {
GreetingService service = context.getBean(GreetingService.class);
assertThat(service.greet("Franz")).isEqualTo("HELLO, FRANZ!");
});
}
@Test
@DisplayName("sollte nested greeting.format.* Properties binden")
void shouldBindNestedFormatProperties() {
contextRunner
.withPropertyValues(
"greeting.format.emoji=👋",
"greeting.format.separator= -> ",
"greeting.format.suffix=???"
)
.run(context -> {
GreetingService service = context.getBean(GreetingService.class);
assertThat(service.greet("Franz")).isEqualTo("👋 Hello -> Franz??? 👋");
});
}
}
@Nested
@DisplayName("@ConditionalOnProperty (greeting.enabled)")
class ConditionalOnProperty {
@Test
@DisplayName("sollte Bean erstellen wenn enabled nicht gesetzt ist")
void shouldCreateBeanWhenEnabledNotSet() {
contextRunner.run(context -> {
assertThat(context).hasSingleBean(GreetingService.class);
});
}
@Test
@DisplayName("sollte Bean erstellen wenn enabled=true")
void shouldCreateBeanWhenEnabledTrue() {
contextRunner
.withPropertyValues("greeting.enabled=true")
.run(context -> {
assertThat(context).hasSingleBean(GreetingService.class);
});
}
@Test
@DisplayName("sollte KEINEN Bean erstellen wenn enabled=false")
void shouldNotCreateBeanWhenEnabledFalse() {
contextRunner
.withPropertyValues("greeting.enabled=false")
.run(context -> {
assertThat(context).doesNotHaveBean(GreetingService.class);
assertThat(context).doesNotHaveBean(DefaultGreetingService.class);
});
}
}
@Nested
@DisplayName("@ConditionalOnMissingBean (Überschreibbarkeit)")
class ConditionalOnMissingBean {
@Test
@DisplayName("sollte Custom-Implementierung bevorzugen")
void shouldPreferCustomImplementation() {
contextRunner
.withUserConfiguration(CustomGreetingServiceConfig.class)
.run(context -> {
// Es gibt einen GreetingService...
assertThat(context).hasSingleBean(GreetingService.class);
// ...aber NICHT unseren Default!
assertThat(context).doesNotHaveBean(DefaultGreetingService.class);
// Die Custom-Implementierung wird genutzt
GreetingService service = context.getBean(GreetingService.class);
assertThat(service.greet("Test")).isEqualTo("CUSTOM: Test");
});
}
/**
* Test-Configuration mit Custom GreetingService.
*/
@Configuration(proxyBeanMethods = false)
static class CustomGreetingServiceConfig {
@Bean
GreetingService greetingService() {
return name -> "CUSTOM: " + name;
}
}
}
@Nested
@DisplayName("Context Lifecycle")
class ContextLifecycle {
@Test
@DisplayName("sollte Context sauber starten")
void shouldStartContextCleanly() {
contextRunner.run(context -> {
assertThat(context).hasNotFailed();
});
}
}
}
Was macht dieses code?
Der ApplicationContextRunner erstellt für jeden Test einen isolierten „Mini-Context“ mit unserer Auto-Configuration. Mit withPropertyValues() setzen wir Properties, mit withUserConfiguration() fügen wir Test-Beans hinzu.
In der Praxis bedeutet das:
Diese Tests sind extrem schnell (keine Spring-Startup-Zeit) und testen die Auto-Configuration exakt so, wie sie in einer echten Anwendung geladen würde. Das ist der Gold-Standard für Starter-Tests!
Schritt 9: Demo-Projekt erstellen
Jetzt erstellen wir ein Demo-Projekt um unseren Starter in Aktion zu sehen.
Demo POM (greeting-spring-boot-starter-demo/pom.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.javafleet</groupId>
<artifactId>greeting-spring-boot-starter-project</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>greeting-spring-boot-starter-demo</artifactId>
<packaging>jar</packaging>
<name>Greeting Starter Demo</name>
<dependencies>
<!-- Unser Custom Starter! -->
<dependency>
<groupId>com.javafleet</groupId>
<artifactId>greeting-spring-boot-starter</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
DemoApplication.java
java
package com.javafleet.demo;
import com.javafleet.greeting.GreetingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class DemoApplication {
private static final Logger log = LoggerFactory.getLogger(DemoApplication.class);
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
/**
* CommandLineRunner für Konsolen-Demo beim Start.
* Der GreetingService wird automatisch injiziert!
*/
@Bean
CommandLineRunner demo(GreetingService greetingService) {
return args -> {
log.info("========================================");
log.info(" Greeting Starter Demo");
log.info("========================================");
log.info("Test 1: {}", greetingService.greet("Franz"));
log.info("Test 2: {}", greetingService.greet("Nova"));
log.info("Test 3: {}", greetingService.greet("Elyndra"));
log.info("========================================");
log.info(" REST: http://localhost:8080/greet/{name}");
log.info("========================================");
};
}
}
GreetingController.java
java
package com.javafleet.demo;
import com.javafleet.greeting.GreetingService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/greet")
public class GreetingController {
private final GreetingService greetingService;
public GreetingController(GreetingService greetingService) {
this.greetingService = greetingService;
}
@GetMapping("/{name}")
public ResponseEntity<String> greet(@PathVariable String name) {
return ResponseEntity.ok(greetingService.greet(name));
}
@GetMapping
public ResponseEntity<String> greetWorld() {
return greet("World");
}
}
application.properties
properties
# Server server.port=8080 # Greeting Starter Configuration greeting.message=Hello greeting.uppercase=false greeting.enabled=true # Format-Optionen greeting.format.separator=, greeting.format.suffix=! # Debug-Modus (aktivieren für Auto-Configuration Report) # debug=true
Schritt 10: Demo-Tests mit MockMvc
GreetingControllerTest.java
java
package com.javafleet.demo;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("GreetingController Web Tests")
class GreetingControllerTest {
@Autowired
private MockMvc mockMvc;
@Nested
@DisplayName("GET /greet/{name}")
class GreetWithName {
@Test
@DisplayName("sollte 200 OK zurückgeben")
void shouldReturn200OK() throws Exception {
mockMvc.perform(get("/greet/Franz"))
.andExpect(status().isOk());
}
@Test
@DisplayName("sollte korrekte Begrüßung zurückgeben")
void shouldReturnCorrectGreeting() throws Exception {
mockMvc.perform(get("/greet/Franz"))
.andExpect(status().isOk())
.andExpect(content().string("Hello, Franz!"));
}
}
@Nested
@DisplayName("GET /greet")
class GreetWorld {
@Test
@DisplayName("sollte 'World' als Default begrüßen")
void shouldGreetWorldByDefault() throws Exception {
mockMvc.perform(get("/greet"))
.andExpect(content().string("Hello, World!"));
}
}
}
Projekt bauen und starten
bash
# Im Root-Verzeichnis mvn clean install # Demo starten cd greeting-spring-boot-starter-demo mvn spring-boot:run
Testen:
bash
curl http://localhost:8080/greet/Franz # Output: Hello, Franz! curl http://localhost:8080/greet # Output: Hello, World!
🔵 BONUS: Advanced Features
Bonus 1: Eigene Implementierung überschreiben
Dank @ConditionalOnMissingBean kannst du den Default überschreiben:
java
@Configuration
public class MyConfig {
@Bean
public GreetingService greetingService() {
return name -> "🎉 Custom: Hey " + name + "! 🎉";
}
}
Der DefaultGreetingService wird dann nicht erstellt!
Bonus 2: Debug-Modus
Setze debug=true in application.properties:
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
GreetingAutoConfiguration matched:
- @ConditionalOnClass found required class 'com.javafleet.greeting.GreetingService'
GreetingAutoConfiguration#greetingService matched:
- @ConditionalOnMissingBean did not find any beans
- @ConditionalOnProperty (greeting.enabled=true) matched
✅ Checkpoint: Hast du Tag 1 geschafft?
Grundlagen (🟢):
- Du verstehst die drei Säulen (Dependencies, Auto-Configuration, Properties)
- Du kannst
@ConfigurationPropertiesnutzen - Du kennst
@ConditionalOnProperty,@ConditionalOnClass,@ConditionalOnMissingBean - Du hast
AutoConfiguration.importserstellt - Du hast Unit Tests für Properties und Service geschrieben
Professionals (🟡):
- Du verstehst
ApplicationContextRunnerund hast Auto-Configuration Tests - Du kannst den Starter mit
enabled=falseausschalten - Du kannst Debug-Modus nutzen
- Du hast REST-Controller mit MockMvc getestet
Bonus (🔵):
- Du kannst den Default-Service überschreiben
- Du verstehst Nested Properties
Spring Boot Aufbau - Tag 1
Auto-Configuration & Custom Starter
🔥 Elyndras Real Talk:
Nach der Session kam das Team zusammen…
Nova: „Elyndra, das war… WOW! Ich dachte immer Spring Boot macht einfach Magie. Aber jetzt verstehe ich es!“
Elyndra: „Genau, Nova! Und das Schöne ist: Du kannst jetzt eigene ‚Magie‘ bauen.“
Code Sentinel: „Eine Sache noch: Ich hab gesehen, dass in vielen Tutorials die Tests fehlen. Das geht gar nicht.“
Elyndra: nickt „Absolut. Ein Starter ohne Tests ist wie ein Auto ohne Bremsen. Klar, es fährt. Aber…“
Nova: „…beim ersten Problem steht man dumm da!“
Franz-Martin: „Der ApplicationContextRunner ist übrigens genial. So testen wir seit Jahren unsere internen Starter. Schnell, isoliert, zuverlässig.“
Code Sentinel: „Und man testet die Auto-Configuration exakt so, wie sie in Production läuft. Keine Überraschungen.“
Nova: „Ich hab eine Frage: Warum testen so viele Tutorials das nicht?“
Elyndra: seufzt „Gute Frage. Vielleicht weil Tests ‚langweilig‘ sind? Oder weil man denkt, ein einfacher Starter braucht das nicht?“
Franz-Martin: „Das ist ein Trugschluss. Je einfacher etwas aussieht, desto wichtiger sind Tests. Weil man sich darauf verlässt, dass es ‚einfach funktioniert‘.“
Code Sentinel: „Und wenn es dann nicht funktioniert – in Production, natürlich – dann ist das Geschrei groß.“
Nova: „Okay, Message verstanden: Tests sind Pflicht, keine Option!“
Elyndra: lächelt „Genau. Und jetzt: Wer hat Lust auf Kaffee?“
❓ FAQ (Häufige Fragen)
Q: Warum ApplicationContextRunner statt @SpringBootTest?
A: ApplicationContextRunner startet keinen echten Spring Context. Das macht Tests:
- Schneller (Millisekunden statt Sekunden)
- Isolierter (jeder Test hat eigenen Context)
- Flexibler (Properties pro Test ändern)
Q: Muss ich wirklich alle diese Tests schreiben?
A: Ja. Ein Starter wird von anderen genutzt. Wenn er nicht funktioniert, blockierst du andere Entwickler. Tests sind deine Qualitätsgarantie.
Q: Kann ich auch Mockito für Auto-Configuration Tests nutzen?
A: Kannst du, aber ApplicationContextRunner ist besser. Er testet das echte Spring-Verhalten, nicht gemockte Annahmen.
Q: Was ist der Unterschied zwischen @Configuration und @AutoConfiguration?
A: @AutoConfiguration ist neu in Spring Boot 3 und speziell für Auto-Configuration gedacht. Es hat bessere Ordering-Unterstützung.
Q: Was macht ihr eigentlich wenn im Team mal Spannungen entstehen? 🤔
A: Manche Geschichten passen nicht in Tech-Blogs. Die gehören zu private logs. Aber das ist ein anderes Kapitel. 📖
📅 Nächster Kurstag: Tag 2
„Tag 2: Spring Data JPA Basics – Von ArrayList zur Datenbank“
Was du lernen wirst:
- JPA Setup mit MariaDB
- Erste Entity mit
@Entity,@Id,@GeneratedValue - Repository Pattern mit
JpaRepository - Natürlich mit Tests!
📥 Download & Ressourcen
Projekt zum Download:
👉 greeting-spring-boot-starter
Was ist im Repository enthalten:
- ✅ Kompletter Starter Code (Spring Boot 3)
- ✅ Vollständige Unit Tests mit ApplicationContextRunner
- ✅ Demo-Projekt mit REST-Controller
- ✅ MockMvc Tests für Controller
- ✅ Multi-Modul Maven Setup
- ✅ Ausführliche README
Das war Tag 1 vom Spring Boot Aufbau-Kurs!
Du kannst jetzt:
- ✅ Spring Boot Auto-Configuration verstehen
- ✅ Eigene Starter mit Tests entwickeln
- ✅
ApplicationContextRunnerfür Auto-Configuration Tests nutzen - ✅ Die Spring Boot „Magie“ erklären
Morgen: Spring Data JPA – von ArrayList zur echten Datenbank! 🚀
Keep coding, keep testing! 💙
Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting
Tags: #SpringBoot #CustomStarter #AutoConfiguration #Testing #ApplicationContextRunner #Java #Tutorial

