CodeGym /Kurse /Spring Core /Erster AppConfig und Kontextstart

Erster AppConfig und Kontextstart

Spring Core
Level 3, Lektion 2
Verfügbar

1. AppConfig und composition root

Wenn wir in plain Java eine Klasse hatten, wo wir den Objektgraphen von Hand zusammenbauten — im Grunde ein „LEGO-Bauzimmer" —, dann verschieben wir mit Spring dieses Zimmer in den Container. AppConfig ist der Ort, wo wir dem Container beschreiben, welche Objekte er erstellen können muss und wie sie zusammenhängen. Das ist keine Business-Klasse und kein „weiterer Service". Wenn man diese Grenze nicht von Anfang an hält, verwandelt sich die Konfiguration sehr schnell in eine „zweite Anwendungsschicht".

Wir haben bereits zwei Rollen getrennt: BeanFactory hält die grundlegende Container-Mechanik, ApplicationContext wird zum normalen Einstiegspunkt. Jetzt muss dieses Bild in Code umgesetzt werden: AppConfig erstellen, AnnotationConfigApplicationContext hochfahren und sehen, wo genau der Bootstrap endet und die normale Anwendung beginnt.

In unserem Projekt ist es bequem, die Konfiguration in einem separaten Paket zu halten, damit das Auge nicht verwirrt wird:

com.example.contextflow.config

Das minimale Konfigurationsskelett sieht so aus:

package com.example.contextflow.config;

import org.springframework.context.annotation.Configuration;

@Configuration // Sagt Spring: Das ist eine Konfigurationsklasse, die beim Kontextstart verarbeitet werden soll
public class AppConfig {
    // Vorerst leer: Hier erscheinen @Bean-Methoden mit der Beschreibung, was und wie im Container erstellt wird
}

Diese Annotation bedeutet für uns gerade eine sehr einfache Sache: „Spring, behandle diese Klasse als Beschreibung dessen, was im Container registriert werden soll". Ja, unter der Haube steckt viel Mechanik — und die ist wirklich interessant —, aber heute halten wir den Fokus auf dem praktischen Start: Es gibt eine Konfiguration → Es gibt einen Kontext → Die Anwendung startet.

2. @Bean: „Ein Objekt, das der Container erstellt, nicht du"

Bei der ersten Begegnung mit @Bean denkt man leicht: „Na, das ist einfach eine Factory-Methode". Fast. Der Unterschied ist, dass man eine normale Factory selbst aufruft, während eine @Bean-Methode der Container aufruft, wenn er dieses Objekt erstellen und verwalten muss. Man sagt „wie erstellen", und Spring entscheidet „wann erstellen" und „wie an andere Abhängigkeiten weitergeben".

Beginnen wir mit einem einfachen, fast „Hello, World"-Beispiel: Wir registrieren appName als Bean. Das wirkt ein bisschen seltsam — einen String im Container registrieren —, aber zum Lernen ist das ideal: minimale Domäne, maximale Transparenz.

package com.example.contextflow.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration // Diese Klasse beschreibt den Container-Inhalt
public class AppConfig {

    @Bean // Diese Methode wird vom Container aufgerufen, um ein verwaltetes Objekt (Bean) zu erstellen
    public String appName() {
        // Gibt einen Wert zurück, der im Container als Bean vom Typ String liegt
        return "ContextFlow";
    }
}

Hier gibt es eine wichtige Kleinigkeit, die immer wieder auftaucht: Der Bean-Name wird standardmäßig aus dem Methodennamen genommen. Das heißt, die Bean heißt "appName". Über Bean-Namen sprechen wir später ausführlicher, aber schon jetzt ist es nützlich zu merken: Spring hat für jedes verwaltete Objekt einen Namen, auch wenn man häufiger „nach Typ" denkt.

3. Start von AnnotationConfigApplicationContext

Wenn man Spring Boot startet, erledigt es viel Arbeit für einen. Im reinen Spring Core ist alles ehrlich: Man erstellt selbst den Kontext, gibt selbst die Konfiguration an und holt sich selbst das initiale Objekt. Ja, das klingt anfangs etwas „niederstufiger", dafür sieht man das Bild ohne Spezialeffekte.

package com.example.contextflow;

