CodeGym /Kursy /Spring Core /BeanPostProcessor: przed i po init

BeanPostProcessor: przed i po init

Spring Core
Poziom 19, Lekcja 2
Dostępny

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.

1
Zadanie
Spring Core, poziom 19, lekcja 2
Niedostępne
Śledzenie faz wokół init dla jednego beana
Śledzenie faz wokół init dla jednego beana
1
Zadanie
Spring Core, poziom 19, lekcja 2
Niedostępne
Opakowywanie beana przez `BeanPostProcessor`
Opakowywanie beana przez `BeanPostProcessor`
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION