CodeGym /Kursy /Spring Core /Interfejsy Aware w Springu

Interfejsy Aware w Springu

Spring Core
Poziom 20, Lekcja 2
Dostępny

1. Kiedy bean potrzebuje kontekstu kontenera

Czasem chce się, żeby klasa była „czystym" POJO i w ogóle nie podejrzewała o Springu. I to prawidłowe pragnienie — zwłaszcza dla warstwy biznesowej. Ale w prawdziwej aplikacji czasem pojawiają się zadania techniczne: wydrukować diagnostykę startu, sprawdzić aktywne profile, zrozumieć, jak kontener zarejestrował bean, upewnić się, że jakiś zasób jest dostępny. I tu zaczyna się ostrożna rozmowa: jak dać beanowi kontekst, nie zamieniając aplikacji w „serwis zorientowany na Spring-lokalizator".

Wyobraź sobie nasz ContextFlow. Mamy użyteczną, ale czysto techniczną potrzebę: przy starcie aplikacji chcemy starannie wydrukować, który profil jest aktywny (np. dev/demo/test), jak nazywa się aplikacja (contextflow.app-name) i gdzie leży katalog raportów. To nie logika biznesowa i nie audyt zamówień, lecz „puls przy wejściu": diagnostyka, żeby student (i przyszły ty) rozumiał, co teraz jest złożone.

Można by rozwiązywać to bezpośrednio przez @Value w konkretnych miejscach. Można by przekazać Environment do jakiegoś diagnostycznego serwisu. A można skorzystać ze specjalnie zaprojektowanego mechanizmu Spring: callbacków Aware, gdy kontener sam przychodzi do beana i mówi: „trzymaj dane o swoim otoczeniu".

Ważne uchwycić subtelną myśl: Aware nie jest potrzebne po to, żeby „dostać dowolną zależność". Jest potrzebne, gdy piszesz infrastrukturalny bean, któremu naprawdę przydatne jest wiedzieć coś o kontenerze, a nie o domenie zamówień.

2. Czym jest Aware i dlaczego to nie „kolejny rodzaj DI"

Tu łatwo wyciągnąć błędny wniosek: skoro jest ApplicationContextAware, znaczy można przestać myśleć o zależnościach i wyciągać wszystko z kontenera na miejscu. Nie — to ten sam service locator, tylko pod nową nazwą. Aware nie jest do tego.

Interfejs Aware — to kontenerowy kontrakt callback. Czyli bean jawnie deklaruje kontenerowi: „potrzebuję informacji serwisowej". Spring widzi taki interfejs i we właściwym momencie cyklu życia wywołuje specjalną metodę — na przykład setBeanName(...) lub setEnvironment(...).

Kluczowa różnica od zwykłego DI: przy DI prosimy o zależności, które uczestniczą w biznesowej pracy klasy, i ustalamy je w konstruktorze. Przy Aware dostajemy serwisowe dane o kontenerze, przy czym często są to dane, które nie są „prawdziwymi beanami" w zwykłym sensie (na przykład nazwa beana).

Ważne są dla nas cztery interfejsy:

Interfejs Aware Co daje Czym to jest w praktyce
BeanNameAware nazwę beana w kontenerze diagnostyka, wyjście, czasem — rejestr/routing po nazwie
BeanFactoryAware referencję do BeanFactory minimalny dostęp do kontenera (ostrożnie)
ApplicationContextAware referencję do ApplicationContext „rozszerzony kontener": zasoby, zdarzenia, komunikaty itp.
EnvironmentAware referencję do Environment properties i profile jako techniczny kontekst

I jeszcze jeden ważny inżynierski szczegół: większość wywołań Aware dzieje się nie „magicznie", lecz dzięki infrastrukturalnym mechanizmom Spring (często przez wewnętrzny BeanPostProcessor). Czyli Aware to kolejny przykład tego, jak kontener rozszerza zachowanie obiektów przez swój pipeline.

3. BeanNameAware: nazwa beana

W Springu każdy bean ma nazwę. Czasem ustawiasz ją jawnie, czasem jest generowana według reguł (zazwyczaj camelCase od nazwy klasy). W większości przypadków biznesowych ta nazwa nie jest potrzebna, bo zależności są rozwiązywane po typie i życie jest łatwiejsze. Ale dla zadań infrastrukturalnych nazwa bywa przydatna: w diagnostykach, w logice „katalogu strategii", w debugowaniu problemów startowych, a czasem po prostu żeby wydrukować „kim jestem i skąd jestem" przy starcie.

BeanNameAware to najbardziej bezpieczny z interfejsów Aware: nie daje dostępu do kontenera, nie pozwala „wyciągać" beanów, a po prostu informuje obiekt, jak jest zarejestrowany. To jak plakietka na konferencji: nie daje supermocy, ale pomaga rozumieć, kto jest przed tobą, zwłaszcza gdy w projekcie są setki uczestników.

Minimalny przykład dla naszego projektu można umieścić w support.diagnostics. Sens: bean przechowuje swoją nazwę i możemy ją w dowolnym momencie wydrukować (do diagnostyki lub testu).

import org.springframework.beans.factory.BeanNameAware;
import org.springframework.stereotype.Component;

@Component
public class BeanNamePrinter implements BeanNameAware {

    // Tu będziemy przechowywać nazwę, pod którą Spring zarejestrował ten bean.
    private String beanName;

    @Override
    public void setBeanName(String name) {
        // Tę metodę wywołuje sam kontener Spring podczas inicjalizacji beana.
        this.beanName = name;
    }

    public String beanName() {
        // Wygodna metoda do diagnostyki (np. wydrukować lub sprawdzić w teście).
        return beanName;
    }
}

Teraz wyobraźmy sobie, że startujemy kontekst (jak robiliśmy wcześniej) i chcemy zobaczyć, co Spring uważa za nazwę tego beana:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var ctx = new AnnotationConfigApplicationContext("com.example.contextflow")) {
    // Ważne: kontekst skanuje pakiet i podnosi @Component, w tym BeanNamePrinter.
    BeanNamePrinter printer = ctx.getBean(BeanNamePrinter.class);

    // Domyślnie zobaczymy "beanNamePrinter" (camelCase od nazwy klasy).
    System.out.println(printer.beanName()); // beanNamePrinter
}

Zwróć uwagę: beanNamePrinter — to nie przypadkowe. Domyślnie Spring bierze nazwę klasy BeanNamePrinter, robi pierwszą literę małą — i dostaje nazwę beana. Gdybyśmy zarejestrowali bean z jawną nazwą (na przykład @Component("diagnosticsPrinter")), to w setBeanName(...) przyszłaby już ta nazwa.

I tu nowicjusze często popełniają zabawny błąd: oczekują, że beanName będzie równe BeanNamePrinter (jak nazwa klasy). Spring, w przeciwieństwie do niektórych ludzi, wszędzie preferuje camelCase.

4. BeanFactoryAware: dostęp do minimalnego kontenera

W pewnym momencie „poznać swoje imię" staje się za mało, i pojawia się pokusa dostania dostępu do kontenera programowo. Do tego istnieje BeanFactoryAware: Spring przekazuje beanowi referencję do BeanFactory — najbardziej bazowe API kontenerowe. To już poważniejsze: faktycznie dostajemy „dostęp do magazynu", gdzie leżą inne beany (przynajmniej potencjalnie).

I właśnie dlatego uważa się to za mechanizm infrastrukturalny. U serwisu biznesowego prawie nigdy nie ma uczciwego powodu znać BeanFactory. Jeśli zaczyna „wyciągać" zależności sam, znowu wpadamy w ukryte zależności i service locator smell. Ale do diagnostyk lub rzadkich adapterów BeanFactoryAware bywa przydatny: na przykład żeby sprawdzić obecność jakichś beanów po nazwie i wydrukować ostrzeżenie.

