1. Przeznaczenie BeanPostProcessor
Jeśli właśnie oswajasz się z dependency injection, może pojawić się odczucie: „No, kontener umie tworzyć obiekty i łączyć zależności — czego więcej trzeba?" W praktyce chce się znacznie więcej: żeby kontener mógł wmieszać się w moment narodzin każdego beana, podobnie jak na linii produkcyjnej są stanowiska kontroli jakości, pakowania i oznaczania.
BeanPostProcessor to właśnie takie stanowisko na taśmie. Kontener już stworzył obiekt, wstrzyknął zależności, ale jeszcze nie uważa go za w pełni gotowego. W tym momencie Spring daje dwa „okna" wokół fazy init: jedno przed inicjalizacją, drugie po. I w tych oknach możesz robić techniczne rzeczy, których bardzo nie chcesz kopiować ręcznie do wszystkich klas.
Ważne od razu utrwalić filozofię: BeanPostProcessor to infrastrukturalny mechanizm kontenera, a nie „kolejne miejsce na logikę biznesową". Jeśli zaczniesz wstawiać tam reguły domenowe dotyczące zamówień („jeśli kwota przekracza 1000 — rabat 5%"), dostaniesz aplikację, której nie da się zrozumieć bez wiedzy o wewnętrznej kuchni kontenera. Natomiast jeśli używasz BPP do diagnostyki, technicznych opakowań, starannej konfiguracji i integracji — to staje się potężnym narzędziem, które wyjaśnia połowę „magii" Springa.
2. BeanPostProcessor w lifecycle
Żeby BeanPostProcessor przestał być abstrakcją, trzeba postawić go na osi czasu lifecycle beana. I tu dobry moment, żeby przypomnieć sobie wykłady o lifecycle beanów: mówiliśmy, że bean ma „narodziny", „inicjalizację", „gotowość", „zniszczenie". BPP jest wbudowany dokładnie w tę część, gdzie kontener doprowadza obiekt do gotowego stanu.
Spójrzmy na uproszczony schemat (bez rzadkich szczegółów, ale z prawidłowym sensem):
flowchart TD %% Uproszczony schemat: BPP otacza fazę init BD["BeanDefinition
opis"] --> C["constructor
tworzenie obiektu"] C --> DI["dependency injection
wstrzykiwanie zależności"] DI --> B1["BPP beforeInit
postProcessBeforeInitialization"] B1 --> INIT["init callbacks
@PostConstruct / initMethod / InitializingBean"] INIT --> B2["BPP afterInit
postProcessAfterInitialization"] B2 --> READY["bean gotowy do pracy"]
Sens jest taki: postProcessBeforeInitialization wywoływany jest po wstrzyknięciu zależności, ale przed @PostConstruct i innymi init-callbackami. A postProcessAfterInitialization — już po tym, jak bean przeszedł fazę init i „oficjalnie ożył".
I teraz — ważny szczegół, od którego nowicjuszom często „jedzie beret". Metody BPP zwracają Object. To nie jest przypadkowe i nie „no po prostu tak zrobili". Kontener mówi ci: „Trzymaj bieżącą instancję. Chcesz — zwróć ją jak jest. Chcesz — zwróć inny obiekt (opakowanie). Ale dalej będę pracował z tym, co zwróciłeś."
Żeby utrwalić różnicę z BFPP, wygodnie porównać je w tabeli. Tabela tu lepsza niż lista: mniej patosu, więcej sensu.
| Mechanizm | Kiedy wykonywany | Z czym pracuje | Co realnie zmienia | Typowy sens |
|---|---|---|---|---|
| BeanFactoryPostProcessor | przed tworzeniem zwykłych singleton-beanów | BeanDefinition i metadane | „rysunki": scope, lazy-init, wartości właściwości na poziomie opisu | sprawdzenia fail-fast i poprawki struktury kontekstu |
| BeanPostProcessor | wokół fazy init każdej instancji beana | prawdziwy obiekt + beanName | obiekt, który trafi dalej do kontenera (w tym opakowania) | techniczna obsługa instancji, diagnostyka, dekorowanie |
Jeśli trzymać w głowie tę tabelę, to zamieszanie „a dlaczego w BFPP nie można po prostu wziąć beana i wywołać metodę?" znika samo z siebie: BFPP żyje w świecie rysunków, BPP — w świecie złożonych części.
3. Minimalny BeanPostProcessor: kolejność faz
Teraz chcemy zrobić jak najbardziej „uczciwy" przykład, żeby zobaczyć: BPP wywołuje się nie „kiedyś", lecz w bardzo konkretnym momencie. I tu świetnie pasuje prosta diagnostyka: drukować do konsoli wiadomości „before init" i „after init" dla wybranych beanów.
W ContextFlow nie chcemy logować absolutnie wszystkiego (tam byłoby za głośno), więc będziemy filtrować. Najprostszy filtr dla projektu dydaktycznego — po nazwie beana: na przykład śledzić wszystko, co kończy się na Service.
package com.example.contextflow.support.postprocessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class TrackedComponentBeanPostProcessor implements BeanPostProcessor {
@Override public Object postProcessBeforeInitialization(Object bean, String name) {
// Filtrujemy beany, żeby nie utonąć w logach (w kontekście jest dużo infrastrukturalnych beanów Spring)
if (name.endsWith("Service")) System.out.println("Before init: " + name); // Before init: orderPlacementService
// Ważne: zwracamy obiekt z powrotem do kontenera (lub opakowanie, jeślibyśmy je robili)
return bean;
}
@Override public Object postProcessAfterInitialization(Object bean, String name) {
// Logika ta sama: reagujemy po fazie init, ale nie ruszamy pozostałych beanów
if (name.endsWith("Service")) System.out.println("After init: " + name); // After init: orderPlacementService
// Też obowiązkowo zwrócić końcowy obiekt
return bean;
}
}
Zwróć uwagę na dwie rzeczy. Po pierwsze, zawsze zwracamy bean. To ważne: kontener potrzebuje końcowego obiektu. Po drugie, filtr jest maksymalnie wąski, bo inaczej szybko utopimy się w logach i przestaniemy rozumieć, co się dzieje.
Ten BPP trzeba teraz zarejestrować. Umówiliśmy się, że takie mechaniki żyją w support.postprocessor i podłączane są przez config.*. Dla lokalnej demonstracji wystarczy mały BppDemoConfig: pokazuje sam fakt, że BPP to zwykły bean, który kontener podchwyci we właściwej fazie.
package com.example.contextflow.config.core;
import com.example.contextflow.support.postprocessor.TrackedComponentBeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BppDemoConfig {
@Bean
public TrackedComponentBeanPostProcessor trackedComponentBeanPostProcessor() {
// BeanPostProcessor — to też bean: wystarczy go zarejestrować w kontekście
return new TrackedComponentBeanPostProcessor();
}
}
Jeśli po tym uruchomisz aplikację (kontekst podnosi się, tworzone są singleton-beany), zobaczysz, że wiadomości „Before init" pojawiają się przed zakończeniem fazy init, a „After init" — po. W rzeczywistym wyjściu będzie dużo linii, ale kolejność wewnątrz jednego beana odpowiada logice taśmy.
I tu zazwyczaj pojawia się pytanie: „A gdzie @PostConstruct?" Świetne pytanie, i możemy to pokazać na mini-fragmencie z już znajomego NotificationTemplateCatalog (to bean zarządzany przez lifecycle z Dnia 10). Niech ma taki metodę:
import jakarta.annotation.PostConstruct;
@PostConstruct
void loadTemplates() {
// Ten callback powinien trafić między beforeInit i afterInit BeanPostProcessor
System.out.println("Templates loaded"); // Templates loaded
}
Jeśli twój BPP jest skonfigurowany do śledzenia też tego beana (na przykład tymczasowo dodaj warunek || name.equals("notificationTemplateCatalog")), kolejność będzie taka: najpierw „Before init …", potem „Templates loaded", potem „After init …". I to bezpośredni dowód, że BPP otacza etap init.
4. Podmiana beana przez dekorator
Sama idea „BPP może zwrócić inny obiekt" brzmi jednocześnie potężnie i trochę przerażająco. Dlatego zróbmy przykład maksymalnie przyziemny: nie AOP, nie proxy, nie czarna magia, a zwykły dekorator Java.
W ContextFlow mamy interfejs NotificationSender. Wyobraźmy sobie uproszczone:
package com.example.contextflow.domain.ports;
public interface NotificationSender {
// Kontrakt portu: konkretna implementacja może być dowolna, ale metoda wysyłania — wspólna
void send(String message);
}
Chcemy dodać czysto techniczne zachowanie: przed wysłaniem powiadomienia drukować diagnostyczny string (na przykład żeby zobaczyć, że powiadomienia w ogóle się wysyłają i jakie wiadomości lecą). I nie chcemy przepisywać każdej implementacji (EmailNotificationSender, SmsNotificationSender, ConsoleNotificationSender) i dodawać tam System.out.println.
Zróbmy opakowanie:
package com.example.contextflow.support.postprocessor;
import com.example.contextflow.domain.ports.NotificationSender;
public final class TrackingNotificationSender implements NotificationSender {
private final NotificationSender delegate; // Oryginalny nadawca, który już umie robić prawdziwą pracę
public TrackingNotificationSender(NotificationSender delegate) { this.delegate = delegate; }
@Override public void send(String message) {
// Techniczna diagnostyka: dodajemy zachowanie, nie zmieniając implementacji źródłowych
System.out.println("[tracked] " + message); // [tracked] Order created: A-101
delegate.send(message); // Delegujemy dalej do „prawdziwej" implementacji
}
}
To zwykły dekorator: implementuje ten sam interfejs i deleguje wewnątrz do „prawdziwego" nadawcy. Teraz najciekawsze: każemy kontenerowi podmienić bean na to opakowanie na etapie postProcessAfterInitialization.
package com.example.contextflow.support.postprocessor;
import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class TrackedComponentBeanPostProcessor implements BeanPostProcessor {
@Override public Object postProcessAfterInitialization(Object bean, String name) {
// Opakowujemy tylko potrzebny typ, żeby nie wmieszywać się we wszystkie beany z rzędu
if (bean instanceof NotificationSender sender && !(bean instanceof TrackingNotificationSender)) {
// Ochrona przed podwójnym opakowaniem: jeśli to już TrackingNotificationSender — nie ruszamy
return new TrackingNotificationSender(sender);
}
// Jeśli warunek nie pasuje — zwracamy oryginalny obiekt
return bean;
}
}
Tu logika jest mała, ale sens ogromny. Jeśli kontener stworzył jakiś NotificationSender, po fazie init zwracamy nie oryginalny obiekt, lecz opakowanie. Od tej chwili wszyscy, którzy proszą o NotificationSender (przez DI), będą dostawać nie „czystą" implementację, lecz „implementację plus śledzenie".
I tu ważna myśl metodyczna: BPP zmienia zachowanie aplikacji nie przez zmianę klasy źródłowej, lecz przez podmianę obiektu żyjącego w kontekście. Z zewnątrz wygląda, jakbyś „domieszał" zachowanie. A faktycznie po prostu podłożyłeś opakowanie, centralnie.
Jeśli teraz gdzieś w aplikacji jest wywołanie:
notificationSender.send("Order created: A-101"); // W runtime to już może być wywołanie na dekoratorze, a nie na „czystej" implementacji
to wyjście może stać się takie:
[tracked] Order created: A-101
...dalej już wyjście konkretnej implementacji (np. ConsoleNotificationSender)
I to, na najbardziej podstawowym poziomie, wyjaśnia, jak Spring w ogóle potrafi „dodawać zachowanie" do beanów. Nie zagłębiamy się w to, jak dokładnie Spring robi duże uniwersalne opakowania — to temat kolejnych dni. Teraz wystarczy zobaczyć zasadę: kontener zarządza tym, który obiekt jest naprawdę uznawany za bean.
5. Integracja BPP w ContextFlow
Bardzo łatwo zrobić BeanPostProcessor, który „jakoś działa". Znacznie trudniej zrobić go tak, żeby za tydzień nie powiedzieć sobie: „Kto to napisał? A, ja... no to tym bardziej wstyd".
W ContextFlow trzymamy dyscyplinę: post-procesory żyją w support.postprocessor, a podłączane są przez config.*. Warstwa biznesowa (domain, application) nie powinna wiedzieć, że w ogóle mamy BPP. Jeśli serwisy biznesowe zaczną zależeć od „faktu opakowywania", podpinasz domenę do Springa (i potem leczysz się z tego kolejnymi kursami).
Technicznie rejestracja pozostaje taka sama jak w BppDemoConfig powyżej: BPP to też bean. W projekcie ważniejsze jest inne: trzymać go w osobnym module konfig i nie ciągnąć tej mechaniki do warstwy biznesowej.
Właśnie tu Spring często i wygrywa: skomplikowana wewnętrzna mechanika podłącza się małym, czytelnym, uczciwym kodem wirowania.
6. Niuanse pracy z BeanPostProcessor
Gdy w rękach pojawia się mechanizm, który uruchamia się na każdym beanie i potencjalnie może podmienić dowolny obiekt — to trochę jak dostać prawa administratora systemu operacyjnego. Fajnie, ale konsekwencje błędów też „administratorskie".
Najważniejszy niuans — BPP wywoływany jest dla bardzo wielu beanów. W twoim kontekście jest nie tylko beany aplikacji, ale i infrastrukturalne beany Spring. Dlatego każde „ciężkie" działanie w BPP (drogie obliczenia, czytanie plików, wywołania sieciowe, próby wchodzenia do bazy — chociaż nie mamy bazy) drastycznie pogorszy start i sprawi, że zachowanie będzie nieprzejrzyste.
Drugi niuans — zwracany obiekt musi pozostać kompatybilny z kontraktem. Jeśli wszystkie zależności wymagają interfejsu NotificationSender, to zwrócenie dekoratora implementującego NotificationSender jest bezpieczne. Ale jeśli gdzieś w kodzie oczekiwany jest konkretny class (np. ConsoleNotificationSender) — a ty zwróciłeś opakowanie innej klasy — to instanceof ConsoleNotificationSender nagle przestanie działać. To nie „bug Spring", to konsekwencja tego, że sam zmieniłeś obiekt. I to kolejny powód, dlaczego w normalnym projekcie zależności powinny być po interfejsach, a nie po konkretnych klasach.
Trzeci niuans — BPP tworzy łańcuch. Czyli możesz mieć kilka BeanPostProcessor, i każdy dostaje wynik poprzedniego. To znaczy, że „bean" w metodzie BPP — niekoniecznie „oryginalny obiekt", czasem to już czyjeś opakowanie. W projekcie dydaktycznym staramy się trzymać to prosto: jeden BFPP, jeden BPP. Ale model mentalny warto mieć już teraz.
Żeby nie zamieniać tego w listę „zakazów", pokażę małą tabelkę „adekwatnie/niebezpiecznie" — jako punkt orientacyjny, gdy projektujesz BPP:
| Pomysł | W dydaktycznym ContextFlow | Dlaczego |
|---|---|---|
| Drukować diagnostykę (po wąskim filtrze) | normalnie | pomaga zobaczyć pipeline kontenera |
| Dekorować bean interfejsowy (jak NotificationSender) | normalnie, jeśli z ograniczeniami | demonstruje podmianę obiektu bez niszczenia domeny |
| Przetwarzać wszystkie beany z rzędu bez filtra | niebezpiecznie | szum, spowolnienie, trudno debugować |
| Chowanie logiki biznesowej w BPP | niebezpiecznie | zachowanie aplikacji staje się „niewidoczne" w kodzie serwisów |
| Domieszanie zależności w BPP, które mogą tworzyć cykle | niebezpiecznie | można zepsuć start kontekstu w nietrywialny sposób |
| Robienie BPP „uniwersalnym frameworkiem wewnątrz frameworka" | niebezpiecznie | kurs o rozumieniu Springa, a nie o pisaniu własnego Springa |
Jeśli trzymać te proste punkty orientacyjne w głowie, BeanPostProcessor będzie odczuwany nie jak magia, lecz jak uczciwy punkt rozszerzenia.
7. Typowe błędy przy pracy z BeanPostProcessor
Błąd nr 1: zapomniane zwrócenie beana z postProcessBeforeInitialization(...) / postProcessAfterInitialization(...).
BeanPostProcessor to nie listener zdarzeń, który „po prostu się powiadomił". To część taśmy, i kontener czeka od niego końcowy obiekt. Jeśli zwrócisz null lub przypadkowo zgubisz return, dostaniesz dziwne zachowanie i bardzo nieprzyjemną diagnostykę. W projekcie dydaktycznym lepiej zawsze zaczynać od „zwróciłem jak jest", a potem dopiero dodawać dekorowanie.
Błąd nr 2: przetwarzanie wszystkich beanów z rzędu i utopienie się w szumie.
Nowicjuszowi chce się „zobaczyć wszystko", więc zaczyna drukować linijki dla każdego beana. A potem widzi 200 linii logów, wśród których nie da się znaleźć własnych serwisów. Prawidłowiej wybrać wąski kryterium: po nazwie (*Service), po pakiecie (com.example.contextflow.*) lub po interfejsie (NotificationSender). Wtedy BPP pozostaje narzędziem, a nie generatorem chaosu.
Błąd nr 3: podmiana beana na opakowanie, które nie odpowiada typowi zależności.
Najczęstszy scenariusz — zwróciłeś obiekt, który nie implementuje potrzebnego interfejsu, lub zwróciłeś opakowanie, ale zależność gdzieś oczekiwała konkretnego klasy. W efekcie DI może się zepsuć, a czasem logika (na przykład instanceof w diagnostyce) zaczyna działać inaczej. Leczy się to dyscypliną: zależności — po interfejsach, a opakowania — kompatybilne.
Błąd nr 4: przenoszenie logiki biznesowej do BPP „bo wygodnie".
Czasem wydaje się kuszące: „no skoro zdarzenie już przyszło, zróbmy tu wszystko". Ale wtedy traci się sens rozdzielenia. BPP to punkt wejścia, a nie miejsce, gdzie budujesz duży scenariusz. Jeśli reakcja jest złożona, lepiej wyciągnąć ją do osobnego serwisu i wywołać go z BPP.
Błąd nr 5: zrobienie BPP zależnym od połowy kontekstu.
Technicznie BPP to bean i też można mu wstrzykiwać zależności. Ale jeśli zaczniesz ciągnąć wewnątrz BPP mnóstwo serwisów, łatwo dojść do cykli, wczesnej inicjalizacji i „wszystko zepsuło się, ale dlaczego — niejasne". W dydaktycznym ContextFlow BPP powinien być prosty: maksymalnie wąska diagnostyka i demonstracja opakowania.
GO TO FULL VERSION