CodeGym /Kursy /Spring Core /Inicjalizacja beana

Inicjalizacja beana

Spring Core
Poziom 10, Lekcja 1
Dostępny

1. Rola fazy init i konstruktora

Oddzieliliśmy już sam fakt tworzenia obiektu od momentu, gdy bean jest naprawdę gotowy do pracy. Teraz trzeba zrozumieć, czym kontener wypełnia tę przerwę.

Jeśli dopiero zaprzyjaźniłeś się z DI, bardzo chce się powiedzieć: „No przecież dostaję zależności w konstruktorze. Znaczy konstruktor to idealne miejsce, żeby od razu wszystko przygotować". Logika zrozumiała, ale w świecie Springa często prowadzi do kodu, który jest trudny do czytania, testowania i utrzymania. Faza init to sposób na to, żeby przygotowanie obiektu było jawne i zarządzane przez kontener, a nie ukryte w konstruktorze.

Konstruktor w kodzie przyjaznym DI dobrze pełni rolę „kontraktu": mówi, bez czego obiekt nie może żyć. Powinien być szybki i przewidywalny jak dobre poniedziałkowe rano dewelopera: otworzyłeś laptopa — i nic nie płonie. Natomiast przygotowanie wewnętrznego stanu to często osobny krok. Na przykład obiekt może chcieć zbudować wewnętrzny indeks, rozgrzać cache, sprawdzić coś w swoich zależnościach lub przygotować folder roboczy dla raportów. To bliżej „przed startem sprawdź, że kabel zasilający jest podłączony" niż do prostego tworzenia obiektu.

Warto trzymać w głowie prostą granicę: konstruktor to kwestia pobierania obowiązkowych zależności, faza init — doprowadzania obiektu do stanu roboczego. To nie jest „oficjalne prawo fizyki Springa", lecz inżynierski nawyk, który mocno zmniejsza liczbę „dziwnych" bugów przy starcie.

Mała tabela orientacyjna:

Gdzie żyje logika Co tam wygląda dobrze Co tam wygląda podejrzanie
Konstruktor zapisanie zależności, lekka weryfikacja argumentów ciężkie przygotowania, IO, „przypadkowe" uruchomienie scenariusza biznesowego
Faza init przygotowanie cache/indeksu, walidacja, tworzenie katalogu wysyłanie powiadomień „do wszystkich klientów, że serwis wstał", generowanie raportów „na wszelki wypadek"

Żeby nie brzmiało to jak czysta filozofia, zróbmy proste spostrzeżenie: konstruktor jest wywoływany zawsze, gdy robisz new, a faza init wywoływana jest tylko gdy obiekt jest stworzony przez kontener jako bean. I właśnie tu Spring ma supermoce: może zagwarantować, że logika init zostanie wywołana dokładnie w odpowiednim momencie, a nie „gdzieś zapomniano — i się nie stało".

2. @PostConstruct

@PostConstruct to niemal idealny start dla początkującego. Po pierwsze, czyta się jak ludzkie zdanie: „po stworzeniu — zrób to". Po drugie, to nie czysto Springowa adnotacja, lecz część Jakarta annotations (historycznie JSR-250), czyli wygląda bardziej neutralnie. Po trzecie, w klasie pojawia się oczywiste miejsce, gdzie leży jej przygotowanie, a nie „magia w konfiguracji pod stringową nazwą".

Dla @PostConstruct zazwyczaj wystarczy pamiętać trzy rzeczy: metoda powinna być bez parametrów, powinna być void i powinna opisywać przygotowanie obiektu, a nie uruchamianie głównego scenariusza aplikacji. Jeśli metoda rzuci wyjątek, Spring, co do zasady, przerwie start kontekstu, co dla weryfikacji i zachowania fail-fast jest zazwyczaj bardzo korzystne: lepiej paść od razu niż „popracować chwilę i umrzeć w piątek wieczorem".

Pokażemy to na uproszczonym NotificationTemplateCatalog — katalogu szablonów powiadomień dla ContextFlow. Na razie nie komplikujemy: szablony będą po prostu w pamięci (bez zasobów i plików).

import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;

@Component
class NotificationTemplateCatalog {

    // Wątkowo-bezpieczne repozytorium szablonów, dostępne po fazie init.
    private final ConcurrentHashMap<String, String> templates = new ConcurrentHashMap<>();

    @PostConstruct
    void init() {
        // Faza init: raz wypełniamy katalog przy starcie kontekstu.
        templates.put("order-created", "Order %s created");
        templates.put("order-cancelled", "Order %s cancelled");
    }

    String template(String key) {
        // Po starcie kontekstu oczekujemy, że potrzebne klucze są już załadowane.
        return templates.get(key);
    }
}

Sens jest taki: konstruktor tu w ogóle nie jest potrzebny (na razie nie mamy zewnętrznych zależności), ale faza init — owszem. Raz wypełniamy katalog i dalej działa szybko i przewidywalnie.

Teraz zobaczmy, jak tego używa serwis formujący treść powiadomienia. Ważne: serwis nie musi wiedzieć, kiedy jest wypełniany katalog. Dostaje już „gotowy" obiekt.

import org.springframework.stereotype.Service;

@Service
class NotificationDispatchService {
    private final NotificationTemplateCatalog catalog;

    NotificationDispatchService(NotificationTemplateCatalog catalog) {
        // Zależność dostajemy przez DI (constructor injection).
        this.catalog = catalog;
    }

    String buildCreatedText(String orderId) {
        // Zakładamy, że init wypełnił klucz "order-created".
        return catalog.template("order-created").formatted(orderId);
    }
}

Jeśli catalog.template("order-created") zwróci null, boleśnie poczujesz to w runtime. Dlatego często w @PostConstruct robi się małą walidację, żeby paść przy starcie „zrozumiale i uczciwie".

import jakarta.annotation.PostConstruct;

class CatalogValidation {
    @PostConstruct
    void validate() {
        // Demonstracja fail-fast: jeśli inicjalizacja jest krytyczna — padamy przy starcie kontekstu.
        throw new IllegalStateException("Example: init validation failed");
    }
}

Ten przykład celowo jest „zły", żeby podkreślić mechanikę: faza init to normalne miejsce, żeby stwierdzić „bez tego mój bean nie może działać".

Żeby @PostConstruct działał, adnotacja musi być dostępna na classpath. W naszym kursie to jest zapewnione, ale w prawdziwym życiu czasem łapiesz sytuację „adnotację postawiłem, a metoda się nie wywołała", bo zależności nie dodano.

Mini-przypomnienie w stylu „jedna linijka, która oszczędza godzinę":

dependencies {
    // Potrzebna, żeby adnotacja @PostConstruct była dostępna na classpath.
    implementation("jakarta.annotation:jakarta.annotation-api")
}

Ważny dla nas fakt praktyczny: jeśli bean jest container-managed, to @PostConstruct to oficjalny sposób powiedzenia kontenerowi „mam fazę przygotowania". Za kulisami obsługuje to infrastruktura kontenera, ale dla bieżącego zadania wystarczy sam kontrakt.

3. InitializingBean: inicjalizacja przez interfejs

Interfejs InitializingBean to „oldschoolowy, ale uczciwy" sposób. Działa w czystym Springu bez dodatkowych adnotacji: implementujesz metodę afterPropertiesSet() i kontener wywołuje ją po tym, jak zależności są już wstrzyknięte. Duchem jest to bardzo podobne do @PostConstruct, tylko zamiast adnotacji — interfejs.

Początkujący często tu pyta: „A po co w ogóle interfejs, skoro jest adnotacja?" Prosta odpowiedź: czasem ważniejsza jest jawna zależność od Springa niż neutralność. Szczególnie dotyczy to infrastrukturalnych beanów, gdzie nikt nie udaje, że klasa jest „frameworkowo-agnostyczna". Ale dla warstwy biznesowej i większości komponentów aplikacyjnych to jednak zbędne powiązanie: klasa zaczyna wprost zależeć od API Springa.

Zróbmy mały przykład beana, który po prostu zaznacza „kontekst gotowy" i drukuje wiadomość. To nie logika biznesowa, to czysta diagnostyka/infrastruktura — właśnie ten przypadek, gdzie powiązanie ze Springiem nie przeraża.

import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
class ContextFlowStartupMarker implements InitializingBean {
    @Override
    public void afterPropertiesSet() {
        // Metoda zostanie wywołana przez kontener po wstrzyknięciu zależności (i przed używaniem beana).
        System.out.println("ContextFlow startup marker: READY");
    }
}

Metoda nazywa się „afterPropertiesSet" historycznie, bo Spring kiedyś intensywnie używał setter injection. My woleliśmy constructor injection, ale metoda nadal oznacza zrozumiałą rzecz: „po tym jak kontener wszystko wstrzyknął i skonfigurował — zrób finalne przygotowanie".