Zróbmy mały i maksymalnie niewinny przykład: BeanFactoryProbe potrafi sprawdzać, czy bean o wskazanej nazwie istnieje. Nikogo nie tworzy, nie wywołuje getBean(...) (który może sprowokować zbędną inicjalizację), a po prostu pyta „czy taki jest".

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.stereotype.Component;

@Component
public class BeanFactoryProbe implements BeanFactoryAware {

    // BeanFactory jest ustawiany przez kontener przez callback (sami go nie tworzymy).
    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        // Spring przekazuje referencję do „minimalnego kontenera" podczas inicjalizacji.
        this.beanFactory = beanFactory;
    }

    public boolean hasBean(String name) {
        // containsBean() sprawdza obecność po nazwie i nie musi tworzyć beana (jeśli jest leniwy).
        return beanFactory.containsBean(name);
    }
}

Użycie jest elementarnie proste:

BeanFactoryProbe probe = ctx.getBean(BeanFactoryProbe.class);

// Sprawdzamy obecność beana po nazwie (przydatne do diagnostyki konfiguracji startowej).
System.out.println(probe.hasBean("orderPlacementService")); // true
System.out.println(probe.hasBean("bananaService"));         // false

Tu ważny morał: BeanFactory to jak „kuchnia kontenera". Możesz tam zajrzeć, żeby zrozumieć, co się dzieje, ale jeśli zaczniesz gotować biznesową kolację bezpośrednio na tej kuchni, używając garnków kontenera jako narzędzi biznesowych, szybko dostaniesz danie „nie wiadomo co, ale działa".

5. ApplicationContextAware: dostęp do „bogatego świata"

Jeśli BeanFactory to minimalny kontener, to ApplicationContext — już „kontener plus wszystko, co sprawia, że Spring jest wygodny": zasoby, komunikaty, zdarzenia, środowisko i wiele innych serwisów. Pracowaliśmy już z tym „bogatym światem" w module o Resource, MessageSource i zdarzeniach aplikacji. Czasem infrastrukturalnemu beanowi rzeczywiście wygodnie dostać referencję do kontekstu: na przykład żeby sprawdzić dostępność zasobu lub wydrukować context id.

ApplicationContextAware — interfejs, który pozwala kontenerowi przekazać do beana referencję do ApplicationContext. To potężne narzędzie, więc trzymamy prostą regułę: im potężniejsze narzędzie, tym łatwiej nim przypadkowo zbudować anty-wzorzec.

Pokażemy staranny przykład, który wygląda jak diagnostyka, a nie service locator. Niech nasz bean sprawdza, że szablon raportu naprawdę leży w classpath (a przecież używamy już szablonów w projekcie):

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

@Component
public class ReportTemplateDiagnostics implements ApplicationContextAware {

    // Referencję do ApplicationContext przekaże Spring (przez callback).
    private ApplicationContext ctx;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        // Ważne: to nie jest „zwykła zależność", lecz infrastrukturalna informacja o kontenerze.
        this.ctx = applicationContext;
    }

    public boolean headerTemplateExists() {
        // ApplicationContext potrafi szukać zasobów (classpath/file/url itp.) przez ujednolicone API.
        Resource resource = ctx.getResource("classpath:templates/reports/daily-report-header.txt");
        return resource.exists();
    }
}

Teraz, jeśli w jakimś scenariuszu chcesz wydrukować diagnostykę:

ReportTemplateDiagnostics d = ctx.getBean(ReportTemplateDiagnostics.class);

// Diagnostyka: upewniamy się, że potrzebny szablon naprawdę jest w classpath.
System.out.println(d.headerTemplateExists()); // true

Dlaczego to wciąż „normalnie" dla warstwy support? Bo używamy kontekstu nie jako magazynu zależności, lecz jako źródła infrastrukturalnych możliwości, które i tak należą do kontekstu: getResource(...) — to część ApplicationContext jako superset nad DI.

Ale gdybyśmy wewnątrz tej klasy zaczęli wywoływać ctx.getBean(ReportFormatter.class) i na bieżąco wybierać implementację — to już byłaby inna historia (i staranne ją omówimy w następnym wykładzie).

6. EnvironmentAware: techniczny kontekst

Umiemy już pracować z Environment, properties i profilami. Zazwyczaj do tego wystarczy @Value lub przekazanie Environment w konstruktorze. Ale Spring pozwala też dostać Environment przez callback Aware: bean implementuje EnvironmentAware, i kontener we właściwym momencie przekazuje mu referencję do środowiska.

Do czego to przydatne? Najczęściej do diagnostyk, konfiguracyjnych helperów i technicznych adapterów, które muszą „widzieć" aktywne profile lub czytać wartości właściwości. Ważne jest pamiętanie: Environment to nie zależność biznesowa. To „tabliczka z ustawieniami uruchomienia". Dlatego EnvironmentAware najczęściej mieszka w support.*, a nie w application.service.

Zróbmy prosty przykład: bean, który zwraca string z listą aktywnych profili.

import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
public class ProfileDiagnostics implements EnvironmentAware {

    // Environment zostanie przekazany przez kontener (przez callback), a nie stworzony przez nas ręcznie.
    private Environment environment;

    @Override
    public void setEnvironment(Environment environment) {
        // Spring wywołuje tę metodę i przesyła bieżące środowisko (properties + profile).
        this.environment = environment;
    }

    public String activeProfilesLine() {
        // Pobieramy aktywne profile i drukujemy w jednej linii (do diagnostyki startu).
        return String.join(",", environment.getActiveProfiles());
    }
}

Sprawdzenie w runnerze lub po prostu w main:

ProfileDiagnostics d = ctx.getBean(ProfileDiagnostics.class);

// Techniczny wydruk: jakie profile są naprawdę aktywne w bieżącym uruchomieniu.
System.out.println(d.activeProfilesLine()); // dev

Jeśli aktywnych profili jest kilka, zobaczysz dev,demo lub coś podobnego (w zależności od tego, jak uruchamiasz kontekst).

Jeszcze jeden przydatny scenariusz: wyciągnąć jakąś właściwość, żeby wydrukować ją w diagnostyce. Na przykład nazwę aplikacji:

public String appName() {
    // Jeśli właściwości nie ma — bierzemy domyślną, żeby diagnostyka nie łamała startu aplikacji.
    return environment.getProperty("contextflow.app-name", "ContextFlow");
}

I wydruk:

// Drukujemy wyliczoną nazwę aplikacji z Environment (lub domyślną).
System.out.println(d.appName()); // ContextFlow

Co miłe tu: w ogóle nie zaglądamy do warstwy biznesowej. Nie zmieniamy logiki tworzenia zamówienia. Po prostu robimy „techniczny check" i drukujemy informację, którą studentowi wygodnie widzieć przy uruchomieniu.

7. Kolejność wywołania callbacków Aware

Bardzo łatwo pomyśleć, że Aware to coś w stylu „konstruktora nr 2": niby kontener tworzy obiekt, i gdzieś potem „kiedyś" przekazuje mu dane. W praktyce wszystko jest bardziej rygorystyczne: callbacki Aware są wbudowane w cykl życia beana. I to jest ważne, bo od tego zależy, w jakim momencie możesz używać otrzymanych danych i dlaczego w konstruktorze ich jeszcze nie ma.

Nie potrzebujemy pełnego analizowania refresh() po kodzie źródłowym (celowo trzymamy kurs na poziomie modelu mentalnego), ale przydatne jest zobaczenie sekwencji kroków, żeby nie było niespodzianek. W bardzo uproszczonym widoku cykl życia singleton beana wygląda tak:

