BeanPostProcessor & BeanFactoryPostProcessor

작성자

카테고리:

← 피드로
DEV Community · Ankit Verma · 2026-08-17 개발(SW)

Two hooks with nearly the same name

Spring’s container builds your objects so you don’t have to. You mark a class, and a fully-wired object — a bean — appears, ready to use. Most days you never think about how that happens.

But Spring itself has to think about it constantly. To turn a ${db.url} placeholder into a real value, to satisfy an @Autowired field, to wrap a service in a transaction proxy — Spring needs a way to reach into its own build process and change what comes out. It does this through two official extension points, and their names are the single most confused pair in the framework: BeanFactoryPostProcessor and BeanPostProcessor.

They sound like synonyms. They are not. The difference is when they run and what they are allowed to touch — and once you see that, the confusion disappears for good. So we’ll build the timeline first, then meet each hook in its place.

The container works in two passes

When the context starts, it does not build your beans in one motion. It works in two distinct passes.

First, it reads every source of configuration — your @Configuration classes, component scans, any XML — and turns each one into a bean definition: a plain metadata object describing how a bean should be built. Its class, its scope, its constructor arguments, its property values. A definition is a blueprint. No object exists yet; at this point the container is just holding a stack of blueprints.

Second, once all the blueprints are collected, the container starts instantiating — walking the definitions and actually building live objects from them, wiring each one’s dependencies, running its init code.

Hold onto that split: blueprints first, buildings second. Each of the two post-processors hooks into exactly one of those passes. That is the whole distinction.

BeanFactoryPostProcessor — editing the blueprints

The first hook runs at the seam between the two passes: after every bean definition is loaded, but before a single bean is instantiated. It is called BeanFactoryPostProcessor, and it exists to let you edit the blueprints while they are still just blueprints.

The interface has one method:

public interface BeanFactoryPostProcessor {
    void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory);
}

Enter fullscreen mode Exit fullscreen mode

That beanFactory argument is the container holding all the definitions. You can walk it, read any bean’s definition, and change it before that bean is ever built.

Here is a small one that forces every bean to be lazily initialised:

@Component
public class LazyEverything implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
        for (String name : bf.getBeanDefinitionNames()) {
            bf.getBeanDefinition(name).setLazyInit(true);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

We loop over every definition and flip its lazyInit flag. Because this runs before instantiation, the change lands in time to matter — when the container later builds these beans, it reads the modified blueprint. We changed the plan, not the product.

You have almost certainly relied on a BeanFactoryPostProcessor without noticing. Whenever a ${...} placeholder appears in your configuration — a database URL, a port number — something has to replace it with a real value from your properties files. That something is PropertySourcesPlaceholderConfigurer, a BeanFactoryPostProcessor Spring registers for you. It runs in this between-the-passes window and wires up placeholder resolution before any bean that needs a value is built.

There is a close cousin worth naming once: BeanDefinitionRegistryPostProcessor. It runs a moment earlier and can add whole new definitions, not just tweak existing ones — it is how your @Configuration classes get turned into bean definitions in the first place. Same idea, one step further up the chain.

The rule to carry: a BeanFactoryPostProcessor sees definitions, never instances. The moment you find yourself wanting the actual built object, you have reached for the wrong hook — which is the other one.

BeanPostProcessor — intercepting each finished bean

The second hook runs in the second pass, during instantiation. After the container builds a bean and injects its dependencies, and around the moment it runs the bean’s init code, every BeanPostProcessor gets a turn with that live object.

This interface has two methods:

public interface BeanPostProcessor {
    Object postProcessBeforeInitialization(Object bean, String beanName);
    Object postProcessAfterInitialization(Object bean, String beanName);
}

Enter fullscreen mode Exit fullscreen mode

Recall the init step of a bean’s life — the callback that runs once a bean is fully wired, like a method marked @PostConstruct. These two methods bracket that step: before runs just before your init code, after runs just after. Every bean in the container passes through both, one bean at a time.

These callbacks are not exotic edge cases — Spring implements its own annotations with them. The @Autowired field injection and the @PostConstruct call you use every day are each carried out by a BeanPostProcessor that Spring registers on your behalf. The hook is not a rarely-used escape hatch; it is the machinery the framework itself runs on.

A trivial logger shows the bare shape:

@Component
public class InitLogger implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String name) {
        System.out.println("Finished building: " + name);
        return bean;
    }
}