Jest tu jedno metodyczne ryzyko: początkujący widzi interfejs i zaczyna myśleć, że to „właściwy standard". W praktyce częstszy domyślny teaching default to @PostConstruct, bo jest prostszy do czytania i mniej przywiązuje kod do Springa. InitializingBean warto znać właśnie jako część ogólnej mapy: spotkasz go w kodzie legacy i infrastrukturalnym, i ważne jest, żeby nie panikować.

4. initMethod w @Bean

Teraz trzeci sposób — bardzo praktyczny. Czasem klasa jest nasza, ale chcemy, żeby pozostała maksymalnie „zwykłą rzeczą Javową" bez adnotacji. Czasem klasa w ogóle nie jest nasza: biblioteczna, legacy albo taka, gdzie nie mamy prawa zmieniać kodu. A jeszcze bywa, że bean jest tworzony przez metodę @Bean i wygodnie nam jest opisać lifecycle bezpośrednio w konfiguracji, nie dotykając samej klasy.

Do tego służy parametr initMethod w @Bean. Kontener po stworzeniu beana wywoła metodę o podanej nazwie. Tak, to string (i tak, IDE może nie wychwycić błędu przy zmianie nazwy jeśli nie jesteś ostrożny), ale dla infrastrukturalnych rzeczy jest to często uzasadnione.

Zróbmy ReportOutputManager — obiekt odpowiadający za przygotowanie katalogu dla raportów. Według zasad projektu artefakty mają trafiać do build/reports, więc i przygotujemy build/reports.

import java.nio.file.Files;
import java.nio.file.Path;

class ReportOutputManager {
    private final Path dir;

    ReportOutputManager(Path dir) {
        // Zależność (ścieżka) podawana z zewnątrz — obiekt pozostaje prostym POJO.
        this.dir = dir;
    }

    void prepare() {
        // Logika init: upewnij się, że katalog istnieje.
        try {
            Files.createDirectories(dir);
        } catch (Exception e) {
            // Jeśli katalogu nie można stworzyć — lepiej paść przy starcie z czytelną przyczyną.
            throw new IllegalStateException(e);
        }
    }

    Path dir() {
        return dir;
    }
}

Klasa to absolutne POJO: żadnych interfejsów Springa, żadnych adnotacji. Teraz opiszemy fazę init w konfiguracji.

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

import java.nio.file.Path;

@Configuration
class ReportingConfig {
    @Bean(initMethod = "prepare")
    ReportOutputManager reportOutputManager() {
        // Kontener sam wywoła prepare() po stworzeniu beana.
        return new ReportOutputManager(Path.of("build/reports"));
    }
}

Fika polega na tym, że prepare() wywoła kontener. Oznacza to, że każdy serwis, który dostał ReportOutputManager przez DI, może zakładać: „jeśli kontekst wystartował, katalog jest już stworzony". I to właśnie ten rodzaj gwarancji, dla których Spring powstał: mniej ręcznego protokołu między obiektami, więcej przewidywalności.

Mały przykład użycia — choćby tylko dla demonstracji, że serwis nie musi już „sam wywoływać prepare()".

import org.springframework.stereotype.Service;

@Service
class ReportingService {
    private final ReportOutputManager output;

    ReportingService(ReportOutputManager output) {
        // Dostajemy już „przygotowany" bean (jeśli kontekst wystartował pomyślnie).
        this.output = output;
    }

    void printOutputDir() {
        // Demonstracja: katalog powinien być gotowy do użycia.
        System.out.println(output.dir()); // build/reports
    }
}

Ważne, żeby nie mylić initMethod z „no to wywołam prepare() bezpośrednio wewnątrz metody @Bean". Wywołać możesz oczywiście, ale wtedy zamienia się to w ręczny protokół wewnątrz konfiguracji. O wiele uczciwiej (i łatwiej do czytania), gdy jawnie mówisz kontenerowi: „to jest część lifecycle, wywołaj sam".

5. Wybór sposobu inicjalizacji

Po trzech wariantach chce się zapytać: „To który jest właściwy?" I to dobre pytanie, bo u początkującego często włącza się tryb „zbiorę combo": @PostConstruct + InitializingBean + initMethod — na wszelki wypadek, żeby na pewno zadziałało. Ale Spring to nie mikrofalówka: „podgrzej dwa razy" nie znaczy „dwa razy smaczniejsze". Raczej odwrotnie: dostaniesz niejasną kolejność wywołań i nieprzewidywalność.

