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.
- Projekt (Definicja beana): Masz projekt samochodu (np. klasa z adnotacją
@Component). - Montaż (Instancjacja): Fabryka tworzy podstawowy szkielet samochodu (tworzy instancję obiektu).
- Instalacja podzespołów (Wstrzykiwanie zależności): Roboty montują silnik, koła, fotele (wstrzykiwanie zależności przez
@Autowired). - Pierwsze uruchomienie (Inicjalizacja): Samochód jest po raz pierwszy uruchamiany, aby sprawdzić, czy podstawowe systemy działają (wywołanie metod z
@PostConstructlubinit-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@PostConstructczyInitializingBean#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
beanbez zmian, albo go zmodyfikować. Uwaga: Jeśli zwrócisznull, 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 (
@PostConstructitp.). 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:
- Instancjacja: Spring tworzy obiekt za pomocą konstruktora.
- Wypełnianie właściwości: Spring wstrzykuje zależności (pola z
@Autowireditd.). - Wywołanie metod interfejsów
Aware(np.BeanNameAware). - **
postProcessBeforeInitialization()(Twój kod działa tutaj). - Inicjalizacja: Spring wywołuje metody oznaczone
@PostConstructlubinit-method. - **
postProcessAfterInitialization()(Twój kod działa tutaj). - Bean gotowy: Bean jest w pełni gotowy i może być używany przez inne komponenty.
- (Później, przy zamykaniu aplikacji) Destrukcja: Wywołanie metod
@PreDestroylubdestroy-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ń:
MyAwesomeServicejest tworzony.InjectRandomIntBeanPostProcessor.postProcessBeforeInitializationjest wywoływany.- Pole
repeatCountotrzymuje losową wartość (np. 12). - Metoda
MyAwesomeService.init()z@PostConstructjest wywoływana. Powinna już widzieć wartość 12. 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:AutowiredAnnotationBeanPostProcessorprzeszukuje beany w poszukiwaniu pól i metod z@Autowiredi wstrzykuje odpowiednie zależności.- AOP (Programowanie Aspektowe):
AnnotationAwareAspectJAutoProxyCreatortoBeanPostProcessor, który sprawdza, czy dany bean powinien być "opakowany" w proxy, aby dodać do niego logikę aspektów (np.@Transactional,@Secured). Robi to wpostProcessAfterInitialization, zwracając proxy zamiast oryginalnego obiektu. @PostConstruct/@PreDestroy:CommonAnnotationBeanPostProcessorodpowiada za wykrywanie i wywoływanie tych metod.- Walidacja:
BeanValidationPostProcessorwłącza walidację beanów oznaczonych@Validated.
Podsumowanie
BeanPostProcessorto interfejs-hak (ang. hook), który pozwala Ci ingerować w proces tworzenia każdego beana w kontenerze.- Posiada dwie metody:
postProcessBeforeInitialization(przed inicjalizacją) ipostProcessAfterInitialization(po inicjalizacji). - 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.
- Sam Spring intensywnie z niego korzysta do realizacji swoich kluczowych funkcji. Zrozumienie
BeanPostProcessorto zrozumienie, jak Spring działa "pod maską".