Enter fullscreen mode Exit fullscreen mode

Notice the method returns a bean. That return value is the detail that makes this hook powerful — and it is worth slowing down on.

The return value is a swap point

Whatever object you return from postProcessAfterInitialization is what the container uses from then on. Return the same bean, and nothing changes. Return a different object that wraps the original, and you have quietly substituted the bean the whole application will see.

That is exactly how Spring gives you proxies. When a method carries @Transactional, the real bean is built normally — then a BeanPostProcessor catches it in the after-init step and returns a proxy: a stand-in object of the same type that opens a transaction, calls through to your real bean, and commits. Nobody who injected that bean can tell the difference. They asked the container for a PaymentService and got something that looks and types like one.

// what you wrote
@Transactional
public void transfer(...) { ... }

// what the container actually hands out
PaymentService proxy = wrapInTransaction(realService);

Enter fullscreen mode Exit fullscreen mode

The same mechanism powers Spring AOP, caching, and security checks — any feature that needs to run code around your methods. All of it rides on that one move: a BeanPostProcessor returning a wrapper in the after-init step. Once you see the swap, a large amount of Spring’s “magic” stops being magic.

Why this order causes a real trap

Now the gotcha, and it follows straight from the timeline. For a BeanPostProcessor to wrap other beans, it obviously has to exist before those beans are built. So Spring instantiates all BeanPostProcessors first, up front, before it starts on your ordinary beans.

That early instantiation is a trap waiting to spring. Suppose your post-processor needs a dependency:

@Component
public class AuditingProcessor implements BeanPostProcessor {
    @Autowired
    private PaymentService paymentService;   // trouble
    // ...
}

Enter fullscreen mode Exit fullscreen mode

To build this post-processor, Spring must first build PaymentService to inject it. But this is happening in the early phase — before all the other post-processors are registered. So PaymentService gets created too soon and skips the post-processors that are not set up yet. If one of them was going to give it a transaction proxy, that proxy never happens. Your @Transactional silently does nothing.

Spring even warns about it in the log, in a line worth recognising:

Bean ‘paymentService’ is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

The fix is to keep post-processors lean. They should depend on as little as possible, and never pull in the very beans they are meant to process. A post-processor is infrastructure, built before the world exists — treat it that way.

When many hooks compete: ordering

Because these hooks decide the shape of every bean, the order they run in matters. Two post-processors could both want to wrap the same object; which one wins? Spring lets a hook declare its place by implementing Ordered (or PriorityOrdered for the ones that must run first of all), returning a number — lower runs earlier.

@Component
public class FirstInLine implements BeanPostProcessor, Ordered {
    @Override public int getOrder() { return 0; }   // runs before higher numbers
    // ...
}

Enter fullscreen mode Exit fullscreen mode

The same ordering applies to BeanFactoryPostProcessors among themselves. It rarely matters for hand-written hooks, but when two of them interact — two things both rewriting definitions, or two proxies stacking on one bean — Ordered is how you make the outcome deliberate instead of accidental.

The one mental model

Strip it all back and it is a single picture. The container runs in two passes: collect the blueprints, then construct the buildings.

  • A BeanFactoryPostProcessor works on the blueprints, between the passes. It sees bean definitions and can rewrite them before anything is built. Placeholder resolution lives here.
  • A BeanPostProcessor works on the buildings, during the second pass. It sees each live bean as it is initialised and can wrap or replace it. Proxies — transactions, AOP, caching — live here.

Both are called “post-processor” because both are Spring’s way of stepping into its own construction line. Once you know which pass a hook belongs to, you always know what it is allowed to touch — a definition or an object — and every confusingly-named “…PostProcessor” in the framework falls neatly into one of the two.

원문에서 계속 ↗