Why place resource-opening or dependency-validation logic in @PostConstruct rather than the constructor, and how does this interact with AOP proxies and post-processors?
answer
- Constructor: field/setter deps still null
- @PostConstruct runs after injection, before proxy
- AOP proxy created in postProcessAfterInitialization
- self-invoked @Transactional/@Async in init = unadvised
- need advised startup work → ApplicationRunner / ContextRefreshedEvent
basics
~10 sIn the constructor, field/setter-injected dependencies aren't set yet, and the bean isn't fully post-processed. @PostConstruct runs after injection, so collaborators are available. But it runs before AOP proxying, so self-invocation there isn't advised.
solid answer
~50 sThe constructor executes before Spring has finished wiring the bean: with field or setter injection the collaborators are still null, and aware-callbacks and post-processors haven't run. @PostConstruct fires during postProcessBeforeInitialization, after all injection and aware-callbacks, so it's the correct place to validate that required dependencies are present and to open resources that depend on them. Two architectural nuances matter. First, with constructor injection the dependencies ARE available in the constructor, which is why constructor injection is preferred — but heavy work still belongs in @PostConstruct to keep construction cheap and side-effect-free and to respect the container's initialization ordering. Second, @PostConstruct runs on the raw target bean before AOP proxy creation (which typically happens in postProcessAfterInitialization), so calling your own @Transactional/@Async/@Cacheable method from within @PostConstruct bypasses the proxy and the advice won't apply. If you need advised behavior at startup, use ApplicationRunner, an ApplicationContext event, or SmartInitializingSingleton instead.
code
java · 27 lines@Service
class ReportService {
private final ReportRepo repo;
ReportService(ReportRepo repo) { this.repo = repo; } // ctor injection: repo available
@PostConstruct
void init() {
// Good: dependency ready, do startup validation / cache warming here.
Assert.notNull(repo, "repo required");
// BAD: this @Transactional call bypasses the proxy — no transaction here!
// preloadInTransaction();
}
@Transactional
void preloadInTransaction() { /* ... */ }
}
// For advised startup work, defer past context refresh:
@Component
class Preloader implements ApplicationRunner {
private final ReportService svc;
Preloader(ReportService svc) { this.svc = svc; }
@Override public void run(ApplicationArguments args) {
svc.preloadInTransaction(); // goes through the proxy -> transactional
}
}go deeper
Know @PostConstruct runs after injection so dependencies are available there, unlike (field-injected) constructor.
Explain the injection-timing reason and use @PostConstruct for validation/resource setup.
Explain the constructor-injection nuance and keep construction cheap/side-effect-free.
Reason about proxy creation timing, why self-invoked advice fails at init, and the correct deferral mechanisms (ApplicationRunner, events, SmartInitializingSingleton).
**Setup: the creation sequence.** For each bean Spring roughly does: instantiate (constructor) → populate properties (field/setter injection) → invoke `*Aware` callbacks (`BeanNameAware`, `ApplicationContextAware`, …) → `BeanPostProcessor.postProcessBeforeInitialization` (**`@PostConstruct` fires here**) → `InitializingBean.afterPropertiesSet()` → custom init method → `BeanPostProcessor.postProcessAfterInitialization` (**AOP proxy usually created here**) → bean placed in the singleton registry, ready. **Why not the constructor?** - With **field injection** (`@Autowired` on a field) or **setter injection**, the fields are still `null` when the constructor body runs — Spring can't set them until *after* the instance exists. Opening a connection that needs an injected `DataSource` in the constructor NPEs. - Aware-callbacks and other post-processing haven't happened, so the bean isn't in its final, usable state. - The constructor should stay cheap and deterministic; doing I/O there couples object *construction* to *environment readiness* and hurts testability. `@PostConstruct` runs *after* injection and aware-callbacks, so all collaborators are guaranteed present — the right home for validation (`Assert.notNull(...)`), cache warming, opening pools, registering listeners, etc. **The constructor-injection subtlety.** With **constructor injection** the dependencies are passed as arguments, so they *are* available inside the constructor — this is a real advantage and part of why constructor injection is the recommended style (immutability, guaranteed non-null, easy testing). Even so, keep *behavioral* startup work (I/O, thread spawning, expensive computation) in `@PostConstruct`, because (a) it keeps construction side-effect-free, (b) it participates in the container's ordering and error handling, and (c) throwing from a constructor vs. an init callback gives different diagnostics. **The AOP / proxy gotcha (the principal-level point).** `@PostConstruct` executes in `postProcessBeforeInitialization`, but the AOP auto-proxy creator (e.g. for `@Transactional`, `@Async`, `@Cacheable`, `@Retryable`) wraps the bean in `postProcessAfterInitialization` — **after** `@PostConstruct`. Two consequences: 1. **Self-invocation is unadvised.** If your `@PostConstruct` method calls another method on `this` that is `@Transactional`/`@Async`, the call goes through the raw target, not the proxy, so **no transaction/async advice applies** — the same self-invocation limitation as always, but especially easy to hit at init time. Even injecting `self` doesn't fully help because at `@PostConstruct` time the proxy for *this* bean may not exist yet. 2. **Don't rely on proxied behavior during init.** For work that must be transactional or async at startup, defer it past full context refresh: implement `ApplicationRunner`/`CommandLineRunner`, listen for `ContextRefreshedEvent`/`ApplicationReadyEvent`, or use `SmartInitializingSingleton.afterSingletonsInstantiated()` — all of which run after every singleton (and its proxy) is fully created. **Error semantics.** An exception thrown from `@PostConstruct` (or `afterPropertiesSet`) aborts context startup with a `BeanCreationException`, which is usually what you want for fail-fast validation. Note that if init succeeds but a *later* bean's init fails, Spring will still invoke destroy callbacks on the already-initialized singletons, so make destroy callbacks null-safe against partially-initialized state. **Summary heuristic:** constructor = assemble and validate arguments cheaply; `@PostConstruct` = 'the bean and its dependencies are ready, do startup work' but *not* proxied self-calls; `ApplicationRunner`/events/`SmartInitializingSingleton` = 'the whole context is ready, do advised or cross-bean startup work.'
- If you call a @Transactional method of the same bean from its @PostConstruct, does the transaction apply?No. @PostConstruct runs on the raw target before the AOP proxy is created, and self-invocation bypasses the proxy anyway, so no transactional advice is applied. Defer to ApplicationRunner or a ContextRefreshedEvent listener.
- Where does constructor injection change the 'why not the constructor' argument?With constructor injection the dependencies are available in the constructor, so null-dependency concerns vanish; but you still keep heavy/side-effecting startup work in @PostConstruct for cheap, testable construction and proper ordering.
- What runs after every singleton (and its proxy) is fully created?SmartInitializingSingleton.afterSingletonsInstantiated(), and application events like ContextRefreshedEvent / ApplicationReadyEvent, and ApplicationRunner/CommandLineRunner in Boot.
saying these in an interview costs you the question
- Claiming @PostConstruct-time self-calls to @Transactional/@Async methods are advised
- Saying field-injected dependencies are available in the constructor
- Believing the AOP proxy already exists when @PostConstruct runs