1. Sens czytania auto-config
Gdy Spring Boot działa „jak trzeba", wygląda magicznie. Gdy zachowanie odbiega od oczekiwań, chce się szybko zrozumieć trzy rzeczy: kto stworzył ten bean, po jakim warunku włączył się ten blok i jaka dźwignia jest tu w ogóle umiarkowana – property, własny bean czy exclusion. Dobra wiadomość: auto-configuration – to nie szamanizm, lecz zwykłe klasy Java z warunkami.
Do źródeł auto-config warto zaglądać nie dla heroicznego source-divingu, lecz dla bardzo praktycznego zadania: wybrać minimalną interwencję. Czasem wystarczy zmienić property. Czasem potrzebny jest własny bean, żeby Boot ustąpił. Czasem okazuje się, że problem naprawdę leży w całym bloku auto-config.
Potrzebny jest spokojny i powtarzalny algorytm: od „dziwnego zachowania" dojść do konkretnej klasy auto-config, zrozumieć, jakie warunki ją włączyły i wybrać najmniejszą interwencję: property, własny bean lub (w rzadkich przypadkach) exclusion. I można czytać źródła bez bólu, jeśli wiedzieć, gdzie patrzeć i gdzie się zatrzymać, żeby nie wpaść w niekończący się source-diving.
Component scanning i auto-configuration imports
Bardzo częsty błąd nowicjusza – myślenie, że Spring „wszystko znajdzie przez skanowanie pakietów". Tak, Twoje @Component, @Service, @Controller (gdy się pojawią) naprawdę żyją w świecie component scanning. Ale auto-configuration żyje w innym świecie: Spring Boot importuje specjalne klasy konfiguracyjne z zależności na podstawie wcześniej przygotowanej listy. Dlatego szukanie auto-config przez „a w jakim pakiecie to zostało zeskanowane?" – to jak szukanie kota w lodówce: możliwe, że kiedyś go tam zamknąłeś, ale koty zazwyczaj nie mieszkają w tym miejscu.
Porównajmy to w jednej tabeli – naprawdę pomaga utrzymać obraz w głowie, gdy tydzień później otwierasz projekt i próbujesz sobie przypomnieć, „kto tu jest główny".
| Mechanizm | Co rejestruje | Gdzie leży | Jak trafia do kontekstu | Typowe pytanie |
|---|---|---|---|---|
| Component scanning | Twoje komponenty aplikacji | Twój kod (src/main/java) | Spring skanuje pakiety od @SpringBootApplication | „Dlaczego mój serwis nie został stworzony?" |
| Auto-configuration | Domyślne infrastrukturalne beany | Zależności (.jar) | Spring Boot czyta plik imports i importuje klasy auto-config | „Kto stworzył ten bean i dlaczego?" |
| @Configuration + @Bean | Jawne beany | Twój kod (pakiet config) | Sam rejestrujesz | „Jak aknuratnie zastąpić default?" |
Jeśli to utrzymać w głowie, przestajesz „zgadywać" i zaczynasz diagnozować. A zgadywanie w Spring Boot – to zajęcie wdzięczne, bo Boot ma lepszą pamięć niż my: pamięta wszystkie zależności na classpath.
2. Plik imports AutoConfiguration.imports
Gdy mówi się „Boot podłącza auto-config", przydatny jest konkretny obraz. To nie jest mistyczna teleportacja, lecz czytanie zwykłego pliku tekstowego wewnątrz jara. W Spring Boot 4 (jak i w nowoczesnych wersjach wcześniej) główna lista kandydatów do auto-configuration przechowywana jest w zasobie: META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
Format prosty: po jednej linii – pełna nazwa klasy. Boot czyta te linie, dostaje listę kandydatów i dalej stosuje warunki (@Conditional...), żeby zrozumieć, co naprawdę włączyć. To znaczy, że plik imports odpowiada na pytanie: „jakie klasy auto-config w ogóle są brane pod uwagę?" – zanim zacznie się magia warunków.
Weźmy ten sam kawałek aplikacji startup-support: StartupMessageFormatter, StartupSummaryRunner i auto-config wokół nich. Mały przykład pliku imports (mocno skrócony, żeby mózg nie uciekł):
com.example.catalogservice.support.startup.StartupSupportAutoConfiguration
com.example.catalogservice.support.startup.StartupReportAutoConfiguration
Ważne, że to nie jest component scanning. Klasy te nie muszą leżeć w Twoich pakietach. Mogą przyjść z dowolnej zależności – czy z spring-boot-autoconfigure, czy z zewnętrznego startera.
Szukanie pliku imports w IDE i terminalu
Gdy słyszysz „otwórz jar i znajdź wewnątrz META-INF/...", chce się udać, że nagle odcięło Ci internet i idziesz do lasu. W praktyce wszystko jest prostsze: IDE potrafi otwierać zawartość zależności, a terminal potrafi pokazywać listę plików wewnątrz jara.
Jeśli jesteś w IntelliJ IDEA, zazwyczaj wystarczy otworzyć External Libraries, znaleźć spring-boot-autoconfigure-..., rozwinąć go jak folder i przejść po ścieżce META-INF/spring/.... Wygląda prawie jak zwykłe drzewo plików. Nic strasznego – jar to w zasadzie zip z klasami i zasobami.
Jeśli bardziej lubisz terminal, minimalne polecenie wygląda tak (przykład dla Linux/macOS, koncepcyjnie):
# Pokazujemy listę plików wewnątrz jara (bez rozpakowywania)
jar tf ~/.gradle/caches/.../spring-boot-autoconfigure-4.0.3.jar | grep "AutoConfiguration.imports"
Żeby obejrzeć zawartość konkretnego pliku, zazwyczaj robi się jar xf (wyodrębnij) lub używa się unzip -p. Tu nie będę zasypywać Cię „magią poleceń" – myśl jest ważniejsza niż komenda: plik imports to zwykły zasób wewnątrz jara. Można go otworzyć i przeczytać jako tekst.
3. Algorytm czytania auto-config
Podchodząc do auto-config jak do powieści na 900 stron – przegrasz. Podchodząc jak inżynier z pytaniem i krótką listą kontrolną – wygrasz. Poniżej – roboczy algorytm, który dobrze pasuje do rzeczywistości Junior-developera i nie wymaga „rozumienia całego Spring".
| Krok | Co robisz | Co chcesz wiedzieć |
|---|---|---|
| 1 | Formułujesz zachowanie słowami | „Co dokładnie mnie dziwi?" |
| 2 | Określasz, który bean/class za to odpowiada | „To jaki typ/interfejs?" |
| 3 | Znajdujesz klasę auto-config, która tworzy ten bean | „Kto w ogóle tworzy default?" |
| 4 | Sprawdzasz, dlaczego auto-config się włączyła | „Jakie @Conditional... zadziałały?" |
| 5 | Czytasz metodę @Bean i jej warunki | „Kiedy tworzony jest konkretny bean?" |
| 6 | Wybierasz minimalną interwencję | „Property? Własny bean? Exclude?" |
| 7 | Po zmianie potrafisz wyjaśnić wynik | „Co jest teraz default, a co – moje?" |
Jeśli chcesz zapamiętać bardzo krótko, to tak: kto → dlaczego → gdzie ustępuje → czym jest zarządzany.
4. Czytanie auto-config od góry do dołu
Główny lifehack czytania auto-config: nie zacznij od środka. Zacznij od adnotacji klasy i pierwszych linii – tam zazwyczaj leży „reguła włączenia". Dopiero potem schodź do metod @Bean. To jak czytanie umowy: najpierw dowiadujemy się, co w ogóle podpisałeś, a potem studiujemy punkt 7.4.12 o karze za spóźnioną dostawę.
Oto minimalny dydaktyczny auto-config, który wygodnie czytać (podobny do prawdziwych, ale nieprzeciążony detalami):
package com.example.catalogservice.support.startup;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
// Ta klasa uczestniczy w mechanizmie Spring Boot auto-configuration
@AutoConfiguration
// Włączamy konfigurację tylko jeśli potrzebna klasa naprawdę jest na classpath
@ConditionalOnClass(StartupMessageFormatter.class)
public class StartupSupportAutoConfiguration {
@Bean
// Boot "ustępuje", jeśli zadeklarowałeś własny bean tego typu
@ConditionalOnMissingBean
StartupMessageFormatter startupMessageFormatter() {
// Domyślna implementacja: prosty format komunikatu
return appName -> "Started: " + appName;
}
}
Tu „od góry do dołu" czyta się dosłownie jak ludzki tekst. Najpierw widzimy, że to @AutoConfiguration, czyli klasa uczestniczy w mechanizmie auto-configuration. Potem widzimy @ConditionalOnClass(StartupMessageFormatter.class) – czyli „ten blok ma sens tylko jeśli taka klasa w ogóle jest na classpath". I dopiero potem dochodzimy do @Bean, który tworzony jest tylko jeśli w aplikacji nie ma innego StartupMessageFormatter (@ConditionalOnMissingBean).
Najbardziej przydatna rzecz, którą możesz wyciągnąć z czytania takich klas: Boot prawie zawsze wprost mówi, gdzie ustąpi. Jeśli widzisz @ConditionalOnMissingBean, to często oznacza: „chcesz po swojemu – zadeklaruj własny bean".
5. Mini-case: formatowanie komunikatu startowego
Weźmy ten sam kawałek aplikacji startup-support. Kontrakt jest ten sam: StartupMessageFormatter – interfejs z metodą format(String appName). A kod aplikacyjny zależy od niego przez StartupSummaryRunner.
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 {
// Zależność od abstrakcji: implementacja może przyjść z auto-config
private final StartupMessageFormatter formatter;
public StartupSummaryRunner(StartupMessageFormatter formatter) {
// Wstrzykujemy bean przez konstruktor (łatwiej testować i czytać)
this.formatter = formatter;
}
@Override
public void run(ApplicationArguments args) {
// W prawdziwym projekcie tu częściej będzie logger, ale dla przykładu wystarczy println
System.out.println(formatter.format("catalog-service"));
// Oczekiwane wyjście (jeśli włączyła się domyślna implementacja):
// Started: catalog-service
}
}
Jeśli nie tworzyłeś StartupMessageFormatter jawnie, logiczne pytanie: „kto go stworzył?". I tu przestajemy zgadywać i przechodzimy do algorytmu.
Patrzymy na typ: StartupMessageFormatter. Dalej mamy dwa realistyczne scenariusze. Albo to nasz kod (i wtedy łatwo znajdziemy, gdzie jest tworzony), albo przyszedł z auto-config (np. podłączyłeś bibliotekę/starter albo masz wewnętrzny moduł). Wtedy szukamy klasy auto-config tworzącej bean tego typu. W przykładzie dydaktycznym to StartupSupportAutoConfiguration.
Kolejne pytanie: „jak ten auto-config w ogóle się podłączył?". Odpowiedź: przez plik imports wewnątrz jar, gdzie wymieniony jest StartupSupportAutoConfiguration. To znaczy, że nie czytamy „magicznej rzeczywistości", lecz konkretny łańcuch: imports → klasa auto-config → @Conditional... → @Bean.
6. Czytanie auto-config jako droga do rozwiązania
Bardzo typowa pułapka nowicjusza – otworzyć klasę auto-config, zobaczyć wewnątrz dwadzieścia linii i zacząć czytać każdą jak święte pismo. W efekcie mózg się przegrzewa, a rozwiązania nadal nie ma. Żeby tego uniknąć, przydatne jest trzymanie w głowie „pytań-filtrów", które zadajesz kodowi.
Wygodnie trzymać je wprost w formie krótkiego komentarza (to nie lista w sensie „wypiszmy 40 punktów", lecz raczej ściągawka do powtarzania jak mantrę):
// Szybkie czytanie auto-config:
// 1) Co to za blok i jak się nazywa?
// 2) Po jakich warunkach w ogóle się włącza?
// 3) Jaki bean tworzy (typ ważniejszy niż nazwa)?
// 4) Gdzie ustępuje (OnMissingBean / OnProperty)?
// 5) Co jest mniejsze skalą: property, własny bean czy exclude?
Jeśli odpowiadasz na te 5 pytań, prawie zawsze znajdujesz normalną drogę kastomizacji bez „framework takeover". I właśnie to jest dorosły cel: nie stać się osobą, która w każdej niejasnej sytuacji pisze exclude = Everything.class.
7. @ConditionalOnProperty jako przełącznik
Druga złota adnotacja do czytania – @ConditionalOnProperty. Zazwyczaj oznacza: „to zachowanie można włączać/wyłączać ustawieniem". To znaczy, że zamiast pisać własny bean lub wykluczać auto-config, możesz po prostu ustawić property. To prawie zawsze najtańszy sposób.
W tym samym startup-support layer można spotkać inną dźwignię: bean nie jest zastępowany własnym, lecz włączany i wyłączany przez property. Przykład dydaktyczny:
package com.example.catalogservice.support.startup;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class StartupReportAutoConfiguration {
@Bean
@ConditionalOnProperty(
// Nazwa właściwości zarządzającej włączeniem beana
name = "app.catalog.startup-report-enabled",
// Włączamy bean tylko przy jawnej wartości "true"
havingValue = "true",
// Jeśli właściwości nie ma, uważamy że jest true (zachowanie "włączone domyślnie")
matchIfMissing = true
)
StartupReportSink startupReportSink() {
// Domyślny sink: po prostu drukujemy komunikat (w prawdziwym świecie będzie logger/metryki)
return message -> System.out.println(message); // audit
}
}
Czytamy to jako zdanie: „stwórz StartupReportSink, jeśli właściwość app.catalog.startup-report-enabled jest równa true; jeśli właściwości nie ma – uważaj, że jest true". Ważne, że tu już widać rozwiązanie: jeśli chcesz wyłączyć raport – nie musisz wykluczać auto-config. Wystarczy ustawić app.catalog.startup-report-enabled=false w application.yaml.
Tak, nie jesteśmy jeszcze w module konfiguracji zewnętrznej i nie będziemy wchodzić w pierwszeństwo i profile. Ale przynajmniej już widzisz: źródła auto-config wprost pokazują Ci „gałkę sterowania". I to jest bardzo Boot-centryczny sposób myślenia: najpierw szukamy gotowego przełącznika, a potem dopiero wchodzimy w wiring.
8. Auto-config z obcych zależności
Czasem otwierasz klasę w IDE, a tam „decompiled code" i wszystko wygląda jakby pisał to robot nienawidzący ludzi. Normalna sytuacja: źródła zależności mogą nie pobrać się automatycznie. W większości IDE jest akcja w stylu „Download Sources" dla zależności Gradle. Po niej klasy auto-config stają się czytelne i życie się poprawia.
Jeśli źródeł nadal nie ma, masz dwie bezpieczne drogi. Pierwsza – patrzeć na adnotacje i sygnatury metod nawet w zdekompilowanej postaci: @ConditionalOnMissingBean i typ wartości zwracanej są widoczne prawie zawsze. Druga – w ostateczności, nie czytać całej klasy, lecz znaleźć obok klasę *Properties (lub przynajmniej zrozumieć nazwę property z @ConditionalOnProperty) i użyć tego jako „gałki" zachowania.
Sens w tym, że nawet bez idealnych źródeł wystarczy Ci zrozumieć dwie rzeczy: jaki typ beana jest tworzony i przy jakich warunkach. To wystarczy, żeby podjąć decyzję „własny bean vs property vs exclude". A wewnętrzna konfiguracja konkretnej biblioteki (z jej 300 opcjami) prawie zawsze wykracza poza ramy zadania dnia.
9. Typowe błędy przy czytaniu auto-config
Błąd nr 1: mylenie pliku imports z component scanning.
Nowicjusz widzi, że klasa auto-config leży w jakimś pakiecie i zaczyna szukać, dlaczego ten pakiet „jest skanowany". W efekcie zmienia @ComponentScan, przesuwa klasę main i projekt staje się jeszcze dziwniejszy. Prawidłowy model jest inny: auto-config brana jest z pliku imports, a nie ze skanowania.
Błąd nr 2: czytanie auto-config „od środka", pomijając warunki klasy.
Bardzo łatwo otworzyć klasę, przewinąć do @Bean i zacząć rozeznawać, co tam jest tworzone. Ale warunki na poziomie klasy mogą w pełni wyjaśnić, dlaczego ten blok w ogóle się włączył (np. z powodu obecności określonej klasy na classpath). Jeśli pominiesz górę, ryzykujesz rozwiązywanie nie tego problemu.
Błąd nr 3: „tonięcie w szczegółach" wewnątrz metody @Bean.
W prawdziwym kodzie Boot metody @Bean bywają rozbudowane: tam tworzą się buildery, wmieszają się properties, są optional-gałęzie. Nowicjusz próbuje zrozumieć wszystko, włącznie z każdym if. Na poziomie kursu zazwyczaj wystarczy zrozumieć typ tworzonego beana, kluczowe warunki (OnMissingBean, OnProperty) i miejsce, gdzie Boot daje punkt podmiany.
Błąd nr 4: zobaczenie @ConditionalOnProperty i mimo to zrobienie exclusion.
To klasyka: masz gotowy wyłącznik zachowania, ale wyłączasz cały blok przez exclude, a potem dziwisz się, że razem z nim zniknęło coś przydatnego. Jeśli jest property – prawie zawsze zacznij od niego, bo to minimalna interwencja.
Błąd nr 5: wyciągnięcie wniosku „Boot to magia", zamiast „Boot ma reguły".
Najbardziej szkodliwa myśl po czytaniu źródeł – „tam wszystko zbyt skomplikowane, niczego nie rozumiem". Bardziej przydatna myśl – „widzę reguły włączenia i widzę punkty ustąpienia". Nie musisz rozumieć całego frameworka, żeby zrozumieć, dlaczego pojawił się konkretny bean i jak go zastąpić. To jak z samochodem: żeby zmienić koło, nie trzeba znać termodynamiki silnika.
GO TO FULL VERSION