Najprościej wybrać tak: jeśli to twoja klasa i inicjalizacja jest częścią jej logiki — bierz @PostConstruct. Jeśli robisz infrastrukturalny bean i godzisz się, że jawnie zależy od Springa — InitializingBean też jest odpowiedni. Jeśli klasy dotykać nie można lub nie chcesz — initMethod w @Bean. To decyzja nie o „modne/niemodne", lecz o czytelność i architektoniczną cenę.

Kompaktowa tabela, którą wygodnie mieć przed oczami:

Podejście Gdzie najwygodniejsze Główna cena
@PostConstruct twoja klasa, chcesz czytelny init przy kodzie potrzebna adnotacja na klasie (i zależność jakarta.annotation)
InitializingBean infrastrukturalne beany, legacy, gdzie powiązanie ze Springiem jest ok klasa „przecieka" wiedzą o Springu
initMethod rejestracja przez @Bean, cudza klasa, POJO bez adnotacji stringowa nazwa metody, ryzyko błędów przy refaktoringu

I jeszcze jedno ważne kryterium: dobra metoda init powinna być mała i nudna. Nudna — w dobrym sensie. Jeśli metoda init wywołuje pięć serwisów, wysyła powiadomienia, liczy raporty i robi żądania sieciowe, to już nie jest „inicjalizacja", lecz „tajne uruchamianie procesu biznesowego bez pytania". Kontener ci za to nie winien, ani twoi współpracownicy.

6. Kolejność wywołań callbacków init

Czasem warto raz zobaczyć, co będzie, jeśli „wymieszamy wszystko". Robimy to świadomie, jak w laboratorium: w normalnym życiu tak pisać nie trzeba, ale dla rozumienia kolejności wywołań — bardzo poglądowe. Potem spokojnie będziesz czytać cudzy kod i rozumieć, dlaczego ma „dodatkowe logi" i „wszystko wywołuje się trzy razy".

Oto bean używający jednocześnie wszystkich wariantów:

import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.InitializingBean;

class InitOrderDemoBean implements InitializingBean {

    InitOrderDemoBean() {
        // 1) Konstruktor zawsze wywoływany przy tworzeniu obiektu.
        System.out.println("1) constructor");
    }

    @PostConstruct
    void pc() {
        // 2) Wywoływany przez kontener po stworzeniu beana i wstrzyknięciu zależności.
        System.out.println("2) @PostConstruct");
    }

    @Override
    public void afterPropertiesSet() {
        // 3) Callback przez interfejs Springa InitializingBean.
        System.out.println("3) afterPropertiesSet");
    }

    void initMethod() {
        // 4) Callback podany w @Bean(initMethod = "...").
        System.out.println("4) initMethod");
    }
}

Podłączymy go przez @Bean(initMethod = "initMethod"):

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

@Configuration
class DemoConfig {
    @Bean(initMethod = "initMethod")
    InitOrderDemoBean demo() {
        // Ważne: initMethod() zostanie wywołany przez kontener, a nie tu ręcznie.
        return new InitOrderDemoBean();
    }
}

I uruchomimy minimalny kontekst:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App {
    public static void main(String[] args) {
        try (var ctx = new AnnotationConfigApplicationContext(DemoConfig.class)) {
            // Pobieramy bean — do tego momentu faza init jest już wykonana.
            ctx.getBean(InitOrderDemoBean.class);
        }
    }
}

Jeśli to uruchomisz, zobaczysz w konsoli kolejność, którą koncepcyjnie warto pamiętać: najpierw tworzony jest obiekt (konstruktor), potem wywoływane są callbacki init i dopiero po tym bean jest uznawany za gotowy.

Główny wniosek z demo jest prosty: kontener rzeczywiście potrafi wywołać to wszystko, ale nie znaczy to, że trzeba „podwajać zabezpieczenie". Wybierz jeden sposób na beana, a twoje przyszłe ja samo ci za to podziękuje (i przestanie pisać w dzienniku „dlaczego wszystko się psuje rano").

7. Przykłady w ContextFlow

Wróćmy teraz do naszego projektu ContextFlow. Mamy dwa komponenty, które bardzo naturalnie demonstrują fazę init. Pierwszy — NotificationTemplateCatalog: powinien przygotować wewnętrzny cache/katalog szablonów. Drugi — ReportOutputManager: powinien przygotować miejsce, gdzie będzie pisać raporty.