import com.example.contextflow.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {

    public static void main(String[] args) {
        // Der Kontext ist eine Ressource: Er muss geschlossen werden, deshalb try-with-resources
        try (AnnotationConfigApplicationContext context =
                     new AnnotationConfigApplicationContext(AppConfig.class)) { // Konfiguration übergeben

            // Bean nach Name und Typ abrufen: nützlich für den Start, aber nicht für Business-Code
            String appName = context.getBean("appName", String.class);

            System.out.println("App = " + appName); // App = ContextFlow
        } // Hier wird automatisch close() für den Kontext aufgerufen
    }
}

Beachte try (...) { ... }. Wir verwenden try-with-resources, weil der Kontext geschlossen werden muss. Das wirkt gerade wie eine Kleinigkeit — „na ja, eine Konsolenanwendung endet sowieso" —, aber die Gewohnheit, den Kontext zu schließen, hilft später sehr, besonders in Tests und in langlebigen Anwendungen.

Vorerst kann man sich ein sehr einfaches Schema vorstellen:

flowchart TD
    A["main()"] --> B[AnnotationConfigApplicationContext erstellen]
    B --> C[Kontext liest AppConfig]
    C --> D[Kontext erstellt die benötigten Beans]
    D --> E["getBean(...) für das initiale Objekt"]
    E --> F[Anwendung läuft]
    F --> G["Kontext close()"]

Die Worte „liest" und „erstellt" sind hier bewusst allgemein gehalten. Warum der Container erst eine Beschreibung aufbaut und dann Objekte erstellt — das ist genau das Thema der nächsten Vorlesung über BeanDefinition.

Expliziter Start: register(...) + refresh(...)

Der kurze Konstruktor new AnnotationConfigApplicationContext(AppConfig.class) ist gut für den Alltagsstart. Aber zum Lernen und manchmal für Feineinstellungen ist es nützlich, den zweiten Stil zu sehen: Man erstellt einen leeren Kontext, registriert die Konfiguration und startet ihn explizit über refresh().

So ist es für das Gehirn einfacher: Es entsteht eine klare Grenze zwischen „Ich habe dem Container beschrieben, was es in der Anwendung gibt" und „Der Container ist wirklich gestartet".

package com.example.contextflow;

import com.example.contextflow.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {

    public static void main(String[] args) {
        // Wir erstellen einen "leeren" Kontext: Er ist noch nicht als Container gestartet
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext();

        // Konfigurationsklassen registrieren (Beschreibung dessen, was im Container sein wird)
        context.register(AppConfig.class);

        // Den Container explizit starten: Nach refresh() können Beans abgerufen werden
        context.refresh();

        String appName = context.getBean("appName", String.class);
        System.out.println(appName); // ContextFlow

        // Den Kontext manuell schließen (in dieser Variante ohne try-with-resources)
        context.close();
    }
}

Um den Unterschied zwischen den beiden Varianten zu festigen, hilft eine kleine Tabelle:

Start-Stil Wie es aussieht Wann es bequem ist
Kurz new AnnotationConfigApplicationContext(AppConfig.class) Fast immer, wenn die Anwendung einfach gestartet wird
Explizit new ...(); register(...); refresh(); Beim Lernen, wenn man den „Startmoment erfassen" will, oder wenn vor refresh() etwas konfiguriert werden soll

Und ja: Wenn man register(...) gemacht hat, aber refresh(...) vergessen hat, ist der Kontext „halblebig". Er existiert als Objekt, aber ist nicht als Container gestartet. Das ist wie einen Kaffeevollautomaten kaufen, auf den Tisch stellen, aber nicht einstecken: Formal hat man das Gerät, aber warum Kaffee nicht kommt, ist rätselhaft.

Kontext schließen: Das Argument über die Prozessbeendigung

In einer Konsolenanwendung kann man tatsächlich abwinken: Die JVM endet, und „alles schließt sich von selbst". Aber der Container ist eine verwaltete Umgebung, und er kann Ressourcen korrekt beenden, Shutdown-Hooks aufrufen und Infrastruktur sauber schließen. Heute haben wir noch keine Lifecycle-Callbacks gelernt, aber die Gewohnheit, den Kontext zu schließen, formt man besser sofort, solange der Code klein und verständlich ist.

Die sauberste Variante für unser Format ist genau try-with-resources, weil es gleichzeitig kurz und disziplinierend ist:

try (AnnotationConfigApplicationContext context =
             new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Hier arbeiten wir mit Beans, und beim Verlassen des Blocks wird der Kontext automatisch geschlossen
}

