CodeGym /Kursy /Spring Boot /Własny bean zamiast domyślnego

Własny bean zamiast domyślnego

Spring Boot
Poziom 9, Lekcja 0
Dostępny

1. Kiedy potrzebny jest override default

W pewnym momencie każdy programista styka się z odczuciem: „Boot jest świetny, ale tu bym zrobił trochę inaczej". To normalny etap dojrzewania projektu. Serwis już startuje, ma strukturę i zaczynasz widzieć szczegóły: tu chciałbyś inny format wiadomości, inny algorytm, inny mały kawałek zachowania.

Ważne, żeby nie pomylić dwóch rodzajów chęci. Pierwsza – zdrowa: „muszę zmienić jeden punkt zachowania, bo mamy konkretny powód". Druga – niebezpieczna: „nie podoba mi się, że nie rozumiem, skąd się to wzięło, więc wyłączmy połowę platformy i złóżmy wszystko ręcznie". Drugi typ zazwyczaj prowadzi do tego, że niby „przejąłeś kontrolę", ale tak naprawdę straciłeś zalety Boot: przewidywalne defaults, kompatybilność i normalną obsługę aktualizacji.

Ustalmy od razu: najbezpieczniejszy sposób kastomizacji Boot – to nie wyłączanie, lecz zamiana jednego konkretnego beana na własny. Boot ma do tego całą filozofię: „skonfiguruje domyślnie, ale jeśli jawnie powiedziałeś 'mam swój wariant' – usunę się w cień". W anglojęzycznych materiałach często nazywa się to backoffem – „cofaniem się".

Boot już potrafi sam składać defaults przez auto-configuration. Teraz ważne jest, żeby nie psuć tego składania grubiańską ręką. Wygodnie trzymać w głowie prostą drabinę interwencji: najpierw szukamy gotowej właściwości lub flagi, potem zamieniamy jeden konkretny bean i dopiero jeśli cały blok zachowania naprawdę nie jest potrzebny – myślimy o exclusion. A gdy trzeba otwierać źródła auto-config, cel jest ten sam: wybrać najsłabszą dźwignię, a nie przebudowywać platformę ręcznie.

2. Default-beany i auto-configuration

Gdy podłączasz startery, na classpath pojawiają się biblioteki. A razem z nimi – klasy auto-configuration: specjalne konfiguracje, które potrafią rejestrować infrastrukturalne beany. Z punktu widzenia Spring to te same klasy `@Configuration`, tylko podłączają się nie przez component scanning, lecz przez mechanizm auto-configuration.

Wyobraź sobie, że Spring Boot to troskliwy kolega, który przychodzi do projektu i mówi: „Widzę, że w zależnościach masz to i to. Znaczy, że prawdopodobnie potrzebujesz takich-a-takich komponentów technicznych. Mogę je stworzyć i skonfigurować 'rozsądnie domyślnie'". I właśnie to są default-beany: nie „obowiązkowa prawda", lecz propozycja platformy, którą można przyjąć lub zastąpić.

Żeby się nie mylić, wygodnie trzymać w głowie mini-tabelkę:

Termin Po ludzku Przykład w naszym kursie
Business bean Twój kod aplikacyjny: domena, serwisy, repozytoria CourseCatalogService, InMemoryCourseCatalogRepository
Infrastructure bean Techniczna rzecz, która obsługuje działanie aplikacji Formatowanie komunikatów startowych, konwertery, ustawienia „jak startować"
Default bean Infrastrukturalny bean tworzony przez Boot „jeśli nie zrobiłeś inaczej" „Domyślny formater komunikatów" w przykładzie dydaktycznym
User-defined bean Twój bean, który jawnie definiuje potrzebne zachowanie Twój formater komunikatów w config

I tu klucz: Boot tworzy wiele default-beanów tak, żeby można je było bezpiecznie zastąpić. To nie efekt uboczny, lecz design. A mechanizm, który to umożliwia, najczęściej wygląda jak jedna adnotacja – @ConditionalOnMissingBean.

3. @ConditionalOnMissingBean i backoff

Gdy widzisz @ConditionalOnMissingBean, warto tłumaczyć ją nie jako „jakaś warunkowa magia", lecz jako bardzo potoczną frazę:

„Stwórz ten bean, tylko jeśli w aplikacji nie ma jeszcze beana takiego typu (lub o takiej nazwie)."

To właśnie umowa między Boot a Twoją aplikacją. Boot mówi: „Dam Ci wariant domyślny, ale nie będę się kłócić, jeśli zadeklarujesz swój". Dzięki temu możesz zmieniać zachowanie aplikacji punktowo, nie psując wszystkiego pozostałego.

Oto jak mniej więcej wygląda tok myślenia Boot (uproszczony, ale uczciwy):

