@After Advice
@After advice runs after the target function body completes, regardless of whether it
returned normally or threw an exception. It is the AspectK equivalent of a finally block.
Basic Usage
@Target(AnnotationTarget.FUNCTION)
annotation class Audited
@Aspect
object AuditAspect {
@After(target = [Audited::class])
fun doAfter(joinPoint: JoinPoint) {
println("${joinPoint.signature.methodName} finished")
}
}
class OrderService {
@Audited
fun placeOrder(orderId: String) {
println("Placing order $orderId")
}
}
Calling OrderService().placeOrder("ORD-001") prints:
Function Signature Rules
An @After advice method must:
- Be declared inside an
@Aspect-annotated class or object - Accept exactly one parameter of type
JoinPoint - Return
Unit
@After(target = [Audited::class])
fun doAfter(joinPoint: JoinPoint) { ... } // ✅ correct
// ❌ Wrong: no parameter
@After(target = [Audited::class])
fun bad1() { }
// ❌ Wrong: non-Unit return type
@After(target = [Audited::class])
fun bad2(joinPoint: JoinPoint): String = ""
Parameters
@After(
target = [AnnotationClass::class, AnotherAnnotation::class],
inherits = false,
)
fun adviceMethod(joinPoint: JoinPoint) { ... }
| Parameter | Type | Default | Description |
|---|---|---|---|
target |
KClass<out Annotation> (vararg) |
— | One or more annotation classes that identify target functions |
inherits |
Boolean |
false |
When true, also intercepts overriding functions |
What Gets Compiled
Given this source:
AspectK transforms it into (pseudocode):
fun processPayment(amount: Double): Boolean {
fun `$processPayment`(amount: Double): Boolean {
charge(amount)
return true
}
return try {
`$processPayment`(amount)
} catch (e: Throwable) {
throw e
} finally {
AuditAspect.doAfter(
DefaultJoinPoint(
target = this,
signature = $MethodSignatures.ajc$tjp_0,
args = listOf(amount),
)
)
}
}
Key points:
- The original function body is extracted into a local function (
$processPayment). - The local function is called inside a
try-catch-finallyblock. @Afteradvice fires in thefinallyblock — always, whether the body succeeds or throws.- The
catchblock re-throws the exception so normal exception propagation is preserved.
Execution Order with Other Advice Types
When @Before, @After, and @Around all target the same function, the execution order is:
@Before fires
↓
(@Around starts)
↓
[Original body]
↓ (finally)
@After fires
↓
(@Around post-proceed logic)
@After is placed innermost — it wraps only the original function body, not the @Around
advice call. See Design Rationale for why.
Exception Behaviour
@After fires regardless of whether the original body threw:
@Aspect
object CleanupAspect {
@After(target = [Transactional::class])
fun cleanup(joinPoint: JoinPoint) {
// Always runs — even if the body threw
releaseResources()
}
}
@Transactional
fun riskyOp() = throw RuntimeException("oops")
// cleanup() still fires; the exception propagates to the caller afterwards
Warning
@After cannot suppress exceptions. The original exception always propagates after
the finally block completes. If you need to catch or replace exceptions, use
@Around instead.
@After on Extension and Top-Level Functions
The JoinPoint.args layout follows the same rules as @Before:
// Extension function — receiver is args[0]
@TargetAnn
fun MyClass.doWork(x: String) { ... }
// jp.target → null
// jp.args → [MyClass instance, x]
// Top-level function
@TargetAnn
fun doWork(x: String) { ... }
// jp.target → null
// jp.args → [x]
See Join Points for the full reference on target and args.
Design Rationale — @After Placement
@After is placed in the finally block that wraps only the original function body ($doSomething),
not the entire @Around chain. This is intentional:
-
@After's contract is "execute after the target function", not "execute after all aspects". If an@Aroundadvice throws before callingpjp.proceed(), the original function never ran, so@Aftershould not fire in that case. -
@Aroundis responsible for handling its own exceptions internally. Wrapping the outer@Aroundcall with thefinallyblock would mean@Afterfires even when@Arounditself fails — which conflates two unrelated concerns. -
Predictable execution order:
@Afterfires first (innermostfinally), then@Around's post-proceed()logic runs outward. The order is deterministic and mirrors the lexical nesting of the generated IR.