Skip to main content
spring internals

BeanFactoryPostProcessor: Rewriting Bean Definitions Before Instantiation

6 min read Chapter 8 of 78

The Interface

org.springframework.beans.factory.config.BeanFactoryPostProcessor has a single method:

void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
    throws BeansException;

When this method executes, the BeanFactory contains BeanDefinition objects loaded from @Configuration classes, component scanning, and XML (if you still use it). No application bean has been instantiated. You are operating on blueprints, not objects.

The ConfigurableListableBeanFactory parameter gives you full access to every BeanDefinition in the registry. You can read them, modify them, or (if you cast to BeanDefinitionRegistry) add new ones.

PropertySourcesPlaceholderConfigurer: The BFPP You Already Use

Every Spring Boot application uses org.springframework.context.support.PropertySourcesPlaceholderConfigurer whether you realize it or not. Spring Boot auto-registers it through PropertyPlaceholderAutoConfiguration.

This BFPP iterates through every BeanDefinition in the factory, inspects property values and constructor argument values, and replaces ${...} placeholders with resolved values from PropertySource instances (application.properties, environment variables, system properties).

// What you write:
@Value("${db.url}")
private String dbUrl;

// What PropertySourcesPlaceholderConfigurer does at BFPP time:
// 1. Finds the BeanDefinition containing a @Value("${db.url}") metadata entry
// 2. Resolves "db.url" from PropertySources
// 3. Replaces the placeholder in the BeanDefinition metadata
// 4. When the bean is later instantiated, the resolved value is injected

This happens before any bean exists. The BFPP modifies the definition so that by the time the bean is created, the placeholder is already resolved. This is why @Value works: the resolution is not magic at injection time. It is a BFPP rewriting metadata during step 5 of refresh().

Writing a Custom BFPP: Multi-Tenant DataSource Registration

Our SaaS backend serves multiple tenants. Each tenant has its own database. Instead of hardcoding DataSource beans, we read tenant configuration and register one DataSource BeanDefinition per tenant dynamically.

package com.saas.config;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import com.zaxxer.hikari.HikariDataSource;

@Component
public class TenantDataSourceRegistrar implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {

        if (!(beanFactory instanceof BeanDefinitionRegistry registry)) {
            throw new IllegalStateException(
                "BeanFactory is not a BeanDefinitionRegistry");
        }

        Environment env = beanFactory.getBean(Environment.class);
        String[] tenants = env.getRequiredProperty("saas.tenants", String[].class);

        for (String tenant : tenants) {
            String beanName = tenant + "DataSource";

            var builder = BeanDefinitionBuilder
                .genericBeanDefinition(HikariDataSource.class)
                .addPropertyValue("jdbcUrl",
                    env.getRequiredProperty("saas.tenant." + tenant + ".db.url"))
                .addPropertyValue("username",
                    env.getRequiredProperty("saas.tenant." + tenant + ".db.username"))
                .addPropertyValue("password",
                    env.getRequiredProperty("saas.tenant." + tenant + ".db.password"))
                .addPropertyValue("maximumPoolSize", 10)
                .setDestroyMethodName("close");

            registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
        }
    }
}

The corresponding configuration:

# application.yml
saas:
  tenants: acme,globex,initech
  tenant:
    acme:
      db:
        url: jdbc:postgresql://db-acme:5432/acme
        username: acme_user
        password: ${ACME_DB_PASSWORD}
    globex:
      db:
        url: jdbc:postgresql://db-globex:5432/globex
        username: globex_user
        password: ${GLOBEX_DB_PASSWORD}
    initech:
      db:
        url: jdbc:postgresql://db-initech:5432/initech
        username: initech_user
        password: ${INITECH_DB_PASSWORD}

After this BFPP runs, the BeanFactory contains three BeanDefinition objects: acmeDataSource, globexDataSource, initechDataSource. None of them exist as objects yet. They are definitions waiting to be instantiated during step 11.

Note that we access Environment through beanFactory.getBean(Environment.class). The Environment is one of the few beans that exists this early because it is created during prepareRefresh() (step 1). It is safe to access. Regular application beans are not.

Modifying Existing Bean Definitions

You can also modify definitions that already exist. For example, forcing all DataSource beans to prototype scope (useful for connection-per-request patterns in testing):

@Component
public class DataSourceScopeModifier implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {

        for (String name : beanFactory.getBeanDefinitionNames()) {
            var definition = beanFactory.getBeanDefinition(name);

            if (definition.getBeanClassName() != null
                    && definition.getBeanClassName().contains("DataSource")) {
                definition.setScope("prototype");
            }
        }
    }
}

You can modify any aspect of a BeanDefinition:

// Change the implementation class
definition.setBeanClassName("com.saas.CustomDataSource");

// Add a qualifier
definition.addQualifier(
    new AutowireCandidateQualifier(Qualifier.class, "primary"));

// Set lazy initialization
definition.setLazyInit(true);

// Add a depends-on relationship
definition.setDependsOn("configService", "migrationRunner");

// Change the factory method
definition.setFactoryMethodName("createFromConfig");

Every one of these modifications takes effect before the bean is instantiated. You are editing the blueprint, not the building.

BeanDefinitionRegistryPostProcessor: Adding Definitions Earlier

If you need to register bean definitions that other BFPPs should process (for example, definitions that contain ${...} placeholders that PropertySourcesPlaceholderConfigurer should resolve), implement org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor instead:

@Component
public class TenantDataSourceRegistrar
        implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
            throws BeansException {
        // Register definitions here. They will be visible to subsequent BFPPs.
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {
        // Optional: modify definitions after all registrations are complete.
    }
}

postProcessBeanDefinitionRegistry() fires before postProcessBeanFactory(). This two-phase design means you can register definitions in the first method and know they will be visible to all BFPPs (including PropertySourcesPlaceholderConfigurer) when postProcessBeanFactory() runs.

org.springframework.context.annotation.ConfigurationClassPostProcessor uses exactly this pattern. It implements BeanDefinitionRegistryPostProcessor and registers all @Bean method definitions in postProcessBeanDefinitionRegistry(). By the time PropertySourcesPlaceholderConfigurer.postProcessBeanFactory() runs, those definitions exist and their ${...} placeholders can be resolved.

The Failure Mode

// BROKEN: BFPP that uses @Autowired
@Component
public class TenantDataSourceRegistrar implements BeanFactoryPostProcessor {

    @Autowired
    private TenantConfigService tenantConfigService; // PROBLEM

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {

        for (TenantConfig config : tenantConfigService.loadTenants()) {
            // Register DataSource definitions...
        }
    }
}

This forces Spring to instantiate TenantConfigService during step 5, before BPPs are registered (step 6). TenantConfigService will not be processed by any BeanPostProcessor. If it has @Autowired fields, they will be null. If it is @Transactional, it will not get a proxy. If it has @PostConstruct, it will not fire.

Spring will log:

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

This warning is easy to miss in a busy log. The consequence is silent: transactions do not work, injection is incomplete, lifecycle callbacks are skipped.

The Correct Pattern

// CORRECT: Use Environment or read configuration directly
@Component
public class TenantDataSourceRegistrar implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {

        // Environment is safe to access: it exists before BFPPs run
        Environment env = beanFactory.getBean(Environment.class);
        String[] tenants = env.getRequiredProperty("saas.tenants", String[].class);

        if (!(beanFactory instanceof BeanDefinitionRegistry registry)) {
            return;
        }

        for (String tenant : tenants) {
            String url = env.getRequiredProperty(
                "saas.tenant." + tenant + ".db.url");

            var builder = BeanDefinitionBuilder
                .genericBeanDefinition(HikariDataSource.class)
                .addPropertyValue("jdbcUrl", url)
                .addPropertyValue("username",
                    env.getRequiredProperty(
                        "saas.tenant." + tenant + ".db.username"))
                .addPropertyValue("password",
                    env.getRequiredProperty(
                        "saas.tenant." + tenant + ".db.password"));

            registry.registerBeanDefinition(
                tenant + "DataSource", builder.getBeanDefinition());
        }
    }
}

The rule is absolute: a BeanFactoryPostProcessor must not depend on application beans. It operates on metadata. Read configuration from Environment, PropertySource, files, or environment variables. Never from service beans. If you need complex configuration logic, put it in a static method or a plain utility class that does not participate in the Spring lifecycle.

The beans you register in a BFPP will be fully processed by all BPPs when they are eventually instantiated during step 11. That is the contract. Definitions first, instances later.