Give a real use case for @After advice and explain why @After (not @Around) fits it.
answer
- Acquire in @Before, release in @After
- Finally without owning proceed()
- Can't swallow exception, can't change result
- Escalate to @Around for value/exception/timing
- Cleanup must be idempotent + null-safe
basics
~20 sUse @After to release a resource acquired for the call — clear a ThreadLocal/MDC context, unlock a lock, or decrement a gauge — because it must happen on success and failure alike and does not need the result or exception. @After is simpler than @Around for that.
solid answer
~50 sA classic fit is per-invocation context cleanup. Suppose a @Before advice sets a correlation id in MDC or acquires a tenant lock; you need it torn down whether the method succeeds or fails. @After gives exactly finally semantics with minimal code and no need to remember to call proceed(). @Around could do the same via try/finally, but it forces you to own the proceed() call, correctly rethrow, and preserve the return value — more surface area and a real risk of accidentally swallowing exceptions or altering the result. Choose @After when the cleanup is unconditional and needs neither the return value nor the exception. Escalate to @Around only when you must also read/replace the result, translate the exception, retry, or measure elapsed time. Caveat: cleanup in @After must be idempotent and safe on the throwing path, since it runs even after a partial failure.
code
java · 15 lines@Aspect
@Component
public class TenantContextAspect {
@Before("@annotation(com.acme.TenantScoped)")
public void openContext(JoinPoint jp) {
TenantContext.set(CurrentTenant.resolve());
}
// Runs on success AND failure — the resource is always released.
@After("@annotation(com.acme.TenantScoped)")
public void closeContext(JoinPoint jp) {
TenantContext.clear(); // idempotent, safe even if open failed
}
}go deeper
Know @After is for cleanup that must always happen.
Contrast with @Around and note @After can't touch the result/exception.
Must reason about idempotency on the throwing path and when to escalate to @Around.
Weigh maintainability of split @Before/@After vs a single @Around for shared setup/teardown state, and how cleanup exceptions can mask originals.
### The scenario Many cross-cutting concerns follow an acquire/use/release shape: - Set a logging **MDC**/correlation id at entry, clear it at exit. - Acquire a distributed or reentrant **lock**, release it after. - Increment an **in-flight gauge** at entry, decrement after. - Open a scratch resource (temp buffer, scoped context) and close it. The *release* half must run on **every** exit path — normal return or exception — and typically does **not** need the method's result or the thrown exception. That is precisely `finally`, which is what `@After` gives you. ### Why @After fits better than @Around here `@Around` is the most powerful advice but also the most error-prone: ``` @Around("...") public Object around(ProceedingJoinPoint pjp) throws Throwable { setup(); try { return pjp.proceed(); // must call it, must return its value } finally { cleanup(); } } ``` With `@Around` you are responsible for calling `proceed()`, returning its exact value, and letting exceptions propagate (declaring `throws Throwable`). Forget `proceed()` and the target never runs; catch too broadly and you silently swallow an exception or change the result. `@After` removes all of that: Spring guarantees it runs after the join point, you cannot accidentally suppress the exception (it is re-thrown for you) and you cannot alter the return value (you never touch it). Less power, fewer footguns. ### When @Around is the right escalation Use `@Around` (not `@After`) when the cleanup logic needs any of: - the **return value** (inspect/replace it), - the **exception** (translate, wrap, or retry), - **timing** (start a clock before, stop after — `@Before`+`@After` share no local state cleanly), - **short-circuiting** (skip or replace the call). ### Correctness concerns for @After cleanup - **Idempotency / null-safety**: `@After` runs even if the method failed early, possibly before the resource was fully set up. Guard against releasing something that was never acquired (e.g. check for null, use `MDC.clear()` which is safe to call unconditionally). - **Pairing with @Before**: if `@Before` acquires and `@After` releases, ensure both pointcuts match the exact same join points; a mismatch leaks resources. Ordering within the aspect is deterministic, but consider whether a single `@Around` would make the acquire/release pairing more obviously correct for complex cases. - **Exceptions inside @After**: if the cleanup itself throws, that exception can mask the original one. Keep `@After` bodies defensive. ### Rule of thumb Unconditional, outcome-blind cleanup -> `@After`. Anything that must read or change the outcome, or that owns setup+teardown as one unit of shared state -> `@Around`.
- What risk appears if the resource acquisition in @Before fails but @After still runs?@After runs on the exception path too, so it may try to release something that was never acquired. Cleanup must be idempotent/null-safe (e.g. clear() that no-ops when empty) to avoid a secondary exception that masks the original.
- When would you refactor a @Before + @After pair into a single @Around?When setup and teardown share local state (a timer, an opened handle), when you must read/replace the return value or exception, or when the acquire/release pairing is easier to reason about as one method with try/finally.
saying these in an interview costs you the question
- Using @After but expecting to read the result or exception
- Assuming @After can retry or suppress the failure
- Non-idempotent cleanup that throws on the failure path and hides the real error