Skip to main content

How to use BeanPostProcessor

Jasne, wytłumaczę Ci dokładnie, czym jest BeanPostProcessor w Springu, jak działa i dlaczego jest jednym z najważniejszych mechanizmów w całym frameworku.

Zacznijmy od analogii, która pomoże Ci to poczuć.

Analogia: Fabryka Samochodów​

Wyobraź sobie, że kontener Springa to zaawansowana fabryka samochodów.

  1. Projekt (Definicja beana): Masz projekt samochodu (np. klasa z adnotacją @Component).
  2. Montaż (Instancjacja): Fabryka tworzy podstawowy szkielet samochodu (tworzy instancję obiektu).
  3. Instalacja podzespołów (Wstrzykiwanie zależności): Roboty montują silnik, koła, fotele (wstrzykiwanie zależności przez @Autowired).
  4. Pierwsze uruchomienie (Inicjalizacja): Samochód jest po raz pierwszy uruchamiany, aby sprawdzić, czy podstawowe systemy działają (wywołanie metod z @PostConstruct lub init-method).

I teraz wkracza BeanPostProcessor. To jest stanowisko kontroli jakości i tuningu, które znajduje się tuż za linią produkcyjną, ale zanim samochód trafi do klienta. Na tym stanowisku każdy gotowy samochód może zostać poddany dodatkowym operacjom.

  • Kontroler 1 (postProcessBeforeInitialization): Sprawdza samochód tuż po montażu, ale przed jego pierwszym uruchomieniem. Może np. sprawdzić ciśnienie w oponach.
  • Kontroler 2 (postProcessAfterInitialization): Sprawdza samochód po jego pierwszym uruchomieniu. Może np. nałożyć specjalną warstwę wosku, podmienić fabryczne radio na lepszy model albo dołożyć spojler.

BeanPostProcessor to dokładnie taki mechanizm: pozwala "przechwycić" każdego beana tworzonego w kontenerze Springa i wykonać na nim dodatkową logikę tuż przed lub tuż po jego właściwej inicjalizacji.


Jak to działa? Interfejs BeanPostProcessor​

Aby stworzyć własny procesor beanów, musisz zaimplementować interfejs BeanPostProcessor. Co ważne, sam procesor musi być beanem Springa (np. oznaczony @Component), aby Spring go wykrył i użył.

Interfejs ten ma dwie metody:

1. postProcessBeforeInitialization​

@Nullable
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// Twoja logika tutaj
return bean; // Zwróć oryginalny lub zmodyfikowany bean
}
  • Kiedy jest wywoływana? Po wstrzyknięciu zależności (@Autowired), ale przed wykonaniem jakichkolwiek metod inicjalizacyjnych (takich jak @PostConstruct czy InitializingBean#afterPropertiesSet).
  • Co robi? Daje Ci dostęp do "surowego" beana, który ma już wypełnione zależności.
  • Parametry:
  • Object bean: Aktualna instancja beana, który jest przetwarzany.
  • String beanName: Nazwa tego beana w kontenerze Springa.
  • Co zwraca? Musisz zwrócić instancję beana. Możesz zwrócić oryginalny bean bez zmian, albo go zmodyfikować. Uwaga: Jeśli zwrócisz null, dalsze przetwarzanie tego beana zostanie przerwane (zazwyczaj nie jest to pożądane).

2. postProcessAfterInitialization​

@Nullable
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// Twoja logika tutaj
return bean; // Zwróć oryginalny lub opakowany (wrapped) bean
}
  • Kiedy jest wywoływana? Po wykonaniu wszystkich metod inicjalizacyjnych (@PostConstruct itp.). Bean jest w tym momencie w pełni skonfigurowany i gotowy do użycia.
  • Co robi? To najczęstsze miejsce na modyfikacje. Możesz tutaj np. opakować oryginalny bean w proxy, co jest fundamentem działania np. Aspektów (AOP) w Springu.
  • Co zwraca? Najczęściej zwraca się tu albo oryginalny bean, albo jego "opakowaną" wersję (proxy).

Miejsce w Cyklu Życia Beana​

Dla pełnego zrozumienia, umieśćmy BeanPostProcessor w uproszczonym cyklu życia beana:

  1. Instancjacja: Spring tworzy obiekt za pomocą konstruktora.
  2. Wypełnianie właściwości: Spring wstrzykuje zależności (pola z @Autowired itd.).
  3. Wywołanie metod interfejsów Aware (np. BeanNameAware).
  4. ** postProcessBeforeInitialization() (Twój kod działa tutaj).
  5. Inicjalizacja: Spring wywołuje metody oznaczone @PostConstruct lub init-method.
  6. ** postProcessAfterInitialization() (Twój kod działa tutaj).
  7. Bean gotowy: Bean jest w pełni gotowy i może być używany przez inne komponenty.
  8. (Później, przy zamykaniu aplikacji) Destrukcja: Wywołanie metod @PreDestroy lub destroy-method.

Praktyczny Przykład: Wstrzykiwanie losowej liczby​

Stwórzmy własną adnotację @InjectRandomInt, która będzie wstrzykiwać losową liczbę całkowitą do pola w naszym beani-e. Zrobimy to właśnie za pomocą BeanPostProcessor.

Krok 1: Stwórz adnotację

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME) // Musi być dostępna w czasie działania aplikacji
public @interface InjectRandomInt {
int min();
int max();
}

Krok 2: Stwórz serwis, który użyje adnotacji

import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;

@Component
public class MyAwesomeService {

@InjectRandomInt(min = 5, max = 15)
private int repeatCount;

public void doWork() {
System.out.println("Wykonuję pracę " + repeatCount + " razy.");
}

@PostConstruct
public void init() {
System.out.println("Faza init: wartość repeatCount to " + repeatCount + " (jeszcze domyślna, czyli 0)");
}
}

Krok 3: Stwórz BeanPostProcessor (serce mechanizmu)

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.util.Random;

@Component // Ważne! Sam procesor musi być beanem!
public class InjectRandomIntBeanPostProcessor implements BeanPostProcessor {

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// Przeszukujemy pola w szukaniu naszej adnotacji
Field[] fields = bean.getClass().getDeclaredFields();
for (Field field : fields) {
InjectRandomInt annotation = field.getAnnotation(InjectRandomInt.class);
if (annotation != null) {
int min = annotation.min();
int max = annotation.max();
Random random = new Random();
int randomValue = min + random.nextInt(max - min + 1);

// Wstrzykujemy wartość do pola
field.setAccessible(true); // Umożliwiamy dostęp do pól prywatnych
ReflectionUtils.setField(field, bean, randomValue);
}
}
return bean; // Zwracamy zmodyfikowany bean
}
}

W tym przykładzie użyliśmy postProcessBeforeInitialization. Zauważ, że nasza wartość zostanie wstrzyknięta przed wywołaniem metody init() w MyAwesomeService.

Krok 4: Uruchomienie aplikacji

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
MyAwesomeService service = context.getBean(MyAwesomeService.class);
service.doWork();
}
}

Wynik w konsoli:

Faza init: wartość repeatCount to 12 (jeszcze domyślna, czyli 0) // <== ZAUWAŻ TO!
Wykonuję pracę 12 razy.

Chwila, dlaczego init() pokazał 0? W moim kodzie BeanPostProcessora umieściłem logikę w postProcessBeforeInitialization. Metoda init() z adnotacją @PostConstruct jest wywoływana po postProcessBeforeInitialization. Powinieneś zobaczyć tam wstrzykniętą wartość. Gdybym popełnił błąd, konsola wyglądałaby inaczej.

Poprawna kolejność zdarzeń:

  1. MyAwesomeService jest tworzony.
  2. InjectRandomIntBeanPostProcessor.postProcessBeforeInitialization jest wywoływany.
  3. Pole repeatCount otrzymuje losową wartość (np. 12).
  4. Metoda MyAwesomeService.init() z @PostConstruct jest wywoływana. Powinna już widzieć wartość 12.
  5. MyAwesomeService.doWork() jest wywoływana i pokazuje wartość 12.

Poprawny wynik w konsoli powinien wyglądać tak:

Faza init: wartość repeatCount to 12
Wykonuję pracę 12 razy.

Jeśli chcesz coś zrobić po inicjalizacji, przenieś logikę do postProcessAfterInitialization. Jest to częstsze, zwłaszcza gdy tworzysz proxy.


Jak Spring używa BeanPostProcessor?​

Ten mechanizm jest kręgosłupem wielu fundamentalnych funkcji Springa:

  • @Autowired: AutowiredAnnotationBeanPostProcessor przeszukuje beany w poszukiwaniu pól i metod z @Autowired i wstrzykuje odpowiednie zależności.
  • AOP (Programowanie Aspektowe): AnnotationAwareAspectJAutoProxyCreator to BeanPostProcessor, który sprawdza, czy dany bean powinien być "opakowany" w proxy, aby dodać do niego logikę aspektów (np. @Transactional, @Secured). Robi to w postProcessAfterInitialization, zwracając proxy zamiast oryginalnego obiektu.
  • @PostConstruct / @PreDestroy: CommonAnnotationBeanPostProcessor odpowiada za wykrywanie i wywoływanie tych metod.
  • Walidacja: BeanValidationPostProcessor włącza walidację beanów oznaczonych @Validated.

Podsumowanie​

  1. BeanPostProcessor to interfejs-hak (ang. hook), który pozwala Ci ingerować w proces tworzenia każdego beana w kontenerze.
  2. Posiada dwie metody: postProcessBeforeInitialization (przed inicjalizacją) i postProcessAfterInitialization (po inicjalizacji).
  3. Jest to niezwykle potężne narzędzie do implementacji logiki przekrojowej (cross-cutting concerns), takiej jak logowanie, obsługa własnych adnotacji, tworzenie proxy (AOP) czy modyfikacja beanów.
  4. Sam Spring intensywnie z niego korzysta do realizacji swoich kluczowych funkcji. Zrozumienie BeanPostProcessor to zrozumienie, jak Spring działa "pod maską".