Blog

BeanPostProcessor Example | Spring Boot

Key Takeaways

  • BeanPostProcessor processes bean instances; BeanFactoryPostProcessor changes the definitions used to create them.
  • The basic example traces constructor → custom before-initialization hook → @PostConstruct → after-initialization hook.
  • You can return a proxy from postProcessAfterInitialization() to intercept calls made through the Spring-managed reference.
  • Registration, processor ordering, early bean creation, and proxy boundaries determine whether your hooks actually run.

A BeanPostProcessor lets you inspect, modify, or wrap Spring-managed bean instances around initialization. This Spring BeanPostProcessor example first traces the lifecycle of a Person bean, then wraps that same bean in a proxy that intercepts sayHello(). Spring documents these callbacks as extension points for instance customization and proxy wrapping.

How Does BeanPostProcessor Work?

The two callbacks are postProcessBeforeInitialization(Object bean, String beanName) and postProcessAfterInitialization(Object bean, String beanName). Property population and dependency injection have completed before the before-initialization callback; the after-initialization callback runs after initialization callbacks.

Person constructor
  -> field/setter injection and property population
  -> before-initialization processor chain
       -> our before hook in this example
       -> @PostConstruct, invoked by Spring's annotation processor
  -> other initialization callbacks, if configured
  -> after-initialization processor chain
       -> our after hook; optionally return a proxy
  -> resulting reference supplied to dependent beans

Spring BeanPostProcessor Example: Basic Lifecycle

Set Up the Spring Boot Project

Use Java 21 and a released Spring Boot 4.x version for this example. Generate a Maven project at Spring Initializr, with package name com.example.demo. The lifecycle APIs used here also apply to Spring Boot 3.x.

For Spring Boot 3 and 4, import jakarta.annotation.PostConstruct, not javax.annotation.PostConstruct. Boot 3 introduced the Jakarta package migration.

Keep the generated Maven parent and Spring Boot plugin. Ensure the following entries are present in the existing <dependencies> section, without duplicating entries already generated. Boot's dependency management supplies their versions; spring-aop provides the proxy APIs used in the second example.

<!-- Fragment: entries inside pom.xml's existing dependencies section. -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
</dependency>
<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
</dependency>

Put each Java file below in src/main/java/com/example/demo/.

Person.java

Use this single Person class throughout both examples. The extra methods will demonstrate proxy interception and self-invocation later.

package com.example.demo;

import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;

@Component
public class Person {
    public Person() {
        System.out.println("Person constructor");
    }

    @PostConstruct
    void init() {
        System.out.println("Person @PostConstruct");
    }

    public void sayHello() {
        System.out.println("Hello!");
    }

    public void callHelloInternally() {
        sayHello();
    }
}

MyCustomBeanPostProcessor.java

This processor receives callbacks for eligible beans but prints only for Person. PriorityOrdered gives the demonstration an explicitly early position; the processor has no application-bean dependencies.

package com.example.demo;

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

@Component
public class MyCustomBeanPostProcessor
        implements BeanPostProcessor, PriorityOrdered {

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        if (bean instanceof Person) {
            System.out.println("Before initialization: " + beanName);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (bean instanceof Person) {
            System.out.println("After initialization: " + beanName);
        }
        return bean;
    }
}

DemoApplication.java

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Run and Read the Lifecycle Output

From the project directory, use the Spring Boot Maven plugin's run goal:

mvn spring-boot:run

The relevant output for this example is:

Person constructor
Before initialization: person
Person @PostConstruct
After initialization: person

This is a focused lifecycle trace, not a promise about the complete startup log or the creation order of unrelated beans. Returning bean leaves the instance unchanged.

BeanPostProcessor Proxy Example

A proxy is an object that stands between a caller and the target object. Here, the processor returns a proxy after Person has initialized, and Spring supplies that reference to Place.

Keep Person.java and DemoApplication.java unchanged. Replace the existing MyCustomBeanPostProcessor.java with the following file, then add Place.java. Don't register both processor implementations.

Replace MyCustomBeanPostProcessor.java

package com.example.demo;

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

@Component
public class MyCustomBeanPostProcessor
        implements BeanPostProcessor, PriorityOrdered {

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        if (bean instanceof Person) {
            System.out.println("Before initialization: " + beanName);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (!(bean instanceof Person)) {
            return bean;
        }

        System.out.println("After initialization: " + beanName);
        ProxyFactory factory = new ProxyFactory(bean);
        factory.setProxyTargetClass(true);
        factory.addAdvice(new LoggingInterceptor());
        return factory.getProxy();
    }

    private static class LoggingInterceptor implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            if (!invocation.getMethod().getName().equals("sayHello")
                    || invocation.getMethod().getParameterCount() != 0) {
                return invocation.proceed();
            }

            System.out.println("Before sayHello()");
            try {
                return invocation.proceed();
            } finally {
                System.out.println("After sayHello()");
            }
        }
    }
}

ProxyFactory creates the wrapper, and addAdvice() attaches the interceptor. Calling proceed() continues the invocation chain toward the target. The finally block prints the after message even if the target throws; it doesn't swallow the exception.

Add Place.java

package com.example.demo;

import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;

@Component
public class Place {
    private final Person person;

    public Place(Person person) {
        this.person = person;
    }

    @PostConstruct
    void init() {
        System.out.println("Place @PostConstruct");
        System.out.println("External call:");
        person.sayHello();

        System.out.println("Self-invocation:");
        person.callHelloInternally();
    }
}

Spring uses the single constructor for injection without requiring @Autowired. If that wiring is unfamiliar, review the Spring dependency injection guide.

Follow the Proxy Call Flow

Place -> Person proxy -> interceptor: before
                           -> target Person.sayHello()
                      <- interceptor: after
      <- return to caller

Target Person.callHelloInternally() -> this.sayHello()
                                      (no proxy crossing)

Restart the application. Its relevant output is:

Person constructor
Before initialization: person
Person @PostConstruct
After initialization: person
Place @PostConstruct
External call:
Before sayHello()
Hello!
After sayHello()
Self-invocation:
Hello!

The external sayHello() call crosses the proxy and is intercepted. The call to callHelloInternally() also reaches the proxy, but our interceptor deliberately passes that method through; its nested sayHello() call runs directly on the target and bypasses advice.

Understand Proxy Types and Limitations

new ProxyFactory(target) discovers the target's interfaces. Without forcing class-based proxying, interface targets normally use JDK dynamic proxies, while targets without interfaces use class-based proxies. This example explicitly calls setProxyTargetClass(true) to produce a class-based proxy compatible with injection as Person.

  • JDK dynamic proxies expose interfaces: inject the service interface rather than assuming the proxy is assignable to its implementation class.
  • Class-based proxies use subclassing: final classes cannot be proxied this way, and final or private methods cannot be advised.
  • Self-invocation bypasses proxy advice: move the advised operation to another bean when you need an internal call to cross a proxy boundary.

These are proxy-based AOP limitations, not problems that a different lifecycle annotation fixes.

Advertisement

BeanPostProcessor vs PostConstruct

Use @PostConstruct when a bean needs to initialize its own state after dependency injection. Use BeanPostProcessor when infrastructure needs to inspect or transform selected bean instances without putting that logic into every target class.

In this example, Person's own @PostConstruct runs before the processor returns its proxy. Place's @PostConstruct can call the already-injected Person proxy because the dependency has completed initialization in this acyclic bean graph. Don't expect a bean's own initialization method to run through a proxy created afterward.

BeanFactoryPostProcessor vs BeanPostProcessor

A bean definition is the container's recipe for creating an object. A BeanFactoryPostProcessor changes that recipe; a BeanPostProcessor works on the resulting instance.

Extension pointOperates onTypical purpose
BeanFactoryPostProcessorBean-definition metadata before ordinary bean creationChange configured property values or other definition settings
BeanPostProcessorCreated bean instances around initializationInspect instances, apply custom conventions, or return wrappers

Don't call getBean() inside a factory post-processor to modify application instances. That can create beans prematurely and bypass normal post-processing.

Why Implement BeanPostProcessor?

Reach for this interface when you're building container-level integration: for example, detecting a marker interface on selected beans and applying a shared initialization convention. If only one class needs setup, prefer a constructor or @PostConstruct.

For persistence work, start with the ordinary repository and service boundaries described in the Spring Data JPA guide, rather than introducing custom bean wrapping by default.

Ordering, Return Values, and Early Creation

Choose Processor Ordering Deliberately

Autodetected processors support PriorityOrdered and Ordered. Priority-ordered processors take precedence over ordinary ordered processors; within an ordering group, lower numeric values have higher priority. @Order alone does not control BeanPostProcessor ordering. Programmatically registered processors instead follow registration order.

Ordering matters when processors transform the same bean: a later processor receives the object returned by an earlier one, which may already be a proxy. Don't copy this tutorial's highest-priority setting into production without identifying the processors you must run before or after.

Return the Original or Wrapped Bean

Normally, return bean when you make no change, or return the wrapper you want Spring to expose. Returning null stops the remaining processors in that phase; it does not remove the bean. An accidental null return can therefore skip another processor's initialization work.

Avoid Pulling Application Beans into Early Startup

Processors are discovered and instantiated during a special early startup phase. They aren't processed like ordinary application beans: processors and their directly referenced dependencies are not eligible for normal AOP auto-proxying, and prematurely created beans may miss other processors.

Keep your processor dependency-light. Injecting a business service into it can instantiate that service before the complete processor chain is available. Watch for startup warnings containing not eligible for getting processed by all BeanPostProcessors.

Advertisement

BeanPostProcessor Not Getting Called?

  1. Check registration and scanning. @Component works only if the processor is in the active scan scope. In this project, keep it under com.example.demo, alongside or below DemoApplication. Boot recommends placing the application class in a root package above your components; see its package-structure guidance.
  2. Check how the target was created. An ordinary new Person() call does not send that object through Spring's bean lifecycle.
  3. Check context boundaries. A processor applies to beans in its own container, not automatically to beans in a parent or child context.
  4. Check early creation. Remove unnecessary processor dependencies and investigate eligibility warnings.
  5. Check the chain and your filter. An earlier null return can stop a phase. Your hook may also run without printing because your type or bean-name condition no longer matches a wrapped object.

As an alternative to component scanning, declare the processor with a static factory method. Remove @Component from the processor first, and place this configuration in the scanned package:

package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ProcessorConfiguration {
    @Bean
    public static MyCustomBeanPostProcessor myCustomBeanPostProcessor() {
        return new MyCustomBeanPostProcessor();
    }
}

The @Bean method should return the concrete processor type or BeanPostProcessor, not Object, so Spring can identify it early. A static, dependency-free method avoids eagerly creating the configuration instance and application dependencies just to register the processor.

Production Checklist

  • Filter target beans narrowly and keep post-processing free of network calls or expensive application work.
  • Keep interceptor state local to each invocation; preserve return values and exceptions.
  • Use an application logger rather than tutorial console output, and redact sensitive arguments or bean state.
  • Test calls through the Spring-managed reference, target exceptions, self-invocation, and interaction with existing proxies.

FAQ

What is a BeanPostProcessor in Spring?

A BeanPostProcessor is a container extension that can inspect, modify, or wrap Spring-managed bean instances around initialization. Returning a proxy lets callers use added behavior without changing the target class.

What is the order of BeanPostProcessor and @PostConstruct?

In this example, the order is constructor, custom before-initialization hook, @PostConstruct, then after-initialization hook. Spring invokes @PostConstruct inside the before-initialization processor chain, so don't assume every processor's before hook runs before it.

How do I register a BeanPostProcessor in Spring Boot?

Register the implementation as a scanned @Component, or return it from a static @Bean method in a discovered configuration class. Give that factory method a BeanPostProcessor or concrete processor return type.

Why is my BeanPostProcessor not called?

Check registration and scan scope first, then confirm the target is Spring-managed and belongs to the same application context. Also inspect early-creation warnings, earlier processors returning null, and filters that might reject an already-wrapped bean.

What is the difference between BeanPostProcessor and BeanFactoryPostProcessor?

BeanFactoryPostProcessor changes bean-definition metadata before ordinary beans are created. BeanPostProcessor works on bean instances and can return a wrapped instance, including a proxy.

Further Reading

Advertisement

Written by Sam Erickson

Pen name of a software engineer with 20+ years in the industry, from Fortune 500 companies to startups. Newer articles are researched and drafted with AI tools I direct and fact-checked against their sources. Author of Mastering the Tech Interview. More about me

Advertisement