🍃 Розділ 5 · Питання #8

Що таке BeanPostProcessor

Практично вся ключова «магія» Spring реалізована саме через внутрішній ланцюжок BeanPostProcessor:


🟢 Junior Level

BeanPostProcessor (BPP) — це фундаментальний інтерфейс розширення у Spring Framework, який дозволяє перехоплювати, модифікувати, конфігурувати або навіть повністю підміняти екземпляри бінів у момент їх створення IoC-контейнером.

Навіщо потрібен BeanPostProcessor

Практично вся ключова «магія» Spring реалізована саме через внутрішній ланцюжок BeanPostProcessor:

  • Впровадження залежностей через @Autowired та @Value (AutowiredAnnotationBeanPostProcessor).
  • Обробка callback-методів життєвого циклу @PostConstruct та @PreDestroy (CommonAnnotationBeanPostProcessor).
  • Створення динамічних AOP-проксі для транзакцій @Transactional, асинхронності @Async та безпеки (AnnotationAwareAspectJAutoProxyCreator).

Контракт інтерфейсу

Інтерфейс містить два методи з реалізацією за замовчуванням (default methods):

package org.springframework.beans.factory.config;

public interface BeanPostProcessor {

    // Викликається ДО init-методів (@PostConstruct, afterPropertiesSet, custom init)
    default Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean; // Повертаємо вихідний об'єкт або його модифікацію
    }

    // Викликається ПІСЛЯ init-методів (ТУТ створюються AOP-проксі!)
    default Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean; // Повертаємо вихідний об'єкт або згенерований Proxy
    }
}

Простий приклад: кастомний BPP для заміру часу створення бінів

package com.example.bpp;

import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class BenchmarkBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        // Виконується перед фазою @PostConstruct
        System.out.println("BPP BeforeInit: підготовка біна -> " + beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // Виконується після @PostConstruct, тут можна обгорнути об'єкт у проксі
        System.out.println("BPP AfterInit: бін повністю ініціалізовано -> " + beanName);
        return bean;
    }
}

🟡 Middle Level

Місце BeanPostProcessor у конвеєрі створення біна

1. Instantiation (Конструктор)
             ↓
2. Populate Properties (Впровадження полів і сетерів)
             ↓
3. Aware-інтерфейси (BeanNameAware, BeanFactoryAware)
             ↓
4. BPP: postProcessBeforeInitialization()
             ↓
5. @PostConstruct / InitializingBean.afterPropertiesSet() / custom initMethod
             ↓
6. BPP: postProcessAfterInitialization() ← [ТОЧКА ГЕНЕРАЦІЇ ДИНАМІЧНИХ ПРОКСІ]
             ↓
7. Готовий бін розміщується в кеші синглтонів (singletonObjects)

Різниця між BeanFactoryPostProcessor та BeanPostProcessor

Це одне з найважливіших питань на співбесідах будь-якого рівня:

Критерій BeanFactoryPostProcessor (BFPP) BeanPostProcessor (BPP)
З чим працює З метаданими (BeanDefinition) З живими екземплярами об’єктів у купі (Heap)
Коли виконується До створення будь-яких бінів (фаза завантаження контексту) Під час створення кожного конкретного біна
Що може робити Змінювати властивості біна, класи, параметри конструктора, підставляти значення з properties Обгортати в проксі, перевіряти анотації, валідувати поля, викликати колбеки
Типові приклади PropertySourcesPlaceholderConfigurer (${...}) AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor

Практичний приклад: створення проксі через postProcessAfterInitialization

Реалізуємо власну анотацію логування часу виконання @AuditLog за допомогою Spring AOP ProxyFactory:

package com.example.bpp;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
}
package com.example.bpp;

import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class AuditLogBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        Class<?> beanClass = bean.getClass();
        
        // Якщо клас біна позначено нашою анотацією @AuditLog
        if (beanClass.isAnnotationPresent(AuditLog.class)) {
            ProxyFactory proxyFactory = new ProxyFactory(bean);
            
            // Додаємо Advice (перехоплювач викликів методів)
            proxyFactory.addAdvice((MethodInterceptor) invocation -> {
                System.out.println("[AUDIT START] Виклик методу: " + invocation.getMethod().getName());
                long start = System.currentTimeMillis();
                try {
                    return invocation.proceed();
                } finally {
                    long duration = System.currentTimeMillis() - start;
                    System.out.println("[AUDIT END] Метод " + invocation.getMethod().getName() 
                            + " виконано за " + duration + " ms");
                }
            });
            
            // Повертаємо динамічний проксі замість оригінального об'єкта!
            return proxyFactory.getProxy();
        }
        
        return bean;
    }
}

🔴 Senior Level

Спеціалізована ієрархія під-інтерфейсів BPP

Інтерфейс BeanPostProcessor має розвинену ієрархію для глибокої інтеграції в ядро контейнера:

  1. InstantiationAwareBeanPostProcessor:
    • postProcessBeforeInstantiation(Class<?> beanClass, String beanName): викликається до виклику конструктора. Дозволяє повернути готовий екземпляр (наприклад, попередньо створений стаб або проксі). Якщо повертається не-null, стандартний цикл інстанціювання повністю переривається!
    • postProcessAfterInstantiation(Object bean, String beanName): викликається одразу після конструктора, але до ін’єкції полів. Якщо повернути false, Spring пропустить фазу впровадження залежностей.
    • postProcessProperties(PropertyValues pvs, Object bean, String beanName): саме тут AutowiredAnnotationBeanPostProcessor інжектує залежності в поля @Autowired.
  2. SmartInstantiationAwareBeanPostProcessor:
    • predictBeanType(...): визначення типу майбутнього біна до його фактичного створення.
    • determineCandidateConstructors(...): вибір відповідного конструктора для Constructor Injection.
    • getEarlyBeanReference(Object bean, String beanName): викликається при розв’язанні циклічних залежностей через 3-й кеш (singletonFactories) для раннього створення AOP-проксі.
  3. MergedBeanDefinitionPostProcessor:
    • postProcessMergedBeanDefinition(...): сканування метаданих класу та кешування знайдених анотацій (@Autowired, @PostConstruct) у внутрішні структури дескриптора RootBeanDefinition.
  4. DestructionAwareBeanPostProcessor:
    • postProcessBeforeDestruction(Object bean, String beanName): викликається перед знищенням біна (так працює обробка @PreDestroy).

Порядок виконання: PriorityOrdered vs Ordered

Коли в контексті зареєстровано безліч BPP, порядок їх виконання має критичне значення. Наприклад, BPP, що впроваджує поля, зобов’язаний виконатися до BPP, який перевіряє значення цих полів.

Spring сортує BeanPostProcessor за 4 категоріями:

  1. Ті, що реалізують інтерфейс org.springframework.core.PriorityOrdered (найвищий пріоритет).
  2. Ті, що реалізують інтерфейс org.springframework.core.Ordered.
  3. Анотовані @Order.
  4. Без зазначення порядку (виконуються наприкінці у довільному порядку).

Проблема: Передчасна ініціалізація бінів (BPP Dependency Pitfall)

Якщо кастомний BeanPostProcessor оголошує пряму залежність від бізнес-сервісу:

@Component
public class ProblematicBpp implements BeanPostProcessor {

    // ПОМИЛКА АРХІТЕКТУРИ: BPP залежить від сервісу бізнес-логіки!
    @Autowired
    private SecurityService securityService; 

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean;
    }
}

Що відбувається всередині контейнера:

  1. Контейнер стартує та реєструє всі BeanPostProcessor (метод registerBeanPostProcessors() в AbstractApplicationContext).
  2. Щоби інстанціювати ProblematicBpp, Spring змушений задовольнити його залежність SecurityService.
  3. SecurityService інстанціюється до того, як зареєстровані системні BPP (наприклад, AnnotationAwareAspectJAutoProxyCreator для @Transactional).
  4. У консолі з’являється попередження:

    INFO: Bean 'securityService' of type [com.example.SecurityService] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

  5. SecurityService залишається «сирим» об’єктом: методи з @Transactional не відкривають транзакції, а @Async виконується синхронно!

Архітектурне рішення:

Інжектувати залежності в BPP лише через ObjectProvider<T>, ApplicationContext або позначати анотацією @Lazy:

@Component
public class FixedBpp implements BeanPostProcessor {

    private final ObjectProvider<SecurityService> securityServiceProvider;

    public FixedBpp(ObjectProvider<SecurityService> securityServiceProvider) {
        this.securityServiceProvider = securityServiceProvider;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // Отримання залежності відбувається суворо на вимогу в рантаймі
        SecurityService securityService = securityServiceProvider.getIfAvailable();
        return bean;
    }
}

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь

BeanPostProcessor (BPP) — це головна точка розширення Spring IoC, що дозволяє перехоплювати біни під час їх створення. BPP містить два ключові методи:

  1. postProcessBeforeInitialization(): викликається перед init-методами (тут обробляється @PostConstruct та передається ApplicationContext).
  2. postProcessAfterInitialization(): викликається після init-методів і служить головною точкою створення AOP-проксі (@Transactional, @Async, @Cacheable). На відміну від BeanFactoryPostProcessor, який працює з метаданими (BeanDefinition), BPP маніпулює живими об’єктами в пам’яті.

4 каверзних питання з відповідями

1. У чому різниця між BeanFactoryPostProcessor та BeanPostProcessor?

Відповідь: BeanFactoryPostProcessor виконується першим, коли в контексті існують лише мета-описи бінів (BeanDefinition), і дозволяє змінювати властивості конфігурації (наприклад, підставляти значення плейсхолдерів ${db.url}). BeanPostProcessor працює пізніше, для кожного окремого створюваного екземпляра біна, дозволяючи модифікувати стан об’єкта або замінювати його на проксі.

2. Як працює InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()?

Відповідь: Цей метод викликається до виклику конструктора біна. Якщо він повертає не-null об’єкт, Spring повністю пропускає стандартне інстанціювання, впровадження залежностей та ініціалізацію для цього біна, одразу переходячи до postProcessAfterInitialization(). Це використовується, наприклад, для створення синтетичних проксі або інтеграції з альтернативними фабриками об’єктів.

3. Що робить SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference()?

Відповідь: Цей метод використовується для розв’язання циклічних залежностей синглтонів. Якщо бін втягнутий у цикл і потребує обгортання в AOP-проксі (наприклад, @Transactional), getEarlyBeanReference створює ранній екземпляр проксі до завершення повної ініціалізації біна і розміщує його у 2-му рівні кешу синглтонів (earlySingletonObjects).

4. Що означає запис у логах is not eligible for getting processed by all BeanPostProcessors?

Відповідь: Це означає, що бін був створений передчасно, оскільки був прямою залежністю іншого BeanPostProcessor. Оскільки він з’явився до реєстрації повного набору системних BPP, до нього не застосувалися пост-процесори створення проксі. Рішення: використовувати @Lazy або ObjectProvider у коді BPP.


Червоні прапорці (чого категорично не можна говорити)

  • ❌ «BPP створює BeanDefinition» — ні, BeanDefinition створюється парсерами конфігурації та модифікується через BeanFactoryPostProcessor. BPP працює виключно з екземплярами класів.
  • ❌ «AOP-проксі створюються в postProcessBeforeInitialization» — проксі створюються суворо в postProcessAfterInitialization (після виклику init-методів).
  • ❌ «У BPP можна без проблем впроваджувати бізнес-сервіси через @Autowired» — це породжує баг передчасної ініціалізації і позбавляє сервіси підтримки транзакцій та аспектів.
  • ❌ «Кастомні BPP потрібно писати для кожної задачі бізнес-логіки» — для бізнес-логіки використовується Spring AOP (@Aspect). BPP застосовується переважно для інфраструктурних завдань, розробки бібліотек та стартерів.

Пов’язані теми