Wenn man jemals try (BufferedReader r = ...) { ... } geschrieben hat, kennt man diese Idee bereits. Der Kontext ist dieselbe Art Ressource, nur auf Steroiden.

4. Beans über getBean(...) abrufen

Mit getBean() erleben Einsteiger oft eine Romanze „Liebe auf den ersten Aufruf": Es ist doch bequem, man kann immer alles herausholen. Aber diese Romanze hat ein schlechtes Ende, wenn man sie in Business-Klassen trägt: Man bekommt Service-Locator-Stil, versteckte Abhängigkeiten und „irgendwie DI, aber eigentlich nicht".

In der aktuellen Phase ist getBean(...) als Bootstrap-Werkzeug angebracht: Kontext hochgefahren, initiales Objekt geholt, Kontrolle übergeben.

Wenn man zum Beispiel eine Bean nach Typ, ohne Namen, abrufen möchte, sieht das so aus:

package com.example.contextflow;

import com.example.contextflow.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context =
                     new AnnotationConfigApplicationContext(AppConfig.class)) {

            // Abruf nach Typ ist bequem, solange es genau eine Bean dieses Typs im Container gibt
            String appName = context.getBean(String.class);

            System.out.println(appName); // ContextFlow
        }
    }
}

Gleich ein kleiner Hinweis für die Zukunft, ohne tief einzutauchen: Abruf nach Typ ist bequem, solange es eine Bean dieses Typs im Container gibt. Sobald zwei String-Beans erscheinen, wird ein solcher Aufruf mehrdeutig. Das ist eine normale Situation, und wir werden lernen, damit umzugehen — aber nicht heute.

5. Parameter von @Bean-Methoden und Abhängigkeiten

Hier beginnt Spring besonders nützlich zu wirken. Wir wollen, dass AppConfig den Objektgraphen nicht durch manuelles Aufrufen einer @Bean-Methode aus einer anderen zusammenbaut, und nicht über context.getBean(...) innerhalb der Konfiguration — das wäre seltsam —, sondern über ein normales DI-Modell: „Hier sind die Abhängigkeiten, bau die obere Ebene zusammen".

Dafür kann eine @Bean-Methode Parameter haben. Spring sieht diese Parameter und versucht, passende Beans im Container zu finden, um sie der Methode zu übergeben. Das ähnelt dem früheren Übergeben von Abhängigkeiten in den Konstruktor, nur jetzt auf der Ebene der Container-Konfiguration.

Nehmen wir ein vereinfachtes Stück von ContextFlow. Wir haben Interfaces — die sollten nach den vorherigen Tagen bereits vorhanden sein:

package com.example.contextflow.domain.ports;

public interface OrderStore {
    // Domänen-Port: "Bestellung irgendwo speichern" (Implementierung wird Infrastruktur sein)
    void save(String orderId);
}
package com.example.contextflow.infrastructure.store;

import com.example.contextflow.domain.ports.OrderStore;

public class InMemoryOrderStore implements OrderStore {

    @Override
    public void save(String orderId) {
        // Einfachste Implementierung für das Beispiel: statt DB schreiben wir in die Konsole
        System.out.println("saved order " + orderId); // saved order A-100
    }
}

Und der Service, der diesen Port verwendet:

package com.example.contextflow.application;

import com.example.contextflow.domain.ports.OrderStore;

public class OrderPlacementService {

    private final OrderStore orderStore;

    public OrderPlacementService(OrderStore orderStore) {
        // Explizite Abhängigkeit über den Konstruktor: Der Service weiß nichts von Spring
        this.orderStore = orderStore;
    }

    public void place(String orderId) {
        // Business-Operation: Die Bestellungsanlage delegiert das Speichern an den Port
        orderStore.save(orderId);
    }
}

Jetzt das Interessanteste — die Konfiguration. Wir registrieren OrderStore, und dann registrieren wir den Service, indem wir OrderStore als Parameter übergeben.

package com.example.contextflow.config;

import com.example.contextflow.application.OrderPlacementService;
import com.example.contextflow.domain.ports.OrderStore;
import com.example.contextflow.infrastructure.store.InMemoryOrderStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration // Wir beschreiben den Container: Welche Objekte gibt es in der Anwendung
public class AppConfig {