flowchart TD
    A[Boot składa kontekst] --> B[Najpierw rejestrowane są Twoje beany]
    B --> C[Potem podłączają się klasy auto-config]
    C --> D{Potrzebny default-bean X?}
    D -->|"w auto-config stoi @ConditionalOnMissingBean"| E{Czy bean typu X już istnieje?}
    E -->|tak| F[Boot ustępuje: default nie tworzymy]
    E -->|nie| G[Boot tworzy default-bean]

Zwróć uwagę na ważny szczegół: najpierw idą beany aplikacji (user-defined), potem auto-configuration. Właśnie dlatego „ustępowanie" działa. Gdyby kolejność była odwrotna, Boot zdążyłby stworzyć default, a potem Ty stworzyłbyś swój – i miałbyś dwa beany jednego typu, a dalej Spring by powiedział: „Panowie, wybierzcie jednego, nie jestem jasnowidzem".

Jeszcze jeden moment, który ciągle łapie nowicjuszy: najczęściej warunek „missing bean" sprawdza typ, a nie nazwę. To znaczy, że nieważne, że metoda nazywa się startupMessageFormatter() – ważne, że zwracany typ to StartupMessageFormatter. Jeśli chcesz zastąpić default, musisz zadeklarować bean tego samego sensu i tego samego typu, a nie „podobny po nazwie".

4. Praktyka: default + override w catalog-service

Teraz złożymy mały, ale bardzo pokazowy przykład bezpośrednio w kontekście catalog-service. Nie będziemy grzebać w prawdziwym kodzie Boot (to nie kurs „piszemy Spring Boot"), ale zrobimy dydaktyczną infrastrukturalną rzecz, która zachowuje się dokładnie tak, jak zachowują się prawdziwe auto-config defaults.

Idea: mamy StartupSummaryRunner, który coś wypisuje przy starcie. Chcemy, żeby format komunikatu można było zmienić bez przepisywania runnera. W tym celu wprowadzamy abstrakcję-interfejs StartupMessageFormatter, damy mu domyślną implementację „od platformy", a potem pokażemy, jak aplikacja definiuje własny formater i Boot-style default „ustępuje".

Kontrakt: interfejs dla kodu aplikacyjnego

Zaczynamy od najważniejszego: kontraktu. To mały interfejs, od którego będzie zależał nasz StartupSummaryRunner.

package com.example.catalogservice.support.startup;

public interface StartupMessageFormatter {

    /**
     * Formatuje komunikat do wyświetlenia przy starcie aplikacji.
     * Celowo bez adnotacji Spring: to czysty kontrakt.
     */
    String format(String appName);
}

Zwróć uwagę: tu nie ma wcale Spring. To zwykła Java. I to dobrze: im mniej „smaku frameworka" w domenowych/aplikacyjnych kontraktach, tym łatwiej je rozumieć i testować.

Default od platformy: bean tylko jeśli brak własnego

Teraz tworzymy konfigurację, która zachowuje się jak „platforma dała default". W prawdziwym Spring Boot takie rzeczy żyją w klasach auto-config. Zrobimy bardzo małą wersję, żeby zobaczyć wzorzec.

package com.example.catalogservice.support.startup;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;

@AutoConfiguration // To właśnie auto-configuration, a nie zwykła @Configuration "po skanie"
public class StartupSupportAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean // Default tworzony tylko jeśli użytkownik NIE zadeklarował własnego beana tego typu
    StartupMessageFormatter startupMessageFormatter() {
        return (String appName) -> {
            // Domyślny format – maksymalnie prosty i przewidywalny
            return "Started: " + appName;
        };
    }
}

Tu jest wszystko, po co się zebraliśmy:

  • @AutoConfiguration – oznacza „to konfiguracja, którą Boot może podłączać jako część mechanizmu auto-configuration".
  • @ConditionalOnMissingBean – oznacza „twórz default, tylko jeśli użytkownik jeszcze nie stworzył swojego".

Celowo zrobiliśmy implementację przez lambdę, żeby nie mnożyć klas. Teraz ważne jest zachowanie, a nie piękno architektury.

Podłączamy dydaktyczny auto-config przez plik imports

Żeby Boot w ogóle widział nasz dydaktyczny auto-config jako kandydata do auto-configuration, potrzebuje pliku imports. To nie component scan, lecz osobny mechanizm. Na razie traktuj to jako „służbową listę konfiguracji, które Boot jest gotowy podłączać automatycznie".

Plik: src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

com.example.catalogservice.support.startup.StartupSupportAutoConfiguration

Tak, to dosłownie lista klas, po jednej w linii. Żadnych zaklęć po łacinie – wszystko jest wystarczająco proste (co zazwyczaj zaskakuje tych, którzy oczekiwali magii).

Kod aplikacyjny: StartupSummaryRunner zależy od interfejsu

Teraz zmieniamy nasz runner tak, żeby nie zależał od „jakiejś konkretnej klasy", lecz od interfejsu. I właśnie to czyni override bezpiecznym.

package com.example.catalogservice.catalog.bootstrap;

import com.example.catalogservice.support.startup.StartupMessageFormatter;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class StartupSummaryRunner implements ApplicationRunner {

    // Wstrzykujemy właśnie kontrakt, a nie konkretną implementację:
    // dzięki temu zostawiamy sobie możliwość bezpiecznej zamiany implementacji bez przepisywania runnera.
    private final StartupMessageFormatter formatter;

    public StartupSummaryRunner(StartupMessageFormatter formatter) {
        this.formatter = formatter;
    }

    @Override
    public void run(ApplicationArguments args) {
        // Obserwowalne zachowanie: drukujemy ciąg, żeby zobaczyć, który formater jest naprawdę używany.
        System.out.println(formatter.format("catalog-service")); // Started: catalog-service
    }
}

Sens w tym, że StartupSummaryRunner teraz nie wie (i nie powinien wiedzieć), kto dokładnie formatuje komunikat. Wie tylko kontraktu. To jak gniazdko elektryczne: nie interesuje Cię, jaka elektrownia jest w mieście – ważne, żeby napięcie „pasowało".

Na tym etapie, jeśli uruchomisz aplikację, zobaczysz domyślny komunikat:

Started: catalog-service

Własny bean: Boot ustępuje, default nie jest tworzony

Teraz robimy to, po co wszystko było zaplanowane: deklarujemy własny bean tego samego typu StartupMessageFormatter. I kładziemy go tam, gdzie mu miejsce architektonicznie – w pakiecie config, żeby kastomizacje nie były ukryte „gdzieś przypadkowo".

package com.example.catalogservice.config;

import com.example.catalogservice.support.startup.StartupMessageFormatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class StartupConfiguration {

    @Bean
    StartupMessageFormatter startupMessageFormatter() {
        // Ten bean ma ten sam typ co default, więc warunek
        // @ConditionalOnMissingBean "nie przejdzie" i default z auto-config nie zostanie stworzony.
        return (String appName) -> {
            // Tu może być dowolna logika formatowania: prefiksy, środowisko, wersja itp.
            return "[catalog-service] " + appName + " is ready";
        };
    }
}

Teraz przy starcie StartupSupportAutoConfiguration powie: „Aha, StartupMessageFormatter już istnieje, a więc mój default nie jest potrzebny". I Twój runner wyświetli inne:

System.out.println(formatter.format("catalog-service"));
// [catalog-service] catalog-service is ready

To znaczy, że zmieniłeś jeden punkt zachowania i nie ruszyłeś reszty Boot-świata. To właśnie „zdrowa kastomizacja".

5. Interfejs jako polisa dla kastomizacji

Bardzo łatwo wpaść w pułapkę: „Po co mi interfejs, skoro mam konkretną implementację?". Pułapka odpala się szczególnie szybko u nowicjuszy, bo interfejsy często postrzegane są jako „biurokracja dla biurokracji". Ale w projektach Boot interfejs to nie ozdoba, lecz sposób na bezpieczną i tanią zamianę zachowania.

Wyobraź sobie, że StartupSummaryRunner bezpośrednio wstrzykuje konkretną klasę, np. DefaultStartupMessageFormatter. Wtedy, żeby zmienić zachowanie, musiałbyś albo zmieniać kod runnera (czyli ruszać warstwę aplikacyjną dla zmiany infrastrukturalnej), albo próbować „przechytrzyć" kontener przez @Primary, @Qualifier, zmiany nazw i inne tańce. To już nie „punktowa kastomizacja", lecz mini-quest.

Gdy runner wstrzykuje interfejs, masz schludny obraz: jest kontrakt, jest domyślny wariant od platformy i jest Twój wariant. Możesz zamienić implementację, nie zmieniając ani linijki w konsumencie. W dużych systemach oszczędza to nie minuty – dni, bo zmiany stają się lokalne i przewidywalne.

I jeszcze jeden mały, ale ważny plus: interfejs pozwala mentalnie oddzielić „co potrzebne" od „jak zrobione". To przydatne nawet bez Spring. Spring po prostu sprawia, że taki styl jest szczególnie przyjemny, bo wiring bierze na siebie kontener.

6. Sprawdzamy, że override zadziałał

Gdy nadpisujesz zachowanie własnym beanem, ważne jest, żeby umieć uczciwie odpowiedzieć sobie na pytanie: „Czy naprawdę zastąpiłem default, czy przypadkowo stworzyłem drugi bean i teraz wszystko trzyma się na szczęście?". Na poziomie Junior jest to szczególnie ważne, bo pierwsze problemy zwykle wyglądają jak „dlaczego wczoraj działało, a dziś nie".

