Skip to main content
spring internals

BeanFactoryPostProcessor and BeanPostProcessor: The Two Extension Points That Drive Everything Else

8 min read Chapter 7 of 78

The Two Extension Points

Spring’s container is not a static box. It is a pipeline with precisely two extension points that third-party code (and Spring itself) can plug into. Every Spring feature you use, from @Autowired injection to @Transactional proxies to property placeholder resolution, is implemented through one of these two interfaces:

  • org.springframework.beans.factory.config.BeanFactoryPostProcessor operates on bean definitions (metadata). It fires before any bean instance exists.
  • org.springframework.beans.factory.config.BeanPostProcessor operates on bean instances. It fires as each bean is created.

This is the single most important distinction in the Spring container. Confusing the two causes bugs that are difficult to diagnose because they manifest as missing dependencies, unproxied beans, or silent misconfiguration.

BeanPostProcessor lifecycle showing before and after initialization intervention points in the bean creation pipeline

Where They Fire in the Lifecycle

Recall from CH2 that AbstractApplicationContext.refresh() executes a defined sequence of steps. The two extension points occupy different positions in that sequence:

refresh() {
    // Step 1: prepareRefresh()
    // Step 2: obtainFreshBeanFactory()
    // Step 3: prepareBeanFactory()
    // Step 4: postProcessBeanFactory()        // Template method for subclasses
    // Step 5: invokeBeanFactoryPostProcessors() // <-- BFPP fires here
    // Step 6: registerBeanPostProcessors()      // BPP instances are registered (not invoked)
    // ...
    // Step 11: finishBeanFactoryInitialization() // <-- BPP fires here (per bean)
    // Step 12: finishRefresh()
}

Step 5 calls org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(). This method instantiates all BeanFactoryPostProcessor beans and invokes them. At this point, the BeanFactory contains BeanDefinition objects but no application beans. The BFPPs read, modify, or add definitions.

Step 6 instantiates all BeanPostProcessor beans and registers them with the factory. They do not fire yet.

Step 11 calls DefaultListableBeanFactory.preInstantiateSingletons(), which iterates through every non-lazy singleton BeanDefinition and calls getBean(). For each bean, the creation pipeline invokes every registered BeanPostProcessor at two points: before initialization (postProcessBeforeInitialization) and after initialization (postProcessAfterInitialization).

BeanFactoryPostProcessor: Modifying the Blueprint

A BeanFactoryPostProcessor receives the entire ConfigurableListableBeanFactory and can do anything with it: read definitions, modify definitions, register new definitions.

@FunctionalInterface
public interface BeanFactoryPostProcessor {
    void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
        throws BeansException;
}

The most important BFPP in Spring is org.springframework.context.annotation.ConfigurationClassPostProcessor. It implements the specialized sub-interface BeanDefinitionRegistryPostProcessor, which adds a method that fires even earlier:

public interface BeanDefinitionRegistryPostProcessor
    extends BeanFactoryPostProcessor {
    void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
        throws BeansException;
}

ConfigurationClassPostProcessor is responsible for processing @Configuration, @Component, @Import, and @ComponentScan annotations. It reads those annotations from existing BeanDefinition metadata, discovers new classes, and registers their BeanDefinition objects into the registry. Without this single class, annotation-based configuration would not exist.

Another critical BFPP is org.springframework.context.support.PropertySourcesPlaceholderConfigurer. It resolves ${...} placeholders in bean definition property values by reading from PropertySource instances. It modifies BeanDefinition metadata, replacing placeholder strings with resolved values before any bean is instantiated.

What You Can Do in a BFPP

  • Change a bean’s scope from singleton to prototype
  • Add or remove property values on a BeanDefinition
  • Register entirely new BeanDefinition objects
  • Remove bean definitions from the registry
  • Read configuration from external sources and translate it into bean definitions

What You Cannot Do in a BFPP

You cannot safely use @Autowired or depend on other application beans. When BFPPs execute, those beans do not exist yet. If your BFPP forces early instantiation of a bean (by calling beanFactory.getBean()), that bean will miss processing by other BFPPs that have not yet run. This is a source of subtle bugs.

BeanPostProcessor: Modifying the Instance

A BeanPostProcessor intercepts each bean as it is created. It receives the raw bean instance and its name, and returns either the same instance or a replacement (typically a proxy).

public interface BeanPostProcessor {
    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName)
        throws BeansException {
        return bean;
    }

    @Nullable
    default Object postProcessAfterInitialization(Object bean, String beanName)
        throws BeansException {
        return bean;
    }
}

The creation pipeline for a single bean looks like this:

1. Instantiate (constructor call)
2. Populate properties (dependency injection via setters/fields)
3. BeanPostProcessor.postProcessBeforeInitialization()  // for ALL registered BPPs
4. Initialize (@PostConstruct, InitializingBean.afterPropertiesSet(), init-method)
5. BeanPostProcessor.postProcessAfterInitialization()   // for ALL registered BPPs

Spring’s own features are implemented as BPPs:

BeanPostProcessorWhat It Does
AutowiredAnnotationBeanPostProcessorProcesses @Autowired and @Value injection
CommonAnnotationBeanPostProcessorProcesses @PostConstruct, @PreDestroy, @Resource
AbstractAutoProxyCreator (subclasses)Creates AOP proxies for @Transactional, @Async, @Cacheable
ScheduledAnnotationBeanPostProcessorProcesses @Scheduled methods
AsyncAnnotationBeanPostProcessorProcesses @Async methods

This is not a plugin system bolted onto Spring. This is Spring. Remove AutowiredAnnotationBeanPostProcessor and @Autowired stops working. Remove CommonAnnotationBeanPostProcessor and @PostConstruct stops firing.

Ordering

When multiple BFPPs or BPPs exist, ordering determines which runs first. Spring uses three mechanisms, checked in this priority:

  1. PriorityOrdered interface: Highest priority. These run before any Ordered processors.
  2. Ordered interface: Second tier. getOrder() returns an int; lower values run first.
  3. @Order annotation: Equivalent to implementing Ordered.
  4. No ordering: Runs last, in undefined order.

ConfigurationClassPostProcessor implements PriorityOrdered with Ordered.LOWEST_PRECEDENCE because it needs to run early but after any custom BeanDefinitionRegistryPostProcessor that might register @Configuration classes programmatically.

AutowiredAnnotationBeanPostProcessor also implements PriorityOrdered. The ordering ensures that dependency injection is complete before other BPPs (like proxy creators) see the bean.

PostProcessorRegistrationDelegate sorts processors into four groups and invokes them in this order:

  1. PriorityOrdered BFPPs/BPPs (sorted by order value)
  2. Ordered BFPPs/BPPs (sorted by order value)
  3. Regular BFPPs/BPPs (no ordering)
  4. Internal BFPPs/BPPs (framework infrastructure, like MergedBeanDefinitionPostProcessor)

BeanDefinitionRegistryPostProcessor: The Specialized BFPP

BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor and adds postProcessBeanDefinitionRegistry(). The distinction matters:

  • postProcessBeanDefinitionRegistry() fires first. It receives a BeanDefinitionRegistry and is specifically for adding or removing bean definitions.
  • postProcessBeanFactory() fires second. It receives the full ConfigurableListableBeanFactory and is for modifying existing definitions.

ConfigurationClassPostProcessor uses postProcessBeanDefinitionRegistry() to scan @Configuration classes and register their @Bean method definitions. It uses postProcessBeanFactory() to enhance @Configuration classes with CGLIB proxies so that @Bean method calls between methods return the same singleton instance.

The Failure Mode

Here is the most common mistake developers make with these extension points:

// BROKEN: BeanPostProcessor that depends on a service bean
@Component
public class AuditBeanPostProcessor implements BeanPostProcessor {

    @Autowired
    private AuditService auditService; // This forces early instantiation of AuditService

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        auditService.log("Bean created: " + beanName);
        return bean;
    }
}

This compiles and may appear to work, but it causes a cascade of problems. BeanPostProcessor beans are instantiated during step 6 of refresh(), before regular beans. When Spring creates AuditBeanPostProcessor, it must also create AuditService and all of its dependencies. Those beans are instantiated before all BPPs are registered, which means they miss processing by AuditBeanPostProcessor itself and by any BPP registered after it.

Spring logs a warning for this:

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

If AuditService is @Transactional, it will not get a proxy. Transactions will silently fail.

The Correct Pattern

// CORRECT: BeanPostProcessor uses BeanFactory for lazy resolution
@Component
public class AuditBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {

    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // Resolve lazily, after all BPPs are registered
        AuditService auditService = beanFactory.getBean(AuditService.class);
        auditService.log("Bean created: " + beanName);
        return bean;
    }
}

By implementing BeanFactoryAware instead of using @Autowired, the BPP avoids forcing early instantiation. The AuditService is resolved lazily during step 11, when postProcessAfterInitialization is called for each regular bean. At that point, AuditService itself will have been fully processed by all BPPs, including proxy creators.

Alternatively, use ObjectProvider<AuditService> as a constructor parameter. Spring resolves ObjectProvider without instantiating the target bean, deferring resolution until .getObject() is called.

Summary

AspectBeanFactoryPostProcessorBeanPostProcessor
Operates onBeanDefinition (metadata)Bean instance (object)
When it firesStep 5, before any bean existsStep 11, as each bean is created
Can modifyScope, properties, qualifiers, classThe instance itself (wrap, replace, modify)
Can add beansYes (via BeanDefinitionRegistryPostProcessor)No
Key Spring implConfigurationClassPostProcessorAutowiredAnnotationBeanPostProcessor
Common mistakeUsing @Autowired (beans don’t exist)Returning null (removes bean from context)

These two interfaces are the backbone of Spring’s extensibility. Every feature Spring provides, from property resolution to dependency injection to AOP proxies, is implemented through one of them. Understanding where they fire and what they operate on is the foundation for debugging any Spring application.