    @Bean // Port-Implementierung als Bean registrieren
    public OrderStore orderStore() {
        return new InMemoryOrderStore();
    }

    @Bean // Spring setzt den passenden OrderStore selbst als Parameter ein
    public OrderPlacementService orderPlacementService(OrderStore orderStore) {
        return new OrderPlacementService(orderStore);
    }
}

Und der Start:

package com.example.contextflow;

import com.example.contextflow.application.OrderPlacementService;
import com.example.contextflow.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {

    public static void main(String[] args) {
        try (AnnotationConfigApplicationContext context =
                     new AnnotationConfigApplicationContext(AppConfig.class)) {

            // Den "Start-Service" aus dem Container holen und ihm die Kontrolle übergeben
            OrderPlacementService service = context.getBean(OrderPlacementService.class);

            service.place("A-100"); // saved order A-100
        }
    }
}

Hier entsteht eine sehr wichtige Gewohnheit: Die Konfiguration beschreibt das Wiring, und der Business-Code bleibt gewöhnliches Java. Der Service weiß nichts von Spring, es ist ihm egal, wer OrderStore übergeben hat — man selbst oder der Container. Und das ist eine ausgezeichnete Nachricht für Testbarkeit und Klarheit der Architektur.

6. Typische Fehler beim Kontextstart

Beim ersten Spring-Start scheitert es meistens nicht, weil „Spring kompliziert ist", sondern weil Rollen vertauscht und ein kleiner Schritt vergessen wurde. Das ist normal: Der Container ist streng, man kann ihm nichts „ungefähr erklären". Entweder kann er den Graphen zusammenbauen, oder er fällt ehrlich hin. Und das ist übrigens ein guter Stil: Besser beim Start fallen als still falsch laufen.

Fehler Nr. 1: @Configuration auf AppConfig vergessen.
Die Klasse wird zur normalen Java-Klasse, und die @Bean-Methoden werden vom Container nicht als Definitionen wahrgenommen. Von außen sieht das so aus: „Ich habe alles geschrieben, aber Spring sieht nichts". Wenn der Kontext zufällig startet, aber getBean("appName") fällt, ist das Erste, was man überprüft, die Annotation auf der Konfigurationsklasse.

Fehler Nr. 2: @Bean auf der Methode vergessen.
Innerhalb von AppConfig kann eine schöne Factory-Methode stehen, aber ohne @Bean wird der Container sie nicht als Bean-Definition betrachten. Das ähnelt der Situation „Die Funktion ist da, aber du rufst sie nirgends auf". Nur ist der Aufrufer hier Spring, und er weiß nicht, dass man ihn aufrufen muss.

Fehler Nr. 3: Stil register(...) gewählt, aber refresh(...) vergessen.
Das ist ein Klassiker. Der Kontext ist erstellt, die Konfiguration registriert, aber der Container ist noch nicht gestartet. Am Ende funktioniert getBean() nicht oder verhält sich unerwartet. Wer den expliziten Stil verwendet, sollte die Regel im Kopf behalten: register beschreibt, refresh startet.

Fehler Nr. 4: Den Kontext nicht geschlossen.
In einem kleinen main() macht sich das nicht immer bemerkbar, aber man festigt eine schlechte Gewohnheit. Später, wenn Ressourcen, Objekte zum Schreiben in Dateien, Caches oder Lifecycle-Logik erscheinen, beginnt das „nicht geschlossen" mit Unvorhersehbarkeit nachzuwirken. try-with-resources löst das mühelos.

Fehler Nr. 5: Anfangen, getBean(...) in Services zu verwenden.
Anfangs klingt das bequem: „Wenn ich OrderStore brauche, hole ich ihn selbst". Aber das bringt uns zu versteckten Abhängigkeiten und enger Kopplung zurück — nur jetzt versteckt sich die Abhängigkeit nicht hinter new, sondern hinter ApplicationContext. In diesem Kurs lebt getBean() in main — oder an sehr engen infrastrukturellen Stellen —, nicht in der Business-Schicht.

1
Aufgabe
Spring Core, Level 3, Lektion 2
Gesperrt
Erster `AppConfig` mit String-Bean
Erster `AppConfig` mit String-Bean
1
Aufgabe
Spring Core, Level 3, Lektion 2
Gesperrt
Expliziter Start über `register(...)` und `refresh()`
Expliziter Start über `register(...)` und `refresh()`
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION