Advice
An advice is a method of an aspect that runs at a chosen moment of each call its pointcut matches: before, after, or instead of it.
Why you need it
The advice annotation says when your code runs. Pick the weakest one that does the job: @Before for checks, @After for clean-up, @AfterReturning to read results, @AfterThrowing to read errors. Use @Around only when you must change arguments or the result, skip the call, retry it, or time it.
| Annotation | Runs | Parameters (all optional unless noted) | Can change the result |
|---|---|---|---|
@Before |
before the method | JoinPoint |
no; throwing stops the call |
@After |
after the method, whether it returned or threw (like finally) |
JoinPoint |
no |
@AfterReturning |
after it returned normally | JoinPoint, then the result |
no |
@AfterThrowing |
after it threw; the error is rethrown afterwards | JoinPoint, then the error |
no |
@Around |
instead of the method; you decide if and how to call it | ProceedingJoinPoint (required) |
yes |
Parameters are positional. JoinPoint and ProceedingJoinPoint are matched by exact type. Parameters bound by the pointcut (see Pointcuts) are matched by name. Any other parameter is a build error.
All examples below are methods of an aspect class like this one:
import 'dart:async';
import 'package:spring_core_dart/spring_core_dart.dart';
// Marker annotations used below.
class Timed {
const Timed();
}
class Retry {
const Retry();
}
@Aspect()
@Component()
@Log()
class OrderAudit {
OrderAudit(this.shop); // Shop is another bean of the application
final Shop shop;
@Pointcut('within(OrderService)')
void orders() {}
// advice methods go here
}
@Before
Runs before each matched call. Use it for checks and logging that must happen before the method body. It cannot change the arguments or the result. If it throws, the method does not run and the caller gets that error.
// Refuses orders while the shop is closed.
@Before('execution(* OrderService.place(..))')
void checkOpen(JoinPoint call) {
if (!shop.isOpen) {
throw StateError('shop closed, ${call.signature.name} refused');
}
}
| Name | Type | Default | Meaning |
|---|---|---|---|
pointcut |
String |
required | The pointcut expression selecting the advised methods. |
@After
Runs after each matched call, whether it returned or threw, like a finally block. Use it for clean-up and "call finished" logging. It cannot change the result or the error.
// Logs the end of every OrderService call, even a failed one.
@After('orders()')
void after(JoinPoint call) => log.info('finished ${call.toShortString()}');
| Name | Type | Default | Meaning |
|---|---|---|---|
pointcut |
String |
required | The pointcut expression selecting the advised methods. |
@AfterReturning
Runs after each matched call that returned normally, and receives the returned value. Use it to inspect or log results. It cannot replace the result; use @Around for that.
The parameter named by returning receives the value. Without returning, the first parameter that is neither the JoinPoint nor bound by the pointcut receives it. The parameter's type is a filter: the advice runs only when the value has that type.
// Runs only when the result is a double; other results pass by.
@AfterReturning('orders()')
void returned(JoinPoint call, double total) => log.info('total: $total');
// `timed` is bound by the pointcut, so `returning` names the result.
@AfterReturning('@annotation(timed)', returning: 'result')
void timedResult(Timed timed, Object? result) => log.info('$timed: $result');
| Name | Type | Default | Meaning |
|---|---|---|---|
pointcut |
String |
required | The pointcut expression selecting the advised methods. |
returning |
String? |
null |
The advice parameter that receives the returned value. null picks the first parameter that is neither the JoinPoint nor bound by the pointcut. |
@AfterThrowing
Runs after each matched call that threw, and receives the error. Use it to log or count failures. The error is always rethrown afterwards, so the caller still sees it.
The parameter named by throwing receives the error, or else the first parameter that is neither the JoinPoint nor bound. Its type is a filter: the advice runs only for errors of that type.
// Runs only for StateError; other errors pass by.
@AfterThrowing('orders()')
void failed(JoinPoint call, StateError error) =>
log.warning('${call.signature.name} failed: ${error.message}');
| Name | Type | Default | Meaning |
|---|---|---|---|
pointcut |
String |
required | The pointcut expression selecting the advised methods. |
throwing |
String? |
null |
The advice parameter that receives the error. null picks the first parameter that is neither the JoinPoint nor bound by the pointcut. |
@Around
Runs instead of each matched call and decides whether and how the real method runs. It can time the call, change the arguments, replace the result, retry, or skip the method.
The advice takes a ProceedingJoinPoint and returns the call's result, usually what proceed() returns. proceed(args) runs the method with other arguments; the list must have as many elements as the call has arguments, else an ArgumentError is thrown. Not calling proceed() skips the method. Calling it again retries.
// Keeps the discount between 0 and 50 percent.
@Around('execution(* OrderService.discount(..))')
Object? clamp(ProceedingJoinPoint call) {
final [percent] = call.args;
return call.proceed([(percent! as int).clamp(0, 50)]);
}
// Retries a flaky call up to three times.
@Around('@annotation(Retry)')
Object? retry(ProceedingJoinPoint call) {
for (var attempt = 1; ; attempt++) {
try {
return call.proceed();
} on TimeoutException {
if (attempt == 3) rethrow;
}
}
}
| Name | Type | Default | Meaning |
|---|---|---|---|
pointcut |
String |
required | The pointcut expression selecting the advised methods. |
Async methods
For a member that returns a Future (or a FutureOr whose value is a future):
@After,@AfterReturningand@AfterThrowingrun when the future completes, with its value or error, not when the method returns.- In
@Around,proceed()returns the future. Return it, or make the adviceasyncandawaitit to work with the value. @Beforeand the other advice may beasync; the call waits for them before it goes on.- On a synchronous method, an
async@Before,@After,@AfterReturningor@AfterThrowingadvice fails the call with anAopInvocationException. Make the advice synchronous, or the method return aFuture.
// Times every method that returns a Future, until the future completes.
@Around('execution(Future *(..))')
Future<Object?> timeAsync(ProceedingJoinPoint call) async {
final watch = Stopwatch()..start();
try {
return await (call.proceed()! as Future<Object?>);
} finally {
log.info('${call.signature.name}: ${watch.elapsedMilliseconds} ms');
}
}
Order
- Several aspects:
@Orderon the aspect class (orOrdered) decides. The lower value is the outer one: its@Beforeruns first, its@Afterlast. Aspects without an order come last. - Inside one aspect: around, then before, after, afterReturning, afterThrowing. Advice of the same kind keeps declaration order.
In Spring (Java)
@Before("execution(* com.example.OrderService.place(..))")
void checkOpen(JoinPoint call) { ... }
@AfterReturning(pointcut = "orders()", returning = "total")
void returned(JoinPoint call, double total) { ... }
@AfterThrowing(pointcut = "orders()", throwing = "error")
void failed(JoinPoint call, IllegalStateException error) { ... }
@Around("execution(* com.example.OrderService.discount(..))")
Object clamp(ProceedingJoinPoint call) throws Throwable {
Object[] args = call.getArgs();
args[0] = Math.min(Math.max((int) args[0], 0), 50);
return call.proceed(args);
}
Differences that matter:
- The pointcut is the first positional argument:
@AfterReturning('orders()', returning: 'total'). There is nopointcut =/value =choice. returningandthrowingare optional. Without them, the first free parameter gets the value or error.- Advice on methods that return a
Futureruns when the future completes. Java has no such rule. proceeddoes not declareThrowable; errors simply propagate.