JoinPoint

A JoinPoint describes one intercepted call: which member is called, on which bean, with which arguments.

Why you need it

Advice often needs to know what it is advising: to write a log line, to name a metric after the method, or to read the method's annotations. Declare a JoinPoint parameter and the proxy passes it in. @Around advice takes a ProceedingJoinPoint, which can also run the call.

You do not need it when the advice does the same thing for every call, such as @After('orders()') void after() => .... The parameter is optional for every advice except @Around.

JoinPoint

// Logs each call to the `calculator` bean with its arguments.
@Before('bean(calculator)')
void before(JoinPoint joinPoint) =>
    log.info('${joinPoint.toShortString()} with ${joinPoint.args}');
Member Type Meaning
proxy Object The proxy the caller called, the object other beans hold. For a generated subclass proxy it is the bean itself.
target Object The bean whose member runs: the object behind the proxy.
signature MethodSignature The called member: its name, types and annotations.
args List<Object?> The arguments: positional ones, then named ones. A copy: changing it does not change the call.
toShortString() String A short description for logs, such as execution(Greeter.greet(..)).

ProceedingJoinPoint

The join point of @Around advice. It adds one method:

Member Type Meaning
proceed([List<Object?>? args]) Object? Runs the next advice, or the member itself after the last one, and returns its result. args replaces the arguments and must have as many elements as the call has, else an ArgumentError is thrown. For a member returning a Future, the result is that future.
// Keeps the second factor of `multiply` between 0 and 10.
@Around('execution(* Calculator.multiply(..))')
Object? clamp(ProceedingJoinPoint joinPoint) {
  final [a, b] = joinPoint.args;
  return joinPoint.proceed([a, (b! as int).clamp(0, 10)]);
}

MethodSignature

What is known about the advised member without calling it. Dart has no runtime reflection, so the generator writes one constant per proxied member. Type names are simple class names without type arguments (Future, not Future<int>). They are written at build time, so they also work in minified and obfuscated builds. Applications read it; they never create it.

// Logs lines like "int Calculator.add(int, int)".
@Before('execution(* Calculator.*(..))')
void before(JoinPoint joinPoint) {
  final signature = joinPoint.signature;
  log.info('${signature.returnType} ${signature.declaringType}.'
      '${signature.name}(${signature.parameterTypes.join(', ')})');
}
Member Type Meaning
name String The member's name, such as add. A setter's name has no =.
kind MemberKind method, getter or setter.
declaringType String The most abstract type that declares the member, such as the interface Calculator.
declaringTypes List<String> Every type of the bean that declares the member.
targetType String The bean's class, such as SimpleCalculator.
targetTypes List<String> The bean class and all its supertypes.
returnType String The return type's name; void when it returns nothing, dynamic when unknown.
parameterTypes List<String> The parameter types' names.
parameterNames List<String> The parameter names, in the order of args.
parameterAnnotations List<List<Object>> The annotations of each parameter.
annotations List<Object> The member's annotations that @annotation(...) matches: those of the method that runs and of the interface methods it implements.
mergedAnnotations List<Object> The annotations of the member and of every declaration it overrides or implements, most specific first.
targetAnnotations List<Object> The annotations on the bean class.
implementingType String The class whose body runs for the member: the bean class, or the superclass or mixin it inherits it from.
isProtected bool Whether the member is annotated @protected.
toShortString() String Such as Greeter.greet(..).

MemberKind

Dart has getters and setters as members of their own, and a public field is an implicit getter (and setter, unless final). Proxies advise them like methods. Read signature.kind when advice must treat property access differently.

// Logs the calls to Account, but not the getter reads.
@Before('within(Account)')
void audit(JoinPoint joinPoint) {
  if (joinPoint.signature.kind != MemberKind.getter) {
    log.info(joinPoint.toShortString());
  }
}
Value Meaning
MemberKind.method A method; the only kind execution(...) matches.
MemberKind.getter A getter, explicit or the implicit one of a public field.
MemberKind.setter A setter, explicit or the implicit one of a non-final public field.

MethodInterceptor and MethodInvocation

Advice can also be a class instead of an aspect method. Implement MethodInterceptor and pair it with a pointcut in a DefaultPointcutAdvisor bean. Use this for reusable advice, such as a library's cache or security check.

Its invoke method receives a MethodInvocation: a ProceedingJoinPoint whose args are the live arguments. An interceptor may change them in place before it calls proceed().

// Upper-cases every String the slogans bean returns.
class UpperCase implements MethodInterceptor {
  @override
  Object? invoke(MethodInvocation invocation) =>
      (invocation.proceed()! as String).toUpperCase();
}

@Configuration()
class AopConfig {
  @Bean()
  PointcutAdvisor upperCase() =>
      DefaultPointcutAdvisor(ExpressionPointcut('bean(slogans)'), UpperCase());
}
Member Type Meaning
MethodInterceptor.invoke(MethodInvocation invocation) Object? Runs around the call and returns its result. Call invocation.proceed() to continue; not calling it skips the member.
DefaultPointcutAdvisor(pointcut, advice, {order}) - A pointcut, an interceptor and an order. order defaults to Ordered.lowestPrecedence (the innermost). Since it implements Ordered, @Order on its @Bean method is ignored.

In Spring (Java)

@Before("execution(* com.example.Calculator.*(..))")
void before(JoinPoint joinPoint) {
  MethodSignature signature = (MethodSignature) joinPoint.getSignature();
  log.info(signature.getReturnType() + " " + signature.getName()
      + Arrays.toString(joinPoint.getArgs()));
}

Differences that matter:

See also