BeanPostProcessor: Intercepting Bean Creation to Add Behavior
The Two-Phase Callback
Every bean that Spring creates passes through every registered BeanPostProcessor twice. The creation pipeline for a single bean:
1. Instantiate (constructor invocation)
2. Populate (field and setter injection)
3. postProcessBeforeInit (all BPPs, in order)
4. Initialize (@PostConstruct, afterPropertiesSet, init-method)
5. postProcessAfterInit (all BPPs, in order)
6. Bean is ready for use
The two callbacks serve different purposes:
-
postProcessBeforeInitialization: Runs before@PostConstructandInitializingBean.afterPropertiesSet(). Used by processors that need to act on a fully injected but not yet initialized bean.CommonAnnotationBeanPostProcessoruses this phase to invoke@PostConstructmethods. -
postProcessAfterInitialization: Runs after initialization is complete. The bean is fully configured. This is where proxy creation happens.AbstractAutoProxyCreatorwraps beans in AOP proxies during this phase.
Both methods receive the bean instance and its name, and must return either the same bean or a replacement. Returning a different object replaces the bean in the context.
How @PostConstruct Works: CommonAnnotationBeanPostProcessor
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor implements BeanPostProcessor and processes JSR-250 annotations: @PostConstruct, @PreDestroy, and @Resource.
During postProcessBeforeInitialization, it inspects the bean’s class for methods annotated with @PostConstruct and invokes them via reflection:
// Simplified view of what CommonAnnotationBeanPostProcessor does:
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// Find @PostConstruct methods via metadata cache
LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass());
metadata.invokeInitMethods(bean, beanName); // Calls @PostConstruct methods
return bean;
}
This is why @PostConstruct fires before afterPropertiesSet(). The BPP processes it in phase 3 of the pipeline. afterPropertiesSet() is part of phase 4. The ordering is deterministic and designed: annotation-based lifecycle callbacks always precede interface-based ones.
If you remove CommonAnnotationBeanPostProcessor from the context (unlikely, but possible in minimal configurations), @PostConstruct methods will never execute. They are not built into the container. They are processed by a BPP.
How @Autowired Works: AutowiredAnnotationBeanPostProcessor
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor is a BeanPostProcessor (and also a MergedBeanDefinitionPostProcessor, which provides metadata caching). It processes @Autowired, @Value, and @Inject annotations.
The injection actually happens during step 2 (populate), not during the BPP callbacks. AutowiredAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor, a sub-interface that adds a postProcessProperties() callback invoked during property population:
// Simplified view:
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean,
String beanName) {
// Find @Autowired fields and methods via metadata cache
InjectionMetadata metadata = findAutowiringMetadata(beanName,
bean.getClass(), pvs);
metadata.inject(bean, beanName, pvs); // Resolves and injects dependencies
return pvs;
}
For each @Autowired field, it calls beanFactory.resolveDependency(), which triggers getBean() for the dependency (creating it if needed), and then sets the field via reflection.
This means @Autowired fields are populated before postProcessBeforeInitialization runs. By the time @PostConstruct fires, all dependencies are injected. This ordering is not accidental. It is enforced by the pipeline: populate (step 2) always precedes BPP callbacks (step 3).
Preview: AbstractAutoProxyCreator and AOP Proxies
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator is the BPP responsible for creating AOP proxies. It runs during postProcessAfterInitialization (step 5 of the bean pipeline):
// Simplified view:
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean != null) {
Object proxy = wrapIfNecessary(bean, beanName, cacheKey);
if (proxy != bean) {
// The proxy replaces the original bean in the context
return proxy;
}
}
return bean;
}
wrapIfNecessary checks whether any advisor (like @Transactional or @Cacheable advice) applies to the bean. If so, it creates a JDK dynamic proxy or CGLIB proxy and returns it. The proxy wraps the original bean and intercepts method calls.
This fires in postProcessAfterInitialization specifically so that the bean is fully initialized before being wrapped. The proxy delegates to the real bean, which must be complete. We will dissect proxy creation in detail in CH8.
The critical consequence: if a bean is instantiated before AbstractAutoProxyCreator is registered (because a BeanPostProcessor or BeanFactoryPostProcessor forced its early creation), it will not get a proxy. @Transactional methods will execute without transaction management. This is the root cause of the warning discussed in CH3.
Writing a Custom BPP: Bean Creation Audit Logger
For our SaaS backend, we want to log every bean creation with timing information. This is useful for diagnosing slow startup.
package com.saas.diagnostics;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class BeanCreationTimingPostProcessor implements BeanPostProcessor, Ordered {
private final Map<String, Long> startTimes = new ConcurrentHashMap<>();
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE; // Run first to capture total time
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
startTimes.put(beanName, System.nanoTime());
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
Long startTime = startTimes.remove(beanName);
if (startTime != null) {
long durationMs = (System.nanoTime() - startTime) / 1_000_000;
if (durationMs > 100) {
System.out.printf("SLOW BEAN: %s took %d ms to initialize [%s]%n",
beanName, durationMs, bean.getClass().getSimpleName());
}
}
return bean; // Always return the bean
}
}
Key design decisions:
Ordered.HIGHEST_PRECEDENCE: This BPP runs before all others, so the timing captures the full initialization pipeline including other BPPs.ConcurrentHashMap: Bean creation can involve nestedgetBean()calls, so map access must be thread-safe even though Spring’s default behavior is single-threaded startup.- Threshold logging: Only beans taking more than 100ms are logged, keeping output manageable.
- Always returns the bean: This is critical. See the failure mode below.
We avoid @Autowired dependencies. This BPP uses no injected services. If it needed a logger, it would use System.out or a static logger. No Spring-managed dependencies.
The Failure Mode
// BROKEN: BPP that returns null from postProcessAfterInitialization
@Component
public class ConditionalBeanFilter implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean.getClass().isAnnotationPresent(Deprecated.class)) {
return null; // BROKEN: removes the bean from the context
}
return bean;
}
}
Returning null from a BeanPostProcessor callback removes the bean from the context. The AbstractAutowireCapableBeanFactory that drives bean creation checks the return value:
// From AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization:
for (BeanPostProcessor processor : getBeanPostProcessors()) {
Object current = processor.postProcessAfterInitialization(result, beanName);
if (current == null) {
return result; // Stops processing, but result may be used inconsistently
}
result = current;
}
When a BPP returns null, subsequent BPPs do not see the bean. The bean reference stored in the singleton cache may be the last non-null value returned. Depending on the order of BPPs, the bean might be partially processed: it has some BPP enhancements but not others. Other beans that depend on it may get a stale or unproxied reference.
The behavior is especially dangerous with infrastructure beans. Returning null for a DataSource bean will cause NoSuchBeanDefinitionException in every repository that depends on it. Returning null for an internal Spring bean can crash the context during startup with cryptic errors.
The Correct Pattern
// CORRECT: Always return the bean (or a wrapper)
@Component
public class ConditionalBeanFilter implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean.getClass().isAnnotationPresent(Deprecated.class)) {
// Option 1: Log a warning but keep the bean
System.out.printf("WARNING: Deprecated bean in context: %s [%s]%n",
beanName, bean.getClass().getName());
}
return bean; // Always return the bean
}
}
If you genuinely need to exclude beans from the context, do it at the BeanFactoryPostProcessor level by removing their BeanDefinition before instantiation:
// CORRECT: Remove beans at the definition level, not the instance level
@Component
public class DeprecatedBeanRemover implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
if (!(beanFactory instanceof BeanDefinitionRegistry registry)) {
return;
}
for (String name : beanFactory.getBeanDefinitionNames()) {
var definition = beanFactory.getBeanDefinition(name);
if (definition.getBeanClassName() != null) {
try {
Class<?> beanClass = Class.forName(definition.getBeanClassName());
if (beanClass.isAnnotationPresent(Deprecated.class)) {
registry.removeBeanDefinition(name);
}
} catch (ClassNotFoundException e) {
// Class not loadable yet, skip
}
}
}
}
}
This is the correct layer of abstraction. Removing a BeanDefinition means the bean is never created. No partially-processed instances. No dangling references. Dependent beans will fail fast with NoSuchBeanDefinitionException at startup rather than silently receiving null at runtime.
The Rule
A BeanPostProcessor must always return an object from both callback methods. Return the original bean if you have no modifications. Return a proxy or wrapper if you need to add behavior. Never return null. If you need to prevent a bean from existing, operate at the BeanFactoryPostProcessor level and remove its definition before it is ever created.