Tu ważne jest zauważenie, że nie „dodajemy init dla init". Rozwiązujemy praktyczny problem: chcemy, żeby serwisy nie zajmowały się cudzym przygotowaniem i nie nosiły w sobie protokołu „jeśli nie gotowe — przygotuj". W dobrej architekturze NotificationDispatchService zajmuje się powiadomieniami, a nie ładowaniem szablonów; ReportingService zajmuje się raportami, a nie tworzeniem katalogów.

Dlatego wybrane podejścia wyglądają naturalnie: dla NotificationTemplateCatalog — @PostConstruct (nasza klasa, logika przygotowania należy do niej), a dla ReportOutputManager — initMethod (klasę można trzymać jako POJO, a lifecycle opisać w konfiguracji).

Jeśli potem będziesz chciał zmienić implementację, np. zrobić inny ReportOutputManager dla innego trybu, model kontenerowy pozostanie taki sam: „stwórz, wstrzyknij, przygotuj, dopiero potem oddaj do use-case'u". I właśnie ten model mentalny budujemy dziś.

8. Typowe błędy przy inicjalizacji beanów

Błąd nr 1: zamienianie fazy init w „ukryty main".
Czasem deweloper dodaje @PostConstruct i zaczyna tam uruchamiać scenariusze biznesowe: „wygeneruję raport przy starcie", „wyślę wszystkim powiadomienie, że serwis wstał", „stworzę testowe zamówienie". W efekcie aplikacja staje się nieprzewidywalna: samo uruchomienie kontekstu już powoduje efekty uboczne. Logika init powinna być przygotowaniem i weryfikacją, a nie orkiestracją biznesową.

Błąd nr 2: robienie ciężkich przygotowań w konstruktorze „bo tak prościej".
Konstruktor wywołuje się przy każdym new, również w testach jednostkowych i w miejscach, gdzie nie oczekiwałeś realnego uruchomienia przygotowań. Jeśli wewnątrz konstruktora masz IO, tworzenie katalogów lub rozgrzewanie dużych struktur, dostaniesz ciężki i słabo kontrolowalny obiekt. O wiele lepiej trzymać konstruktor szybkim, a przygotowania — w fazie init, gdzie kontener zarządza momentem wywołania.

Błąd nr 3: mieszanie @PostConstruct, InitializingBean i initMethod na tym samym beanie.
Z punktu widzenia Springa to nie jest „błąd kompilacji", lecz dopuszczalne zachowanie: kontener wywoła wszystko, o co prosiłeś. Ale dla człowieka czytającego kod to prawie zawsze zamęt. Nie dostajesz „większej niezawodności", dostajesz „więcej miejsc, gdzie coś może się zepsuć" i „więcej pytań, dlaczego log drukuje się trzy razy".

Błąd nr 4: oczekiwanie, że init wywoła się u obiektu stworzonego ręcznie przez new.
To klasyczna pułapka przy testowaniu lub przy tymczasowych „ręcznych" instancjach. Jeśli tworzysz obiekt ręcznie, kontener nie obsługuje jego lifecycle: @PostConstruct się nie wywoła, afterPropertiesSet() się nie wywoła, initMethod tym bardziej. Albo uruchom kontekst, albo wywołaj przygotowanie jawnie, albo zaprojektuj klasę tak, żeby mogła działać bez kontenerowej magii (ale wtedy być może init w ogóle nie jest ci potrzebny).

Błąd nr 5: „połykanie" wyjątków w init i udawanie, że wszystko jest normalnie.
Czasem piszą try/catch w metodzie init i w catch po prostu drukują „ojej". To zamienia problem konfiguracyjny w bug runtime, który wyjdzie później i w dziwniejszym miejscu. Jeśli inicjalizacja jest krytyczna (katalog się nie stworzył, obowiązkowy szablon się nie załadował), lepiej paść przy starcie z czytelnym wyjątkiem.

1
Zadanie
Spring Core, poziom 10, lekcja 1
Niedostępne
Inicjalizacja katalogu przez @PostConstruct
Inicjalizacja katalogu przez @PostConstruct
1
Zadanie
Spring Core, poziom 10, lekcja 1
Niedostępne
Dwa sposoby init: InitializingBean i initMethod
Dwa sposoby init: InitializingBean i initMethod
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION