CodeGym /Kursy /Spring Core /Końcowe składanie ContextFlow

Końcowe składanie ContextFlow

Spring Core
Poziom 25, Lekcja 0
Dostępny

1. Wprowadzenie

Gdy aplikacja startuje — to jest miłe. Ale projekty Spring mają podstępny efekt uboczny: często startują nawet wtedy, gdy nie do końca rozumiesz, dlaczego wszystko działa. W efekcie w głowie pozostaje „magia": adnotacje, jakieś konfigi, gdzieś zdarzenia, gdzieś aspekt… i wszystko to jakby nie o jednym systemie, lecz o różnych kawałkach demo. Końcowe składanie jest po to, żeby w głowie ułożył się spójny model mentalny: jedna ścieżka, jedna logika, jeden ApplicationContext.

W tym wykładzie będziemy trzymać się prostej osi: main() → tworzenie kontekstu → rejestracja konfiguracji → odczytanie środowiska/profili/właściwości → tworzenie beanów → uruchomienie scenariusza. Wszystko inne (zasoby, komunikaty, zdarzenia, post-processory, AOP, gałąź XML, testy) nie będziemy wymieniać, lecz starannie „wkładać" w tę ścieżkę jak szczegóły do wspólnego mechanizmu.

Żeby się nie zgubić, oto zwięzły schemat „co z czym jest związane":

flowchart TD
  A["main()"] --> B["ApplicationContext"]
  B --> C["Moduły config @Import"]
  B --> D["Environment: profile + właściwości"]
  B --> E["Bean definitions -> beany"]
  E --> F["ScenarioRunner.run()"]
  F --> G["Serwisy use-case"]
  G --> H["publishEvent()"]
  H --> I["@EventListener efekty uboczne"]
  E --> J["Support: BFPP/BPP/FactoryBean/AOP"]
  C --> K["Most legacy XML"]
  B --> L["Kontekst Spring Test"]

Sens diagramu nie polega na tym, żeby nauczyć się strzałek. Sens w tym, że to jest jeden system, gdzie każdy mechanizm ma „punkt wejścia" we wspólny przepływ.

2. Punkt wejścia: main() i ApplicationContext

Normalny start aplikacji — to ten moment, gdy powinieneś widzieć, kto tu jest głównym. I w naszym kursie głównym nie jest „adnotacja dnia", lecz ApplicationContext. W Spring Boot często wydaje się, że aplikacja startuje „sama" (bo SpringApplication.run() robi dużo pracy za ciebie). Ale dziś świadomie pozostajemy w czystym Spring Core: ręcznie tworzysz kontekst, wybierasz profile, rejestrujesz konfigurację i wywołujesz refresh(). To nie jest „zbędny rytuał", to edukacyjne prześwietlenie rentgenem.

Minimalny, uczciwy entry-point finalnej wersji ContextFlow może wyglądać tak:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowMain {

    public static void main(String[] args) {
        // Kontekst lepiej zamykać jawnie: inaczej @PreDestroy/destroyMethod mogą nie zadziałać.
        try (var context = new AnnotationConfigApplicationContext()) {

            // Profil jest ważny PRZED refresh(): wpływa na to, jakie beany w ogóle zostaną zarejestrowane.
            context.getEnvironment().setActiveProfiles("demo");

            // Rejestrujemy główną konfigurację aplikacji (composition root).
            context.register(AppConfig.class);

            // Uruchamiamy fazę składania: odczyt konfigów, tworzenie bean definitions, instancjowanie beanów itp.
            context.refresh();

            // Uruchomienie scenariusza — to jest granica startu, tu dopuszczalne jest ręczne getBean().
            context.getBean(ScenarioRunner.class).run();
        }
    }
}

Zwróć uwagę na dwie rzeczy.

Pierwsza — profil ustawiany jest przed refresh(). To ważne, bo profile wpływają na to, jakie beany w ogóle zostaną zarejestrowane i stworzone. Jeśli ustawić profil po refresh(), dostaniesz klasyczny efekt „przełączyłem tryb, ale nic się nie zmieniło" (i zaczniesz podejrzewać spisek, choć winny będziesz ty).

Druga — try-with-resources. Zamknięcie kontekstu — to nie „ceremonia". To przyczyna, dla której @PreDestroy i destroyMethod naprawdę działają, a zasoby (pliki, strumienie, menedżery wyjścia) zamykają się poprawnie. Spring nie może zamknąć kontekstu „siłą myśli", jeśli go nie zamykasz.

I tak: widzisz tu „ręczny getBean()", ale jest to tylko na granicy startu, jak robiłeś wcześniej z composition root w plain Java. Warstwa biznesowa nie powinna zamieniać się w „profesora getBean".

3. AppConfig: modularna konfiguracja zamiast giant-kupy

Gdy projekt jest mały, jeden klasa konfiguracyjna wydaje się normalna. Ale gdy tylko dodajesz profile, zasoby, komunikaty, raportowanie, gałąź legacy XML i warstwę support, „jeden AppConfig na wszystko" zamienia się w tekstową wersję garażu, gdzie przechowujesz rower, lodówkę i stare dyplomy. Modularna konfiguracja nie jest dla urody, lecz dla czytelności: otwierasz AppConfig i widzisz, z jakich części składa się aplikacja.

W końcowym składaniu wygodnie, gdy główny konfig jest prawie pusty — po prostu „składa moduły":

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

@Configuration
@Import({
        // Baza aplikacji: serwisy domenowe/porty/repozytoria itp.
        CoreConfig.class,

        // Profile (dev/demo/test) i zmienność zachowania.
        ProfilesConfig.class,

        // Raporty, formatowanie i wyjście.
        ReportingConfig.class,

        // Most do fragmentu legacy XML.
        LegacyBridgeConfig.class,

        // Infrastrukturalne rozszerzenia Spring: BFPP/BPP/FactoryBean/AOP i diagnostyka.
        SupportConfig.class
})
public class AppConfig {
}

Teraz ważna myśl: @Import — to nie „kolejny sposób rejestracji". To sposób uczynienia wiringu czytelnym. Twoje moduły — to nie „warstwy architektury dla diagramu", lecz praktyczna nawigacja po projekcie.

Na przykład CoreConfig odpowiada za bazowe składanie portów domenowych i serwisów, ProfilesConfig — za zmienność trybu (dev/demo/test), ReportingConfig — za raporty i wyjście, LegacyBridgeConfig — za podłączenie fragmentu XML, a SupportConfig — za wszystko związane z post-procesorami, factory beanami, diagnostyką i AOP.

Jeśli chcesz sprawdzić, czy modularność działa, zadaj sobie pytanie: „Jeśli jutro popsuje się audyt do plików, w którym module to szukam?" W dobrej modularnej konfiguracji odpowiedź będzie niemal automatyczna.

4. Środowisko: profile, właściwości i konwersja

W Spring wygodne jest to, że logika biznesowa może pozostawać „czysta", a zachowanie aplikacji zmienia się przez konfigurację. Ale ma to warunek: konfiguracja musi być podłączona wcześniej, niż uruchomisz scenariusz. Czyli twój ApplicationContext najpierw buduje środowisko i tworzy potrzebne beany, a potem ty wykonujesz use case. To przypomina przygotowanie kuchni (wyciągnięcie składników, włączenie kuchenki) zanim zaczniesz smażyć jajecznicę, a nie próbowanie włączenia kuchenki w trakcie smażenia.

W ContextFlow trzymamy się prostego modelu: jest bazowy contextflow.properties, a także warianty profile-specific, np. contextflow-demo.properties i contextflow-test.properties. Ważne, żeby nie mylić z konwencjami Boot: w czystym Spring takie profile-specific źródła podłączane są jawnie przez @PropertySource lub programową rejestrację, samemu dopasowaniu nazwy do profilu kontener nie wystarczy. A następnie czytasz je przez @Value lub Environment.

Powiedzmy, że chcemy zarządzać formatem raportu i katalogiem wyjścia:

# contextflow.properties
contextflow.report.format=text
contextflow.report.output-dir=build/contextflow/reports

A następnie w konfiguracji „przyziemić" to do infrastruktury (uproszczone):

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ReportingConfig {

    @Bean
    ReportOutputManager reportOutputManager(
            // Wartość zostanie podstawiona przez kontener przy tworzeniu beana.
            @Value("${contextflow.report.output-dir}") String outputDir) {

        // Tu świadomie trzymamy tylko infrastrukturę (gdzie pisać), nie logikę biznesową.
        return new ReportOutputManager(outputDir);
    }
}

Tu ważny moment dla modelu mentalnego: @Value — to nie „magia stringów". To część mechanizmu, który kontener robi przy starcie, zanim uruchomisz scenariusz. Jeśli wartości brakuje, dowiesz się o tym albo przy starcie, albo w momencie tworzenia konkretnego beana (w zależności od leniwości/chciwości tworzenia). To właśnie podejście fail-fast: lepiej paść od razu niż „w połowie wykonywania zamówienia".

A gdzie tu konwersja? W finalnej wersji nie chcesz pisać if ("text".equals(format)) ... w serwisie biznesowym. Dlatego format raportu staje się np. enumem, a string z właściwości zmienia się w typ przez ConversionService i twój konwerter. Logika ta sama: wszystko to jest przygotowywane przez kontener przed wykonaniem scenariusza.

5. Resource i MessageSource

Gdy po raz pierwszy słyszysz „zasoby" i „komunikaty", pojawia się pokusa myślenia: „no to chyba o UI i web". Ale w aplikacji non-web jest to jeszcze prostsze i uczciwsze: szablony powiadomień i nagłówki raportów — to zewnętrzne artefakty, które powinny żyć obok kodu, być wersjonowane i ładowane przewidywalnie. A zlokalizowane teksty — to sposób na nie wszywanie stringów w kod Java, zwłaszcza gdy są używane i w powiadomieniach, i w raportach, i w wydruku konsolowym.

Wyobraź sobie, że masz szablon powiadomienia o stworzeniu zamówienia:

src/main/resources/templates/notifications/order-created.txt

I katalog szablonów, który jest ładowany przy starcie (inicjalizacja „żyje" w lifecycle, a nie w pierwszym wywołaniu metody biznesowej). Bardzo uproszczony przykład:

import jakarta.annotation.PostConstruct;
import org.springframework.core.io.Resource;

public class NotificationTemplateCatalog {

    // Resource — to nie "java.io.File": to abstrakcja Spring, która potrafi pracować z classpath, URL itd.
    private final Resource orderCreatedTemplate;

    public NotificationTemplateCatalog(Resource orderCreatedTemplate) {
        // Zależność przychodzi z kontenera tak samo jak każda inna.
        this.orderCreatedTemplate = orderCreatedTemplate;
    }

    @PostConstruct
    void init() {
        // Ta faza jest wywoływana przez kontener po stworzeniu beana: wygodnie robić szybkie sprawdzenia i przygotowanie.
        System.out.println("Templates ready: " + orderCreatedTemplate.exists()); // Templates ready: true
    }
}

Ważne nie to, że drukujemy true. Ważne, że widzisz miejsce mechanizmu w ogólnym obrazku: zasób — to nie „zwykły plik", to zależność, którą kontener potrafi dać beanowi, a bean potrafi sprawdzić i przygotować w fazie init.

Z MessageSource historia jest podobna. Podłączasz bundle messages.properties, messages_pl.properties, messages_en.properties, a dalej pobierasz komunikat po kluczu i locale. I to znowu część wspólnego składania: kontener dostarcza MessageSource jako infrastrukturę, a warstwa biznesowa/infrastrukturalna używa jej do tekstu.

6. Use case: scenariusz → serwis → event, a efekty uboczne — w listenerach

Gdy projekt rośnie, najbardziej nieprzyjemna degradacja — gdy „główny" serwis use-case zamienia się w kombajn: i zapisuje zamówienie, i oblicza zniżkę, i pisze audyt, i wysyła powiadomienia, i aktualizuje statystyki, i gdzieś w kącie tworzy raport „na wszelki wypadek". Taki serwis zazwyczaj działa, ale czytać go ciężko: każda zmiana zamienia się w mini-operację na otwartym sercu.

Zdarzenia w naszym projekcie rozwiązują dokładnie to: use case robi główne działanie i publikuje fakt, a efekty uboczne wykonują osobne listenery.

Mini-wersja OrderPlacementService:

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class OrderPlacementService {

    private final ApplicationEventPublisher publisher;

    public OrderPlacementService(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void place(String orderId) {
        // Serwis robi „główne": publikuje fakt biznesowy, a nie ciągnie w sobie audytu/powiadomień/statystyk.
        publisher.publishEvent(new OrderCreatedEvent(orderId));

        // Ważne do pamiętania: domyślnie to jest synchroniczne (listenery wykonują się w bieżącym wątku).
    }
}

Audyt jako listener (uproszczone):

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class AuditOrderEventsListener {

    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // Efekt uboczny żyje osobno: use case nie wie, że ktoś go audytuje.
        System.out.println("AUDIT orderId=" + event.orderId()); // AUDIT orderId=...
    }
}

W tym miejscu warto przypomnieć sobie model mentalny z poprzednich dni: zdarzenia są domyślnie synchroniczne. Czyli publishEvent() nie „wkłada do kolejki", lecz wywołuje listenery bezpośrednio w bieżącym wątku. Oznacza to, że use case „zakończył się" dopiero wtedy, gdy zakończyli wszyscy obsługujący. I to normalne dla naszej edukacyjnej aplikacji non-web.

Z punktu widzenia końcowego składania ważne jest coś innego: serwis use-case nie wie, kto słucha na zdarzenie. Wie tylko, że publikuje fakt „zamówienie zostało stworzone". To właśnie miękkie odsprzężenie, które sprawia, że system jest mniej związany.

7. Warstwa support: BFPP, BPP, FactoryBean i AOP

Wewnętrzne mechanizmy Spring łatwo zamienić w zły nawyk: chce się wszędzie dodać Aware, gdzieś zrobić fabrykę, gdzieś „podkręcić" post-procesor, i nagle kod biznesowy zaczyna przypominać dokumentację Springa, a nie logikę biznesową. Dlatego w naszym projekcie istnieje osobna warstwa support.*: tam żyją post-procesory, FactoryBean, diagnostyczne beany Aware i AOP. To nie „kolejna warstwa dla warstw", to strefa sanitarna: specyfika Spring jest izolowana.

Zacznijmy od dwóch extension points, które wpływają na start.

BeanFactoryPostProcessor żyje w fazie „metadane jeszcze nie zamieniły się w obiekty". To idealne miejsce na sanity check: sprawdzić, że kluczowe właściwości istnieją, że nie ma krytycznych sprzeczności, że obowiązkowe bean definitions są obecne. Warunkowy przykład (silnie uproszczony, żeby się nie utopić w API):

import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

public class ContextFlowSanityCheckBeanFactoryPostProcessor
        implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
        // Ten punkt wykonuje się PRZED tworzeniem beanów: pracujemy z bean definitions, a nie z obiektami.
        System.out.println("Sanity check: beanCount=" + bf.getBeanDefinitionCount());
    }
}

BeanPostProcessor pracuje już z obiektami (przed/po init). Tam wygodnie robić diagnostykę, „śledzenie" komponentów, lekkie opakowanie. Na przykład logować fakt inicjalizacji określonych beanów, nie zaglądając do każdej klasy. To właśnie „infrastrukturalna magia", która nie psuje warstwy biznesowej.

Dalej FactoryBean. Używamy go dokładnie raz i tylko tam, gdzie naprawdę wyjaśnia kontener: ReportFormatterFactoryBean wybiera implementację formatera według konfiguracji. Ważne, że „formatter" — to produkt fabryki, a sama fabryka też jest beanem. I to można zobaczyć, jeśli pamiętasz trick &beanName, który daje dostęp do samej fabryki.

Potem diagnostyczny bean Aware. Może on dostać nazwę, Environment, kontekst — ale tylko jako izolowany komponent, żeby serwisy biznesowe nie zaczęły ręcznie szukać beanów. Jeśli serwis biznesowy dostaje ApplicationContext, to jest prawie zawsze smell: przeniosłeś się z DI na service locator.

I wreszcie AOP. Aspekt ServiceTimingAspect dodaje techniczne zachowanie wokół metod warstwy serwisowej (np. pomiar czasu). I tu końcowe składanie pomaga szczególnie: zaczynasz widzieć, że aspekt — to nie „magia kompilatora", lecz wynik modelu proxy-based, zbudowanego przez kontener przez mechanizmy infrastrukturalne.

8. Legacy XML: izolowany most

XML w nowoczesnych projektach — to nie norma, ale rzeczywistość, którą spotykasz w legacy. I najważniejsze, co powinieneś wynieść z tej części kursu: XML — to ten sam Spring, tylko inna składnia opisu modelu beanów. To nie „osobna technologia", i tym bardziej nie „ciemna magia". Jeśli rozumiesz bean definitions, zakresy, wiring i lifecycle, to XML czyta się niemal jak „adnotacje, tylko tekstem".

W finalnej wersji ContextFlow fragment XML podłączany jest jako most, np. przez @ImportResource w osobnym module:

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

@Configuration
// Most do legacy: osobny moduł, żeby XML nie „rozmywał się" po całym projekcie.
@ImportResource("classpath:legacy/legacy-notification-context.xml")
public class LegacyBridgeConfig {
}

A wewnątrz XML widzisz znajome rzeczy — bean id, class i ewentualnie zależności:

<!-- Zwykła definicja beana: ten sam model beana, po prostu w innej składni. -->
<bean id="consoleAuditWriter"
      class="com.example.contextflow.infrastructure.audit.ConsoleAuditWriter"/>

Fika końcowego składania polega na tym, że gałąź XML nie rozmywa projektu: jest izolowana. Nie zamieniasz aplikacji w XML-first, po prostu potrafisz żyć z kawałkiem legacy, dopóki go nie zmigrowałeś.

9. Testy: weryfikujemy składanie, a nie „wierzymy w nie"

Testowanie konfiguracji Spring — to nie „pokryć wszystko testami", lecz przestać bać się, że przypadkowo zepsułeś wiring, profile, nadpisanie właściwości, listenery lub legacy-bridge. Idealnie testy powinny używać tego samego AppConfig, co prawdziwy start, inaczej testujesz „inny wszechświat" i dziwisz się, dlaczego na prodzie jest inaczej.

Minimalny smoke test może być bardzo mały — czasem wystarczy, że kontekst wstaje:

import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(AppConfig.class)
@ActiveProfiles("test")
class ContextSmokeTest {

    @Test
    void contextStarts() {
        // jeśli kontekst nie startuje — test padnie sam (przy podnoszeniu TestContext).
    }
}

Wygląda to prawie jak „pusty test", ale sprawdza bardzo ważne rzeczy: wiring, profile, property sources, konwersję, rejestrację listenerów, wsparcie dla legacy XML, wsparcie dla AOP i obecność twoich support beanów. Czyli sprawdza właśnie to, czego nie można uczciwie sprawdzić zwykłymi testami jednostkowymi.

I znowu zwróć uwagę na wspólną linię: test — to po prostu inny punkt wejścia do tego samego ApplicationContext. W finalnym modelu mentalnym „uruchomienie aplikacji" i „uruchomienie kontekstu w teście" — to nie różne światy, to ten sam mechanizm w różnych trybach.

10. Typowe błędy końcowego składania

Błąd nr 1: wyjaśniać projekt przez listę adnotacji.
Gdy finał sprowadza się do „mamy @Configuration, @Profile, @EventListener i @Aspect", u studenta znowu pojawia się poczucie magii. Adnotacje zaczynają żyć samodzielnie, bez związku z wykonaniem programu. Ważne jest trzymanie fokusu na przepływie: main → ApplicationContext → tworzenie beanów → uruchomienie scenariusza. Adnotacje — to tylko sposób opisania tej ścieżki, a nie jej zamiennik.

Błąd nr 2: gubić ogólny kontur przez drobne szczegóły.
Łatwo ugrzęznąć w dyskusji „gdzie leży szablon" lub „jak ładuje się zasób", zapominając, że to szczególny przypadek. Jeśli niejasny jest ogólny mechanizm składania, szczegóły nie ratują — tylko przeciążają. Najpierw musi być jasny cały obraz: jak składa się kontener, jak powiązane są beany, jak uruchamia się scenariusz.

Błąd nr 3: mieszać orkiestrację i efekty uboczne.
Gdy serwis use-case zaczyna zarówno zarządzać scenariuszem, jak i pisać do logu, i wysyłać zdarzenia, i robić audyt — rozrasta się i traci czytelność. Zdarzenia stają się formalnością, a listenery — duplikacją. Właściwa struktura: serwis opisuje scenariusz, a efekty uboczne wyniesione są do osobnych handlerów.

Błąd nr 4: traktować XML jako „inny Spring".
XML często postrzegany jest jako coś obcego i przestarzałego, przez co zaczyna się go unikać lub bać. W rzeczywistości to ten sam kontener, tylko opisany inną składnią. Jeśli przetłumaczyć XML na znany model „bean → id → class → zależności", staje się zrozumiały i przewidywalny.

Błąd nr 5: zapominać o punkcie wejścia i scenariuszu uruchomienia.
W końcowym składaniu ważne nie tylko „co jest", ale i „co się dzieje przy uruchomieniu". Jeśli nie można szybko wyjaśnić, który bean inicjuje scenariusz i jak jest wywoływany z main(), znaczy obraz jest nadal rozmyty. Klarowny punkt wejścia — to kotwica, która utrzymuje całą architekturę.

1
Zadanie
Spring Core, poziom 25, lekcja 0
Niedostępne
Modułowe uruchamianie aplikacji z zdarzeniem
Modułowe uruchamianie aplikacji z zdarzeniem
1
Zadanie
Spring Core, poziom 25, lekcja 0
Niedostępne
Ladowanie szablonu z Resource i smoke test
Ladowanie szablonu z Resource i smoke test
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION