1. Niezmienniki Spring Core
Gdy przechodzisz ze Spring Core do Spring Boot, warstwy REST, Spring Data lub Spring Security, pojawia się wrażenie, jakbyś trafił do innego frameworka: nowe startery, nowe adnotacje, nowe DSL-konfiguracje. To normalna emocja — tak mózg reaguje na nowe opakowanie. Ale inżynierska dobra wiadomość polega na tym, że pod maską prawie wszystko kręci się wokół tych samych mechanizmów, które już poznałeś na ContextFlow.
Zadanie teraz nie polega na ponownym uczeniu się DI, profili, lifecycle, zdarzeń i proxy. Powinny być one już w twoich rękach. Ważniejsze jest nauczyć się rozpoznawać te same mechanizmy w nowych ubraniach, gdy powyżej pojawiają się nowe role, adnotacje i runtime-obwiezienie. Jeśli łapiesz się na myśli „to już widziałem", to znaczy, że wszystko idzie dobrze.
Żeby nie utopić się w „zoo adnotacji", przydatne jest trzymanie jednego pytania jako wewnętrznego kompasu: „jaki mechanizm Spring Core teraz naprawdę działa?" Prawie zawsze odpowiedź będzie ze znajomego zestawu: rejestracja beanów, rozwiązywanie zależności, właściwości/profile, lifecycle, zdarzenia, post-procesory, proxy/AOP. A więc — nie zaczynasz od zera, po prostu uczysz się rozpoznawać znajome w nowym kostiumie.
Poniżej starannie (bez uciekania w nowe API) przejdziemy przez niezmienniki, które pozostają niezmienne, nawet jeśli powyżej kontenera pojawia się web, data lub warstwa security. Fokus nie będzie na nowych API, lecz na przekładzie „nowa fasada → znany mechanizm Core".
Niezmiennik nr 1: beany i DI — te same, role inne
Tu nie trzeba na nowo udowadniać, że constructor injection pozostaje bazowym stylem. Ważne jest zobaczenie, że te same obiekty zarządzane przez kontener po prostu dostają inne role.
Gdy w sąsiednich kursach widzisz słowa „controller", „repository", „security filter" lub „configuration properties", łatwo zapomnieć: to wszystko, w końcu, też są obiekty, którymi zarządza kontener. Mogą mieć niezwykłą rolę i szczególne otoczenie (np. żądanie web), ale podstawowa logika jest ta sama: kontener musi zarejestrować definicję beana, stworzyć obiekt, wstrzyknąć zależności, wykonać fazę init i oddać gotową instancję do pracy.
Spójrz na swój ContextFlow. ScenarioRunner — to „punkt wejścia scenariusza", który dostaje serwisy przez konstruktor. To jest już bardzo podobne do tego, jak potem będzie wyglądał web-kontroler: tylko zamiast „uruchom scenariusz" będzie „obsłuż żądanie HTTP". Mechanika DI przy tym się nie zmienia.
import org.springframework.stereotype.Component;
import com.example.contextflow.application.service.OrderPlacementService;
@Component // Spring zarejestruje tę klasę jako bean w kontekście
public class ScenarioRunner {
private final OrderPlacementService orderPlacementService;
public ScenarioRunner(OrderPlacementService orderPlacementService) {
// Zależność deklarowana jawnie: kontener sam dobierze i wstrzyknie potrzebny bean
this.orderPlacementService = orderPlacementService;
}
}
Jeśli mentalnie przenieść tę ideę do świata REST, dostaniesz mniej więcej ten sam kod, tylko w innej roli. Nie traktuj następnego przykładu jako „teraz uczymy się MVC" — to tylko ilustracja, że DI pozostaje takie samo:
import org.springframework.web.bind.annotation.RestController;
import com.example.contextflow.application.service.OrderPlacementService;
@RestController
public class OrderController {
private final OrderPlacementService orderPlacementService;
public OrderController(OrderPlacementService orderPlacementService) {
this.orderPlacementService = orderPlacementService;
}
}
I tu przydatne jest złapanie się na myśli: „aha, rozpoznaję ten trick". Konstruktor utrwala zależności, klasa ich nie szuka w kontenerze, DI pozostaje uczciwe, a kontener robi to, co robił przez cały kurs.
To samo dotyczy „repozytoriów" i „klientów zewnętrznych systemów" w przyszłych kursach. Tak, Spring Data potrafi generować implementację interfejsu repozytorium, i wygląda to jak „magia". Ale w istocie to ten sam mechanizm kontenera: w kontekście pojawia się bean, który spełnia interfejs, i można go wstrzyknąć do serwisu przez konstruktor. Psychologicznie będzie spokojniej, jeśli będziesz na to patrzeć jako na „kolejny sposób dostarczenia beana", a nie na „szczególną encję poza DI".
Niezmiennik nr 2: wybór kandydatów — ten sam ból
W czystym projekcie Core już widziałeś, że niejednoznaczność — to nie „rzadki bug", lecz normalna rzeczywistość. Interfejs często ma kilka implementacji: różne kanały powiadomień, różne strategie zniżek, różne formatery raportów. I kontener musi rozumieć, kogo dokładnie wstrzyknąć.
W Boot/REST/Data/Security ta sytuacja nie znika. Wręcz przeciwnie, jest jej więcej, bo infrastruktura też przynosi „kilku kandydatów" (kilka filtrów, kilka konwerterów, kilka handlerów itd.). Dlatego twoja umiejętność „czytania wiringu" i zarządzania nim — przenosi się bezpośrednio.
W ContextFlow wybór kanału powiadomień wygląda mniej więcej tak (w zależności od tego, jak sformułowałeś konfigurację i implementacje):
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.example.contextflow.domain.ports.NotificationSender;
@Service // Zwykły bean: różnica tylko w roli (serwis), nie w mechanice DI
public class NotificationDispatchService {
private final NotificationSender sender;
public NotificationDispatchService(
@Qualifier("emailNotificationSender") NotificationSender sender) {
// Qualifier sprawia, że wybór kandydata jest jawny: widoczne bezpośrednio w konstruktorze
this.sender = sender;
}
}
Ważne nie to, że tu akurat „email". Ważne, że rozpoznajesz wzorzec: kontener wybiera kandydata przez qualifier, i ta decyzja jest widoczna bezpośrednio w sygnaturze konstruktora. Gdy potem zobaczysz np. kilka DataSource, kilka SecurityFilterChain lub kilka serializerów, nie będziesz próbować „pokonać to zaklęciem". Po prostu zastosujesz znana logikę: albo robimy jeden bean @Primary, albo wybieramy jawnie przez qualifier, albo wstrzykujemy kolekcję i budujemy routing.
Przy okazji, właśnie dlatego tak stanowczo rugaliśmy service locator i statyczny dostęp do kontekstu. Jeśli w przyszłym projekcie zaczniesz robić „wybór implementacji podczas wykonania" przez applicationContext.getBean(...), rozmażesz wiring po kodzie biznesowym i stracisz kontrolę. Właściwa dyscyplina jest taka sama w każdym kierunku: wiring — to praca kontenera, a nie „funkcja serwisu biznesowego".
Niezmiennik nr 3: Environment, właściwości, profile
Na początku wydaje się, że właściwości i profile — to coś „z Boot", bo w Boot są pięknie zapakowane i wszędzie mignęły. Ale znasz już ważny sekret: Spring Core też żyje w środowisku, i Environment — to centralny sposób rozmawiania z tym środowiskiem. Boot po prostu robi to wygodniej i bogatszy „z pudełka", ale sam model się nie zmienia.
W ContextFlow czytałeś ustawienia takie jak contextflow.report.output-dir, contextflow.notifications.default-channel, contextflow.locale.default. To typowy przykład tego, jak aplikacja oddziela kod od środowiska.
import java.nio.file.Path;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component // Mała obwiezienie nad ustawieniami: też bean w kontekście
public class ReportOutputProperties {
private final Path outputDir;
public ReportOutputProperties(@Value("${contextflow.report.output-dir}") Path outputDir) {
// Spring sam weźmie właściwość z Environment i przekonwertuje ją na Path
this.outputDir = outputDir;
}
public Path outputDir() {
// Dalej w kodzie pracujesz z typem, a nie z „magicznym stringiem"
return outputDir;
}
}
Zauważ: to nie jest „specyfika ContextFlow". To ogólny wzorzec: konfiguracja przychodzi z zewnątrz, a wewnątrz aplikacji operujesz już typizowanymi wartościami (mieliście nawet warstwę konwersji, żeby odejść od ręcznego parsowania).
Teraz wyobraź sobie dowolny sąsiedni temat. W Spring Data pojawią się ustawienia połączenia z bazą (tu ich nie studiujemy, ale zasada ta sama). W Spring Security pojawią się ustawienia trybów bezpieczeństwa. W aplikacji REST pojawią się ustawienia serializacji i limitów. Wszędzie pozostanie jedno i to samo: właściwości nie powinny zamieniać się w „magiczne stringi po kodzie", profile nie powinny zamieniać się w „chaos flagów", a Environment pozostaje źródłem prawdy o bieżącym trybie uruchomienia.
Profile też pozostają te same. W twoim projekcie profil test sprawiał, że generator id był deterministyczny i wyłączał prawdziwe powiadomienia. Jest to bardzo podobne do tego, jak potem w aplikacji web profil test będzie włączać jedne beany, a prod — inne. Mechanika ta sama: część konfiguracji włącza się/wyłącza na etapie składania kontekstu.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import com.example.contextflow.domain.ports.OrderIdGenerator;
import com.example.contextflow.infrastructure.support.DeterministicOrderIdGenerator;
@Configuration // Klasa konfiguracyjna: miejsce, gdzie „składamy" graf beanów
public class TestOnlyConfig {
@Bean
@Profile("test") // Ten bean pojawi się tylko przy aktywnym profilu test
OrderIdGenerator orderIdGenerator() {
// Podmiana infrastruktury dla testów — typowy i przenoszalny trick
return new DeterministicOrderIdGenerator();
}
}
I tu pojawia się praktyczna „przenoszalna umiejętność": jeśli w przyszłości widzisz dziwne zachowanie aplikacji, pierwsze pytanie do siebie to nie „jaka adnotacja się zepsuła", lecz „jakie są aktywne profile, jakie property sources, jaki Environment?". To prawie zawsze oszczędza godziny życia.
Niezmiennik nr 4: modularna konfiguracja i kompozycja
Gdy projekt jest mały, pokusa jest jedna: zrobić jeden AppConfig na 500 linii, i „no normalnie, przecież wszystko widzimy w jednym miejscu". Gdy projekt rośnie, zamienia się to w konfiguracyjny monolit: nieporęczny do czytania, straszny do zmieniania, trudny do testowania.
Już rozwiązałeś ten problem na poziomie Spring Core przez modular config (@Import) i dyscyplinę pakietów domain/application/infrastructure/support/config. I jest to dokładnie to samo podejście, które będzie potrzebne w aplikacjach Boot. Co więcej, upraszczając do sedna, auto-configuration w Boot — to też modularna konfiguracja, tylko podłączana automatycznie i często warunkowo.
Twój finalny AppConfig (lub odpowiednik) wygląda jak kompozycja modułów:
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration // Composition root: punkt, gdzie opisane jest „składanie" aplikacji
@Import({
CoreConfig.class,
ProfilesConfig.class,
ReportingConfig.class,
LegacyBridgeConfig.class
// Każdy moduł przynosi swoją część grafu beanów (i tak łatwiej utrzymywać)
})
public class AppConfig {
}
To jest bardzo ważna „niezmienna" umiejętność: zawsze wygrywasz, gdy konfiguracja opisuje strukturę aplikacji, a nie po prostu „magazyn beanów".
Jeśli w przyszłości zobaczysz w projekcie Boot dziwną sytuację „dlaczego ten bean w ogóle się pojawił?", twój mózg będzie mógł przewinąć taśmę wstecz: beany pojawiają się z modułów konfiguracyjnych, moduły są włączane przez import/skanowanie/warunki, a warunki często są uzależnione od Environment. To znowu myślenie Spring Core, tylko w bardziej zautomatyzowanym opakowaniu.
Niezmiennik nr 5: zdarzenia aplikacji jako odsprzęganie
Zdarzenia łatwo pokochać zbyt mocno. Gdy tylko nauczyłeś się publishEvent() i @EventListener, chce się zrobić „wszystko na zdarzeniach": tworzenie zamówienia — zdarzenie, obliczenie ceny — zdarzenie, wydrukowanie raportu — zdarzenie, być może nawet „użytkownik się zamyślił" — też zdarzenie. I właśnie to jest droga do event soup, któryругailiśmy w drugim wykładzie.
Prawidłowa przenoszalna idea jest znacznie spokojniejsza: zdarzenia są dobre dla efektów ubocznych, gdy chcesz oddzielić główną orkiestrację (use case) od dodatkowych reakcji (audyt, powiadomienia, statystyki). I ta reguła pozostaje prawdziwa zarówno w aplikacjach web, jak i w serwisach data-oriented, i w środowisku Boot.
Oto ten sam minimalistyczny wzorzec, który już zrealizowałeś w ContextFlow:
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import com.example.contextflow.domain.events.OrderCreatedEvent;
@Service // Orkiestrator use case: serwis biznesowy
public class OrderPlacementService {
private final ApplicationEventPublisher publisher;
public OrderPlacementService(ApplicationEventPublisher publisher) {
// Publikowanie zdarzeń — infrastrukturalna zależność, ale wstrzykiwana tak samo przez DI
this.publisher = publisher;
}
public void place(String orderId) {
// Zdarzenie — sposób „oddania" efektów ubocznych na zewnątrz bez bezpośrednich zależności od listenerów
publisher.publishEvent(new OrderCreatedEvent(orderId));
}
}
I reakcja listenera, który robi efekt uboczny:
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import com.example.contextflow.domain.events.OrderCreatedEvent;
@Component // Listener — zwykły bean: kontener sam go znajdzie i podłączy
public class AuditOrderEventsListener {
@EventListener // Synchroniczna reakcja domyślnie: wykona się w tym samym wątku
public void onOrderCreated(OrderCreatedEvent event) {
// Przykład efektu ubocznego: audyt (w prawdziwym kodzie tu będzie serwis/repozytorium audytu)
System.out.println("AUDIT " + event.orderId()); // AUDIT <id>
}
}
W sąsiednich kierunkach będziesz widywać zdarzenia w różnych formach. Gdzieś będą zdarzenia „domenowe", gdzieś „infrastrukturalne" (np. zdarzenia startu aplikacji). Ale twój stabilny punkt oparcia pozostaje: zdarzenie — to wewnątrzprocesowe odsprzęganie, jest synchroniczne domyślnie, jest częścią ApplicationContext, i nie należy używać go jako zamiennika jawnej orkiestracji biznesowej.
Niezmiennik nr 6: model mentalny proxy/target
Gdybym musiał wybrać jeden temat Spring Core, który najbardziej „opłaca się" w przyszłych kursach, wybrałbym model proxy. Bo wyjaśnia on połowę tajemniczych pytań w stylu „dlaczego adnotacja nie zadziałała?", „dlaczego metoda nie została przechwycona?", „dlaczego klasa nagle stała się inną klasą?".
Już widziałeś aspekt oparty na proxy (mierzenie czasu serwisów) i omawiałeś self-invocation. Ważne nie konkretne mierzenie czasu, lecz sama idea: kontener czasem oddaje ci nie oryginalny obiekt, lecz wrapper proxy, który dodaje zachowanie przed/po wywołaniu metody.
Oto twój znany przykład, bardzo krótki:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect // Aspekt: zostanie zastosowany przez infrastrukturę AOP Spring
@Component // Sam aspekt też jest beanem, który kontener stworzy i zarejestruje
public class ServiceTimingAspect {
@Around("execution(* com.example.contextflow.application.service..*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// W prawdziwym kodzie zazwyczaj: zaznacz czas -> wywołaj target -> zaloguj czas
return pjp.proceed(); // Wywołanie prawdziwej metody (target) następuje przez join point
}
}
Teraz ważne „przenoszalne" rozumienie, bez zagłębiania się w nowe API: w sąsiednich kursach napotkasz mechanizmy, które też działają przez proxy. Transakcje (nawet jeśli jeszcze nie studiowałeś ich API), bezpieczeństwo metod, część wywołań asynchronicznych, część cachowania, część „magii repozytoriów" — wszystko to w takiej czy innej formie opiera się na tym, że kontener owija wywołania.
Dlatego twoja praktyczna umiejętność na przyszłość brzmi tak: gdy coś „nie zadziałało", nie zaczynaj od mistyki. Zacznij od prostych pytań z Spring Core: „a to w ogóle proxy czy target?", „wywołanie idzie przez referencję kontenerową czy jest to self-call wewnątrz klasy?", „metoda/klasa nie jest przypadkiem final?", „zależność jest wstrzyknięta przez konstruktor czy gdzieś ukryta jest obejście?".
Tak, brzmi to jak paranoja. Ale to dobra inżynierska paranoja, która oszczędza czas. W świecie Spring lepiej być człowiekiem z pytaniami niż człowiekiem z wiarą.
Niezmiennik nr 7: testy kontekstowe weryfikują składanie
Pod koniec kursu nauczyłeś się nie wstydzić testów, które po prostu podnoszą kontekst i weryfikują, że wiring żyje, profile działają, właściwości są odczytywane, zdarzenia są słuchane, a wspierające mechanizmy (post-procesory, FactoryBean) nie psują startu. Nazywa się to jak się chce („smoke tests", „context tests"), ale sens jest jeden: testujemy nie biznes-matematykę, lecz rzeczywistość składania kontenerowego.
Takie testy nie znikają w projektach Boot/REST/Data. Zmieniają się adnotacje, zmienia się infrastruktura, ale idea pozostaje: podnieść kontekst w kontrolowanym środowisku i upewnić się, że aplikacja przynajmniej składa się tak, jak oczekujesz.
Twój znany minimalizm z pure Spring wygląda tak:
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(AppConfig.class) // Podnosimy kontekst z composition root
@ActiveProfiles("test") // Uruchamiamy w profilu testowym, żeby działały beany test-only
class ContextSmokeTest {
@Test
void contextStarts() {
// Jeśli kontekst nie startuje — test padnie przed jakąkolwiek weryfikacją,
// i to właśnie tego potrzebujemy od smoke-testu.
}
}
Jeśli potem w projektach Boot zobaczysz inne wygodne adnotacje, nie traktuj tego jako „nowy sposób testowania". To po prostu inny launcher i bogatsze oprzyrządowanie testowe. Sens pozostaje ten sam: sprawdzić, że kontener zdołał złożyć aplikację.
I tu znowu wypływa twoja najbardziej przydatna umiejętność: umiejętność czytania błędów startu. W testach jest to nawet przyjemniejsze niż w runtime: pada — znaczy dowiedziałeś się o tym wcześniej niż użytkownik.
2. Tabela-ściągawka: fasada → mechanizm Core
Przed przejściem do typowych błędów przydatne jest trzymanie w głowie małej mapy odpowiedniości. To nie lista adnotacji „na pamięć", lecz translator „nowa fasada → stara mechanika".
Jeśli przy napotkaniu nowej technologii potrafisz zrobić taki przekład, nie zaczynasz od zera — po prostu rozpoznajesz znany mechanizm kontenera.
| W sąsiednim kierunku zobaczysz | Wewnątrz prawie zawsze działa |
|---|---|
| „Kontrolery", „filtry", „konwertery", „konfigi" | Zwykłe beany + DI + lifecycle |
| „Automatycznie stworzony repository/klient" | Rejestracja definicji beana przez infrastrukturę (często FactoryBean + post-procesory) |
| „Adnotacja, która dodaje zachowanie metodzie" | Proxy + przechwytywanie AOP (i ograniczenia modelu proxy) |
| „Aplikacja zachowuje się inaczej w dev/test/prod" | Environment + właściwości + profile + warunki |
| „Dziwny bean pojawił się sam" | Modularna konfiguracja (w tym automatyczne podłączanie modułów) |
| „Trzeba sprawdzić, że wszystko się składa" | Context smoke test / integracyjny test kontekstowy |
Ta tabela jest dobra właśnie jako środek uspokajający: nie jesteś w nieznanym kraju, po prostu widzisz nowe szyldy na tych samych ulicach.
3. Typowe błędy przy pracy ze Spring Core
Błąd nr 1: myśleć, że w Boot „kontener jest inny", więc Spring Core można zapomnieć.
To bardzo popularna pułapka: Boot startuje jedną linią, startery podciągają masę zależności, i wydaje się, że żyjesz w osobnej rzeczywistości. W praktyce ApplicationContext nigdzie się nie znika, beany nie przestają być beanami, a DI nie przestaje być DI. Jeśli zapominasz Core, po prostu tracisz zdolność wyjaśniania sobie tego, co się dzieje.
Błąd nr 2: widzieć tylko nowe adnotacje i nie widzieć znajomego mechanizmu kontenera.
W sąsiednich tematach będzie wiele nowych słów: gdzieś @Rest..., gdzieś @Transactional, gdzieś konfiguracyjne DSL. Jeśli mózg zaczyna panikować, przydatne jest zrobienie pauzy i przetłumaczenie tego na język kursu: „to jest rejestracja beana?", „to jest wybór kandydata?", „to jest konfiguracja przez Environment?", „to jest proxy?". Zazwyczaj po takim „przekładzie" staje się zauważalnie łatwiej.
Błąd nr 3: wracanie do myślenia service-locator, gdy robi się trudno.
Gdy tylko projekt się komplikuje, u niektórych pojawia się pokusa „szybkiej naprawy" przez ApplicationContext.getBean(...) bezpośrednio z klasy biznesowej. To wydaje się wygodne, dopóki nie zacznie się chaos. Wiesz już, jak to pachnie: ukryte zależności, nieprzewidywalny wiring, skomplikowane testy i wyciek frameworka do logiki domenowej. W każdej warstwie aplikacji zasada jest jedna: zależności są deklarowane, a nie szukane.
Błąd nr 4: ignorowanie modelu proxy i dziwienie się, dlaczego „adnotacja nie zadziałała".
Prawie każdy, kto potem zacznie pracować z transakcjami lub bezpieczeństwem, trafi przynajmniej raz na sytuację „dlaczego przechwytywanie nie zadziałało?". I bardzo często odpowiedź będzie ta sama co przy self-invocation: wywołanie przeszło obok proxy. Jeśli nie trzymasz w głowie proxy/target, będziesz leczyć symptomy (dodawać adnotacje), a nie przyczynę (przemyśleć punkty wywołania i granice beanów).
Błąd nr 5: zamienianie zdarzeń w główną orkiestrację („event soup"), bo tak „ładnie jest odsprzęgnięte".
Zdarzenia naprawdę dają przyjemne odsprzęganie, ale jeśli główny scenariusz przestaje czytać się bez przejrzenia tuzina listenerów, tracisz kontrolę nad systemem. W każdym kierunku Spring zdarzenia powinny wzmacniać zrozumiałość (wynosić efekty uboczne), a nie zastępować bezpośrednią, czytelną orkiestrację use case.
GO TO FULL VERSION