flowchart TD
    A["Tworzenie obiektu: constructor"] --> B["Dependency injection / set properties"]
    B --> C["Callbacki Aware
BeanNameAware, BeanFactoryAware, ..."] C --> D["Before init BPP"] D --> E["@PostConstruct / initMethod"] E --> F["After init BPP"] F --> G["Bean gotowy do pracy"]

Co z tego wynika w praktyce:

Po pierwsze, jeśli potrzebujesz nazwy beana (BeanNameAware) lub Environment (EnvironmentAware) do zrobienia czegoś w @PostConstruct, to jest to normalne: do momentu @PostConstruct te dane już będą ustawione. Właśnie dlatego często można spotkać wzorzec „dostaliśmy środowisko przez Aware → wydrukowaliśmy diagnostykę w @PostConstruct".

Po drugie, w konstruktorze danych Aware jeszcze nie ma. Dlatego próba używania beanName w konstruktorze nie ma sensu: on jeszcze nie jest ustawiony i dostaniesz null (lub „pustą" diagnostykę). Jeśli potrzebujesz czegoś do konstruktora — to powinna być normalna zależność (constructor injection), a nie callback.

I po trzecie, tu widać, dlaczego Aware jest odbierany jako mechanizm infrastrukturalny: jest powiązany z kontenerowym pipeline'em i wewnętrznymi krokami inicjalizacji. To nie złe. Po prostu to inna warstwa odpowiedzialności i lepiej, żeby była izolowana.

8. Mini-demo dla zrozumienia mechaniki

Żeby mechanikę zobaczyć w jednym miejscu, złożymy mały diagnostyczny bean. Jest celowo zrobiony jako demo-wariant: tu ważna jest sama kolejność callbacków, a nie finalne formatowanie projektu. W roboczej warstwie support taką diagnostykę wygodniej rejestrować jawnie przez config, a nie zostawiać jako przypadkowy @Component znaleziony przez skanowanie.

Niech zna swoją nazwę (BeanNameAware) i środowisko (EnvironmentAware), a potem w @PostConstruct drukuje jedną linię. Będzie mieszkał w support.diagnostics, żeby nie tworzyć wrażenia, że diagnostyka to część serwisów biznesowych.

import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
public class ContextDiagnosticsProbe implements BeanNameAware, EnvironmentAware {

    // Nazwa beana jest ustawiana przez kontener (przez BeanNameAware).
    private String beanName;

    // Środowisko jest ustawiane przez kontener (przez EnvironmentAware).
    private Environment environment;

    @Override
    public void setBeanName(String name) {
        // Spring wywoła to przed @PostConstruct, więc dalej beanName już nie będzie null.
        this.beanName = name;
    }

    @Override
    public void setEnvironment(Environment environment) {
        // Spring przekaże Environment, żebyśmy mogli czytać properties i profile.
        this.environment = environment;
    }

    @PostConstruct
    void printSummary() {
        // Bierzemy nazwę aplikacji z konfiguracji (z domyślną, gdy właściwości brak).
        String appName = environment.getProperty("contextflow.app-name", "ContextFlow");

        // Składamy string profili — czysto do wydrukowania diagnostyki przy starcie.
        String profiles = String.join(",", environment.getActiveProfiles());

        // Drukujemy „puls przy wejściu": co się podniosło i w jakim trybie.
        System.out.println(appName + " | " + beanName + " | profiles=" + profiles);
        // ContextFlow | contextDiagnosticsProbe | profiles=dev
    }
}

Co dobrego w takim przykładzie:

Nie wpływa na przetwarzanie zamówień, nie dodaje efektów ubocznych do serwisów use-case i nie zmienia modelu dziedziny. Po prostu drukuje techniczny string przy starcie. Przy tym pokazowo demonstruje, że interfejsy Aware naprawdę działają jako callbacki: Spring sam ustawi beanName i environment przed wywołaniem @PostConstruct.

Co ważne jest uchwycić: nie zrobiliśmy z niego „centralnej klasy aplikacji". Nie ciągniemy go do OrderPlacementService. Istnieje sam dla siebie, obok infrastruktury. Sam sens tu jest w kolejności callbacków; diagnostykę projektu zazwyczaj formuje się jeszcze jawniej — jako osobny support-bean podłączony przez config.

9. Typowe błędy przy pracy z Aware

Prawie wszystkie błędy wokół Aware pojawiają się nie dlatego, że API jest skomplikowane. API jest maksymalnie prymitywne: jedna metoda, jeden parametr. Błędy pojawiają się z powodu błędnego modelu mentalnego: ludzie zaczynają używać Aware jako „uniwersalnego DI", choć to kontenerowy callback dla infrastruktury.

Błąd nr 1: postrzeganie Aware jak zwykłej zależności i zastępowanie nim constructor injection.
Jeśli przepisujesz serwis biznesowy tak, żeby dostawał Environment lub ApplicationContext przez Aware, robisz zależności mniej jawnymi. W konstruktorze teraz nie widać, że klasie w ogóle cokolwiek jest potrzebne. A to pierwszy krok do „klasa niby prosta, ale dlaczego zależy od całego świata".

Błąd nr 2: oczekiwanie, że dane z Aware będą dostępne w konstruktorze.
Callbacki Aware są wywoływane po stworzeniu obiektu i po bazowym etapie wstrzykiwania zależności. Dlatego kod w stylu „w konstruktorze drukuję beanName" po prostu nie zadziała logicznie: beanName jeszcze nie jest ustawiony i dostaniesz null (lub „pustą" diagnostykę). Jeśli czegoś potrzebujesz na etapie konstruktora — to powinna być normalna zależność, a nie callback.

Błąd nr 3: implementacja od razu pięciu interfejsów Aware „na wszelki wypadek".
To wygląda jak: „no niby kiedy przyda się". Zazwyczaj nie przyda się, a powiązanie ze Springiem urośnie natychmiast. Znacznie lepiej najpierw sformułować, jaka dokładnie serwisowa informacja jest potrzebna beanowi, i wziąć dokładnie jeden interfejs do tego zadania. W 90% przypadków wystarczy Environment lub BeanName.

Błąd nr 4: używanie ApplicationContextAware dla jednego ustawienia lub jednego współpracownika.
Jeśli potrzebujesz stringa contextflow.app-name, wstrzyknij go przez @Value lub przekaż Environment. Jeśli potrzebujesz AuditWriter, wstrzyknij AuditWriter. ApplicationContextAware to nie „wygodne lenistwo", lecz potężny dostęp kontenerowy. Brać go dla drobiazgu — jak zamawiać ciężarówkę dla jednej bochenki chleba (choć bochenek bywa przekonujący).

Błąd nr 5: mieszanie diagnostyki technicznej z audytem biznesowym.
Diagnostyka kontekstu („jaki profil jest aktywny") — to informacja techniczna. Audyt („zamówienie zostało stworzone przez użytkownika X") — to część scenariusza biznesowego. Jeśli zaczniesz pisać techniczne komunikaty przez AuditWriter, sens zostanie wymieszany: audyt przestanie być biznesowym, a diagnostyka zacznie wyglądać jak zdarzenie biznesowe. Trzymaj je osobno: techniczne — osobnym support-beanem, biznesowe — przez zdarzenia domenowe i AuditWriter.

1
Zadanie
Spring Core, poziom 20, lekcja 2
Niedostępne
Nazwa beana i aktywny profil przez `Aware`
Nazwa beana i aktywny profil przez `Aware`
1
Zadanie
Spring Core, poziom 20, lekcja 2
Niedostępne
Sprawdzanie obecności beanów przez `BeanFactoryAware`
Sprawdzanie obecności beanów przez `BeanFactoryAware`
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION