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ą".