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


Custom Starter

🗺️ Deine Position im Kurs

TagThemaStatus
→ 1Auto-Configuration & Custom Starter👉 DU BIST HIER!
2Spring Data JPA Basics📜 Kommt als nächstes
3JPA Relationships & Queries🔒 Noch nicht freigeschaltet
4Spring Security – Part 1🔒 Noch nicht freigeschaltet
5Spring Security – Part 2🔒 Noch nicht freigeschaltet
6Caching & Serialisierung🔒 Noch nicht freigeschaltet
7Messaging & Email🔒 Noch nicht freigeschaltet
8Testing & Dokumentation🔒 Noch nicht freigeschaltet
9Spring Boot Actuator🔒 Noch nicht freigeschaltet
10Template 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:

  • @ConfigurationProperties für typsichere Konfiguration
  • @AutoConfiguration mit Conditional-Annotations
  • spring.factories fü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
  • @ConfigurationProperties für typsichere Konfiguration nutzen
  • @AutoConfiguration und Conditionals verstehen
  • Den Starter via AutoConfiguration.imports registrieren
  • 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-processor für IDE Auto-Complete
  • spring-boot-test fü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 @ConfigurationProperties nutzen
  • Du kennst @ConditionalOnProperty, @ConditionalOnClass, @ConditionalOnMissingBean
  • Du hast AutoConfiguration.imports erstellt
  • Du hast Unit Tests für Properties und Service geschrieben

Professionals (🟡):

  • Du verstehst ApplicationContextRunner und hast Auto-Configuration Tests
  • Du kannst den Starter mit enabled=false ausschalten
  • 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

Frage 1 von 10

Was sind die drei Säulen eines Spring Boot Starters?

Frage 2 von 10

Welche Annotation aktiviert typsichere Konfiguration aus application.properties?

Frage 3 von 10

Was bewirkt @ConditionalOnMissingBean in einer Auto-Configuration?

Frage 4 von 10

Wo registriert man Auto-Configuration-Klassen in Spring Boot 3?

Frage 5 von 10

Warum sollte die Service-Implementierung Prefix + "Service" heißen?

Frage 6 von 10

Was macht @EnableConfigurationProperties(GreetingProperties.class)?

Frage 7 von 10

Warum braucht @ConfigurationProperties einen Default-Konstruktor?

Frage 8 von 10

Was passiert beim ersten Aufruf von greet("Anna") mit Caching?

Frage 9 von 10

Wozu dient spring-boot-configuration-processor?

Frage 10 von 10

Was ist der Unterschied zwischen @AutoConfiguration und @Configuration?

🔥 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
  • ApplicationContextRunner fü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

Autor

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.