
# 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](pointcut.md)) are matched by name. Any other parameter is a build error.

All examples below are methods of an aspect class like this one:

```dart
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.

```dart
// 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.

```dart
// 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.

```dart
// 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.

```dart
// 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.

```dart
// 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`, `@AfterReturning` and `@AfterThrowing` run 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 advice `async` and `await` it to work with the value.
- `@Before` and the other advice may be `async`; the call waits for them before it goes on.
- On a synchronous method, an `async` `@Before`, `@After`, `@AfterReturning` or `@AfterThrowing` advice fails the call with an `AopInvocationException`. Make the advice synchronous, or the method return a `Future`.

```dart
// 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: `@Order` on the aspect class (or `Ordered`) decides. The lower value is the outer one: its `@Before` runs first, its `@After` last. 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)

```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 no `pointcut =` / `value =` choice.
- `returning` and `throwing` are optional. Without them, the first free parameter gets the value or error.
- Advice on methods that return a `Future` runs when the future completes. Java has no such rule.
- `proceed` does not declare `Throwable`; errors simply propagate.

## See also

- [Pointcuts](pointcut.md)
- [JoinPoint](join-point.md)
- [@Aspect](aspect.md)
- [Proxies](proxies.md)
