1. Ręczne składanie stosu – zły start
Jeśli wcześniej pisałeś głównie aplikacje konsolowe Java, zależność wyglądała prosto: „potrzebuję biblioteki – podłączyłem". Z aplikacjami webowymi, nawet małymi, to szybko przestaje działać: dla jednej „ficzery" potrzebujesz dziesiątek jar-ów, przy czym część z nich potrzebna jest nie tobie bezpośrednio, a infrastrukturze. I oto już nie piszesz kodu, a składasz LEGO bez obrazka na pudełku.
Wyobraź sobie, że potrzebujesz „po prostu postawić serwis HTTP i zwracać JSON". Ręcznie oznacza to wybór frameworka webowego, serwera (Tomcat/Jetty/Undertow), biblioteki JSON, logowania, zestawu podstawowych modułów Spring i jeszcze starannego uzgodnienia wersji tak, żeby to wszystko w ogóle ze sobą działało. W tym momencie wielu początkujących zaczyna myśleć, że „Spring jest trudny". Choć komplikuje się tu nie programowanie, a ręczne składanie stosu.
Spring Boot rozwiązuje ten ból nie tym, że „chowa zależności", a tym, że daje wygodne i sensowne punkty wejścia. Pierwszy taki punkt wejścia to starter.
2. Czym jest starter w Spring Boot
Słowo starter w nazwie zależności brzmi tak, jakby dostawano „gotową aplikację w pudełku". W rzeczywistości wszystko jest prostsze i niezawodniejsze: starter to zależność, która ciągnie za sobą zestaw innych zależności, dobranych pod konkretny scenariusz. Zazwyczaj sam starter prawie nie zawiera kodu aplikacyjnego. Jego zadaniem jest powiedzieć budowaniu: „potrzebuję takiego stosu".
W Gradle wygląda to maksymalnie prozaicznie – jak zwykła linia w dependencies:
dependencies {
// Podłączamy nie "jedną bibliotekę", a cały scenariusz (web MVC na stosie servlet)
implementation("org.springframework.boot:spring-boot-starter-webmvc")
}
I tu następuje ważny psychologiczny zwrot. Nie „podłączyłeś jedną bibliotekę webmvc". Zadeklarowałeś intencję: „moja aplikacja będzie serwisem webowym MVC opartym na servletach i Spring". W odpowiedzi Gradle pobierze nie jeden artefakt, a cały graf zależności tranzytywnych. Na razie wystarczy po prostu mieć w głowie: za jedną linią kryje się cała gałąź bibliotek.
Łatwo tu pomylić starter i BOM z poprzedniego wykładu. BOM odpowiada na pytanie „jakie wersje są kompatybilne", a starter – na pytanie „jakie biblioteki zazwyczaj są potrzebne dla tego scenariusza". To różne role, i razem dają bardzo silny efekt: jedną linią wybierasz scenariusz i dostajesz uzgodniony zestaw bibliotek bez ręcznego dobierania wersji.
3. Curated dependency set i „komplet elementów"
Słowo curated w IT zazwyczaj oznacza „dobrane i sprawdzone". W naszym kontekście curated dependency set to zestaw zależności, który już ktoś zebrał za ciebie tak, żeby był logiczny i w większości projektów działał „domyślnie". To przypomina komplet do składania stołu: nie musisz osobno kupować 32 śrub, 4 nóg i klucza sześciokątnego – bierzesz pudełko, gdzie to wszystko już jest i pasuje do siebie.
Jeśli przełożyć to na język projektów Spring Boot, starter to „pudełko pod scenariusz". Kilka przykładów scenariuszy, które napotkamy w catalog-service w miarę wzrostu projektu, wygląda tak:
| Scenariusz (po ludzku) | Jak wyrażane w Gradle | O co tak naprawdę prosisz platformę |
|---|---|---|
| „Potrzebuję serwisu web na MVC" | spring-boot-starter-webmvc | MVC, konwersja JSON, wbudowany serwer i podstawowa infrastruktura web |
| „Potrzebuję diagnostyki w runtime" | spring-boot-starter-actuator | Endpointy Actuator, integracje dla health/info i diagnostyczny szkielet |
| „Potrzebuję normalnego stosu testowego" | spring-boot-starter-test | JUnit 6 + Spring Test + wygodne narzędzia testowe w uzgodnionych wersjach |
Zauważ: w tych liniach nie ma wymieniania niskopoziomowych modułów. To normalne. Dobry build.gradle.kts to nie lista wszystkich śrub, a lista tego, jakie możliwości są potrzebne projektowi.
I jeszcze jedno: starter to nie „magia". To po prostu zależność, która rozkłada się w inne zależności. Magia zaczyna się dopiero wtedy, gdy przestajesz patrzeć, co dokładnie podłączyłeś, i czekasz, że „samo wszystko wymyśli". Tak nie robimy: rozumiemy, co się dzieje, tylko na zrozumiałym poziomie.
4. Starter i zachowanie aplikacji
Gdy początkujący słyszy „podłączyłem starter – i wszystko zadziałało", wydaje się, że tu jest jakaś mistyka. W rzeczywistości logika jest prosta: starter przynosi na classpath potrzebny zestaw bibliotek, a Spring Boot już potrafi podnieść sensowną infrastrukturę ponad takim classpath. To znaczy starter nie „włącza ficzery" magicznym przyciskiem, a zmienia dostępny stos aplikacji.
Na przykład spring-boot-starter-webmvc to nie „jeden jar z kodem web", a wybrany scenariusz web na stosie servlet. Dlatego razem z nim do projektu przyjeżdżają moduły Spring MVC, obsługa JSON, wbudowany serwer i inne części, bez których taki serwis zazwyczaj nie działa.
Na razie wystarczy tu zatrzymać jedną myśl: starter wpływa na zachowanie przez classpath. A jak ten graf naprawdę czytać, skąd dokładnie biorą się Tomcat, Jackson i inne biblioteki, już wyjaśnia dependency tree.
5. Bezpośrednie zależności w catalog-service
catalog-service w tym kursie to projekt edukacyjny, ale powinien przypominać normalny, żywy serwis. I jeden z dobrych nawyków, które warto wyrobić wcześnie: nie zamieniać build.gradle.kts w składowisko przypadkowych jar-ów. Lista bezpośrednich zależności powinna być krótka, bo każda linia to decyzja, która zmienia classpath i zachowanie aplikacji.
Jeśli na chwilę oderwać się od bieżącego catalog-service i wziąć abstrakcyjny minimalny przykład Boot, zestaw bezpośrednich zależności może wyglądać tak. To właśnie minimalny przykład, a nie bieżący snapshot dnia:
dependencies {
// Podstawowy szkielet Spring Boot (bez przywiązania do web/DB itd.)
implementation("org.springframework.boot:spring-boot-starter")
// Testowy „komplet": JUnit + Spring Test + narzędzia
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
Taki przykład jest użyteczny właśnie jako abstrakcja: widać, że bezpośrednich zależności jest mało, i każda linia wyraża rolę, a nie wnętrze classpath.
Roboczy snapshot catalog-service, od którego teraz wychodzimy, jest konkretniejszy: serwis złożony jest przez scenariusz web, a nie przez bazowy starter. Jego jądro to spring-boot-starter-webmvc plus spring-boot-starter-test. Jeśli potem ponad tym snapshotem będzie potrzebna jeszcze warstwa diagnostyczna, build dorośnie dosłownie o jedną zrozumiałą linię:
dependencies {
// Web MVC na stosie servlet: kontrolery, JSON, embedded server i podstawowa infrastruktura web
implementation("org.springframework.boot:spring-boot-starter-webmvc")
// Diagnostyka: health/info/metrics i inne możliwości actuator
implementation("org.springframework.boot:spring-boot-starter-actuator")
// Testowy starter – zazwyczaj zostaje jeden, bez ręcznego składania stosu testowego
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
To rozszerzenie bieżącego baseline'u, a nie jego ukryta zamiana. Nawet z Actuator build nadal czyta się jako lista możliwości: web, diagnostyka, testy. Nie wymieniamy ręcznie Tomcat, Jackson i logback – robi to wybrany zestaw starter'ów.
Porównaj to ze „ręcznym" stylem, który często spotykamy u początkujących po kilku wyszukiwaniach w Google. Wygląda „poważnie", ale w praktyce prawie zawsze jest gorszy:
dependencies {
// „Ręczny" zestaw modułów pod warstwę web: wygląda kontrolowanie, ale łatwo pomylić się i dostać konflikt wersji
implementation("org.springframework:spring-webmvc")
implementation("org.apache.tomcat.embed:tomcat-embed-core")
implementation("com.fasterxml.jackson.core:jackson-databind")
implementation("ch.qos.logback:logback-classic")
implementation("org.slf4j:slf4j-api")
}
Problem tu nie w tym, że te biblioteki są „złe". Problem jest inny: wziąłeś na siebie rolę zespołu Spring Boot. Teraz ty odpowiadasz za kompatybilność, za właściwy zestaw modułów, za wyrównanie wersji i za to, żeby niczego nie zapomnieć. Dla projektu edukacyjnego to gwarantowane przeciążenie poznawcze. Dla produkcji taka ścieżka bywa uzasadniona, ale tylko jeśli już dobrze rozumiesz, co dokładnie robisz i po co.
W ramach kursu chcemy, żeby build.gradle.kts przypominał listę tego, czego projekt potrzebuje – web, actuator, testy – a nie wyliczenie wszystkich wewnętrznych elementów.
6. Starter'y i zwykłe zależności
Łatwo wpaść w skrajność: „skoro w Boot są starter'y, to wszystko podłącza się przez starter'y". Tak nie jest. Starter to dobry format dla scenariuszy. Ale czasem potrzebujesz nie scenariusza, a małego narzędzia. Na przykład procesor adnotacji do generowania metadanych konfiguracji – to nie ficzera runtime, a wsparcie build-time. I tu starter nie jest potrzebny.
W catalog-service będziemy mieć zależność, którą często mylą ze starter'em tylko dlatego, że też jest z Boot. Ale jej rola jest inna: spring-boot-configuration-processor. Pomaga IDE i budowaniu rozumieć twoje własne @ConfigurationProperties i podłącza się jako annotation processor.
W Gradle wygląda to zazwyczaj tak:
dependencies {
// Potrzebny tylko na etapie kompilacji: generuje metadane dla IDE/podpowiedzi dla @ConfigurationProperties
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
// Stos testowy – osobno, żeby nie obciążać runtime classpath
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
I tu pojawia się jeszcze jedna ważna myśl dla początkującego: nie każda zależność powinna leżeć w implementation. Czasem jest to testImplementation – potrzebne tylko do testów, czasem annotationProcessor – potrzebne na etapie kompilacji, czasem developmentOnly – potrzebne do lokalnego developmentu. Jeśli wszystko bez rozróżnienia kłaść do implementation, classpath staje się cięższy, głośniejszy i mniej przewidywalny.
Starter'y pomagają utrzymywać główny stos w porządku, ale nie eliminują zdrowego rozsądku. Po prostu teraz bazowe pytanie brzmi inaczej: „potrzebuję scenariusza – starter, czy narzędzia – zwykła biblioteka/procesor?"
7. Typowe błędy przy pracy ze starter'ami
Ze starter'ami najczęściej się myli nie dlatego, że są trudne, a dlatego, że łamią przyzwyczajony model „jedna linia = jedna biblioteka". Na początku jest to jak przesiadka z roweru na elektryczną hulajnogę: niby to samo, ale nagle szybciej, i w ścianę można wjechać prościej. Poniżej kilka błędów, które warto zawczasu rozpoznawać, żeby nie zamieniać dependency management w loterię.
Błąd nr 1: myślenie, że starter to „jeden gruby jar z wszystkim w środku".
Starter to nie „fat jar" i nie jakaś monolityczna biblioteka. Rozkłada się w zestaw zależności, i właśnie ten zestaw formuje classpath. Jeśli trzymać w głowie model „tam w środku magia", nieuchronnie przestaniesz rozumieć, co naprawdę jest podłączone i dlaczego aplikacja zachowuje się właśnie tak.
Błąd nr 2: podłączyć starter i obok ręcznie „dosypywać" niskopoziomowe moduły tego samego scenariusza.
Na przykład dodać spring-boot-starter-webmvc, a obok jeszcze spring-webmvc, tomcat-embed-core i kilka modułów Jackson „dla pewności". Wygląda jak troska, ale w praktyce to duplikacja i potencjalne konflikty. Jeśli masz już starter, najpierw przyjmij, że przywiózł bazowy zestaw, a dopiero potem dodawaj to, czego naprawdę brakuje – i co potrafisz uzasadnić.
Błąd nr 3: wybrać starter po „podobnej nazwie", a nie po zadaniu.
W ekosystemie Boot są zależności, które brzmią niemal tak samo. Początkujący często wybiera według zasady „w nazwie jest web – to to". Prawidłowo jest najpierw sformułować scenariusz słowami: „chcę servlet MVC" lub „chcę diagnostykę" – i dopiero pod to wybierać starter. Nazwa to wskazówka, ale nie gwarancja trafienia w cel.
Błąd nr 4: oczekiwanie, że starter rozwiąże architekturę aplikacji.
Starter rozwiązuje problem warstwy zależności: przynosi uzgodniony zestaw bibliotek. Nie decyduje, jak organizować pakiety, gdzie trzymać logikę biznesową, jak nazywać serwisy i jakie endpointy robić. Jeśli czekasz, że „podłączyłem starter – i wszystko stało się prawidłowe", bardzo szybko zaczniesz budować aplikację według zasady „no Boot jest mądry".
Błąd nr 5: próbować „zoptymalizować" build i usunąć „zbędne" tranzytywne zależności, nie rozumiejąc ich roli.
Gdy po raz pierwszy zobaczysz drzewo zależności, wyda się ogromne. I to normalna reakcja. Ale chęć „posprzątania" grafu bez rozumienia szybko prowadzi do sytuacji, gdy aplikacja przestaje startować, a przyczyną jest jedna przypadkowo wykluczona biblioteka. Starter'y istnieją właśnie dlatego, że w złożonym stosie „po prostu usunąć zbędne" zazwyczaj nie można bez konsekwencji.
GO TO FULL VERSION