Call adviced method from around advice and avoiding infinite loop without using cflow

45 views Asked by At

I want to capture all method executions in com.ABC.MyClass using AspectJ

 package com.ABC
 
 public class MyClass{
       
        public methodA(){
        }

        public methodB(){
          methodA();
        }
 }

Below is the Aspect :

@Aspect
public class MyAspect{
  @Pointcut("execution(* com.ABC.MyClass.*(..))")
  public void captureMyClassMethods(){
  }
  
  @Around("captureMyClassMethods()")
  public Object aroundMyClassMethods(ProceedingJoinPoint jp) throws Throwable{
    MyClass object = (MyClass)jp.getTarget();

    object.methodA();      //This causes infinite loop

    Object o = jp.proceed();
    System.out.println("Some data from o");
    return o;
  }
}

As mentioned in comments in around advice, while calling methodA() from advice is causing infinite loop.

As mentioned in another stackoverflow answer Aspectj - how to call advised method from within same advice, without triggering an infinite loop if I change the pointcut expression below then I would miss capturing execution of methodA() when it is called from methodB() because it will be in aspects control flow.

@Pointcut("execution(* com.ABC.MyClass.*(..)) && !cflow(within(MyAspect))")

Another way I can think of is using before and after advices instead of around advice. But I want to know if there is a way to handle this with around advice?

1

There are 1 answers

0
kriegaex On

Actually, I am answering because your question presents a nice puzzle, not because I think that your use case is particularly relevant. Probably, it is rather a design flaw. I think, you should not try to call a method from an advice which is targeted by the same advice.

Both your variant !cflow(within(MyAspect)) and something like !adviceexecution() would, as you have noticed, preclude internal method calls from being captured.

The workaround is an if() pointcut and keeping state in a static, thread-local, boolean field, which is ugly and potentially slow. But if it works for you, you can do that.

package de.scrum_master.app;

public class MyClass {
  public static void main(String[] args) {
    MyClass myClass = new MyClass();
    myClass.hello();
    myClass.world();
    myClass.helloWorld();
  }

  public String hello() {
    return "Hello";
  }

  public String world() {
    return "world";
  }

  public String helloWorld() {
    return hello() + " " + world() + "!";
  }
}
package de.scrum_master.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import de.scrum_master.app.MyClass;

@Aspect
public class MyAspect {
  public static ThreadLocal<Boolean> calledFromAdvice = ThreadLocal.withInitial(() -> false);

  @Pointcut("execution(!static * de.scrum_master.app.MyClass.*(..)) && if()")
  public static boolean captureMyClassMethods() {
    return !calledFromAdvice.get();
  }

  @Around("captureMyClassMethods() && target(myClass)")
  public Object aroundMyClassMethods(ProceedingJoinPoint jp, MyClass myClass) throws Throwable {
    System.out.println(jp);
    try {
      calledFromAdvice.set(true);
      myClass.helloWorld();
    } finally {
      calledFromAdvice.set(false);
    }
    String result = (String) jp.proceed();
    System.out.println("  " + result);
    return result;
  }
}

The !static qualifier avoids the static main method from beiong intercepted. There, the target object would be null, causing a null pointer exception when trying to call amethod upon it.

Please also note how I am binding the target object to an advice parameter, which is more elegant than calling jp.getTraget() and casting it.

Last but not least, the try-finally makes sure to always reset the flag, even if there is an exception while calling hello.

The console log looks like this:

execution(String de.scrum_master.app.MyClass.hello())
  Hello
execution(String de.scrum_master.app.MyClass.world())
  world
execution(String de.scrum_master.app.MyClass.helloWorld())
execution(String de.scrum_master.app.MyClass.hello())
  Hello
execution(String de.scrum_master.app.MyClass.world())
  world
  Hello world!

As you can see, in this case the inner method calls inside helloWorld are both captured correctly, but there is no stack overflow exception due to calling hello from the aspect