Najprostszy sposób sprawdzenia w projekcie dydaktycznym – uczynić zachowanie obserwowanym. Już to zrobiliśmy przez wypisanie ciągu: po nim widać, który formater jest używany. Czasem przydatne jest tymczasowe wypisanie jeszcze klasy implementacji, żeby nie zgadywać:

System.out.println(formatter.getClass().getName());
// com.example.catalogservice.config.StartupConfiguration$$Lambda...

Tak, nazwa będzie dziwna (lambdy lubią się maskować), ale myśl jest jasna: dostajemy dowód, a nie odczucie.

Trochę bardziej „inżynierskie" podejście – trzymać kastomizacje w jednym widocznym miejscu. Jeśli Twój bean leży w config, zawsze wiesz, gdzie szukać odpowiedzi na pytanie „dlaczego format się zmienił". Jeśli zaś zadeklarowałeś bean przypadkowo w jakimś @Component ukrytym w głębi pakietów, sam sobie stwarzasz dochodzenie na piątkowy wieczór.

I na koniec: robiąc override, staraj się formułować to słowami tak, żeby brzmiało jak normalne wymaganie, a nie szamanizm. Na przykład: „Zastąpiliśmy StartupMessageFormatter naszym wariantem, dlatego default-bean z auto-config nie jest już tworzony". Brzmi nudno – a nudne znaczy przewidywalne. W infrastrukturze „nudne" to zazwyczaj komplement.

7. Typowe błędy przy przedefiniowywaniu default-beanów

Błąd nr 1: tworzą „własny bean", ale innego typu – i dziwią się, że default nie zniknął.
To najczęstsza pułapka. @ConditionalOnMissingBean zazwyczaj sprawdza typ. Jeśli default tworzony jest dla StartupMessageFormatter, a Ty zadeklarowałeś bean innego kontraktu lub zwracasz nie ten typ, który sprawdza warunek, default nigdzie nie pójdzie. Spring patrzy przede wszystkim na typ, a nie na ładną nazwę metody. Leczy się to prosto: zawsze sprawdzaj, jaki typ zwraca Twoja metoda @Bean i jaki typ stoi w warunku.

Błąd nr 2: wstrzykują konkretną implementację i potem próbują „przedefiniować default".
Dopóki wstrzykujesz interfejs, zamiana to zamiana. Jak tylko wstrzykujesz konkretną klasę, sam sobie odbierasz możliwość miękkiego override. W efekcie albo trzeba przepisywać kod konsumenta, albo urządzać skomplikowany wiring z kwalifikatorami. W projekcie dydaktycznym wygląda to jak „chciałem zmienić jedną linię, a dostałem trzy klasy i pięć adnotacji".

Błąd nr 3: chowają własny bean w przypadkowym miejscu projektu.
Czasem student robi klasę @Component gdzieś w catalog.service i myśli: „No przecież to bean, więc wszystko dobrze". Formalnie – tak, ale architektonicznie pomieszałeś warstwę biznesową z infrastrukturalną kastomizacją. Tydzień później zapomnisz, dlaczego serwis nagle odpowiada inaczej, i zaczniesz szukać „buga". Lepiej trzymać takie rzeczy w pakiecie config i nazywać konfigurację po sensie.

Błąd nr 4: próbują „zastąpić default" kopiując duży kawałek konfiguracji.
Bardzo kuszące: znaleźć w internecie przykład, skopiować całą klasę auto-config i „podrasować parę linii". Problem w tym, że przynosisz do projektu ogromny ogon bocznych ustawień, których nie rozumiesz. Dziś wszystko działa, jutro przy aktualizacji zależności – niespodzianka. Znacznie bezpieczniej zacząć od wąskiego override jednego beana, który naprawdę wpływa na potrzebny punkt zachowania.

Błąd nr 5: nie potrafią wyjaśnić, co dokładnie zmieniło się w kontekście.
Jeśli po override nie możesz krótko powiedzieć „który bean jest teraz używany i dlaczego default nie został stworzony", prawie zawsze oznacza to, że decyzja nie jest jeszcze uformowana w głowie. A wszystko, co nie jest uformowane w głowie, zazwyczaj formuje się w produkcji jako incydent. Dobry nawyk – mówić: „zadeklarowaliśmy własny bean typu X, więc @ConditionalOnMissingBean nie przeszedł i default nie został zarejestrowany". Brzmi jak nudny raport – i właśnie dlatego działa.

1
Zadanie
Spring Boot, poziom 9, lekcja 0
Niedostępne
Domyślny formatter z auto-konfiguracji
Domyślny formatter z auto-konfiguracji
1
Zadanie
Spring Boot, poziom 9, lekcja 0
Niedostępne
Niestandardowa fasola zamiast default-bean
Niestandardowa fasola zamiast default-bean
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION