Search code examples
aopaspectjmethod-interception

How to exclude interception of internal methods in AspectJ AOP?


We are migrating an application that is using Jboss AOP (which is proxy based) to AspectJ AOP with compile time weaving. However, we do not want the internal methods to be intercepted by AspectJ, but that seems to be the default behavior of AspectJ.

There are multiple posts on how to intercept internal method calls in Spring AOP. However, could not find any posts related to excluding internal methods using AspectJ. We want to use AspectJ compile time weaving for the run time performance improvements that it promises.

If another class's method calls any public method in the below class TestService, the call should be intercepted. However, the internal call from method1() to method2() should not be intercepted. We just want the interceptors intercept only once per object.

public class TestService {
  public void method1() {
    …
    // We do not want the below internal call to be intercepted. 
    this.method2();
  }

  // If some other class's method calls this, intercept the call. But do not intercept the call from method1().
  public void method2() {
    ...     
  }
}

An example Aspect:

@Aspect
public class ServiceAspectJHydrationInterceptor {
    @Pointcut("execution(public * com.companyname.service..impl.*ServiceImpl.*(..))")
    public void serviceLayerPublicMethods() {}

    @Pointcut("@annotation(com.companyname.core.annotation.SkipHydrationInterception)")
    public void skipHydrationInterception() {}

    @Around("serviceLayerPublicMethods() && !skipHydrationInterception()")
    public Object invoke(ProceedingJoinPoint pjp) throws Throwable {
        …
    }
}

The behavior of excluding the internal method call interception is default in Spring AOP as it is proxy based. Is there a way to achieve the exclusion of internal method interception using AspectJ with compile time weaving?

Software details: Spring version: 3.2.14. JDK version: 1.8. The maven plugin codehaus “aspectj-maven-plugin” version 1.7 is used to do compile time weaving.


Solution

  • You can use the pattern execution(...) && !cflowbelow(execution(...)). This is not good for performance because the execution path (think callstack) has to be checked during runtime rather than during compile time, but it does what you want. Beware of some key differences due to the non-proxy nature of AspectJ and due to the bigger set of joinpoints and pointcuts available in comparison to other AOP frameworks, such as intercepting private or static methods.

    Now here is a little example along the lines of what you described:

    package de.scrum_master.core.annotation;
    
    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    @Retention(RUNTIME)
    @Target(METHOD)
    public @interface SkipHydrationInterception {}
    
    package de.scrum_master.service.foo.bar.impl;
    
    import de.scrum_master.core.annotation.SkipHydrationInterception;
    
    public class MyServiceImpl {
      public void method1() {
        // We do not want the below internal call to be intercepted.
        method2();
      }
    
      public void method2() {
        // If some other class's method calls this, intercept the call. But do not
        // intercept the call from method1().
      }
    
      @SkipHydrationInterception
      public void method3() {
        // Always skip this method one due to the annotation.
    
        // Should this one be intercepted or not?
        // method1();
      }
    
      public static void main(String[] args) {
        MyServiceImpl service = new MyServiceImpl();
        service.method1();
        System.out.println("-----");
        service.method2();
        System.out.println("-----");
        service.method3();
      }
    }
    
    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;
    
    @Aspect
    public class ServiceAspectJHydrationInterceptor {
      @Pointcut("execution(public !static * de.scrum_master.service..impl.*ServiceImpl.*(..))")
      public void serviceLayerPublicMethods() {}
    
      @Pointcut("@annotation(de.scrum_master.core.annotation.SkipHydrationInterception)")
      public void skipHydrationInterception() {}
    
      @Pointcut("serviceLayerPublicMethods() && !skipHydrationInterception()")
      public void interceptMe() {}
    
      @Around("interceptMe() && !cflowbelow(interceptMe())")
      public Object invoke(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println(pjp);
        return pjp.proceed();
      }
    }
    

    Now run the driver application and you will see this console log:

    execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
    -----
    execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
    -----
    

    This is exactly what you want. So far, so good. Please also note the !static qualifier in the execution pointcut because otherwise static main(..) would be intercepted.

    But now uncomment the method1() call inside the body of method3(). The console log becomes:

    execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
    -----
    execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
    -----
    execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
    

    The question is: Is this what you want? method1() is called by a method that was excluded from interception due to its annotation but on the other hand it is also an internal method call, I like to call it self-invocation. The solution depends on your answer.

    Please also note that public methods called from private or protected methods of the same class would also be intercepted. So cflow() or cflowbelow() do not care about self-invocation, just about the specified control flow.

    Another situation: If an intercepted public method would for some reason call another class and that class would again call a public method of the first class, !cflowbelow(...) would still exclude this call from being intercepted because the first call is already in the control flow.

    Next situation: One public *ServiceImpl method calls another public *ServiceImpl method. The result would also be that the second called method would not be intercepted because something matching its execution pointcut is already in the control flow (call stack).

    So my solution, even if we tweak the pointcuts to cover a few corner cases, is not the same as what a proxy-based solution would do by nature. If corner cases like the ones described can happen in your environment you really should refactor the aspects in order to either do some book-keeping (saving state) and/or use another instantiation model such as percflowbelow (but haven't thought that one through because I don't know your exact requirements). But SO is not a discussion forum and I cannot help you incrementally here. Feel free to check out the contact data (e.g. Telegram) in my SO profile and hire me if you need more in-depth support. But maybe you can also take it from here, I am just mentioning it.


    Update:

    Okay, I came up with a way to emulate proxy-based AOP behaviour via AspectJ. I don't like it and it requires you to switch from execution() to call() pointcut, i.e. you no longer need to control (aspect-weave) the callee (executed code) but the caller (origin of method call to be intercepted).

    You also need a runtime check between two objects this() and target() from an if() pointcut. I don't like that either because it makes your code slower and has to be checked in many places. If you can still reach your goal of performance improvement in comparison to the proxy-based solution you want to get rid of, you got to check by yourself. Remember, you are now emulating what you want to abolish, LOL.

    Let's add another class in order to simulate the interplay of an external class calling the target class in addition to just calling it from a static method, which is not a sufficient test case.

    package de.scrum_master.service.foo.bar.impl;
    
    public class AnotherClass {
      public void doSomething() {
        MyServiceImpl service = new MyServiceImpl();
        service.method1();
        System.out.println("-----");
        service.method2();
        System.out.println("-----");
        service.method3();
        System.out.println("-----");
      }
    }
    

    The original MyServiceImpl class we extend a bit by logging more and also calling AnotherClass.doSomething().

    package de.scrum_master.service.foo.bar.impl;
    
    import de.scrum_master.core.annotation.SkipHydrationInterception;
    
    public class MyServiceImpl {
      public void method1() {
        System.out.println("method1");
        method2();
      }
    
      public void method2() {
        System.out.println("method2");
      }
    
      @SkipHydrationInterception
      public void method3() {
        System.out.println("method3");
        method1();
      }
    
      public static void main(String[] args) {
        MyServiceImpl service = new MyServiceImpl();
        service.method1();
        System.out.println("-----");
        service.method2();
        System.out.println("-----");
        service.method3();
        System.out.println("-----");
        new AnotherClass().doSomething();
      }
    }
    

    The improved aspect looks like this:

    package de.scrum_master.aspect;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    
    @Aspect
    public class ServiceAspectJHydrationInterceptor {
      @Pointcut("call(public !static * de.scrum_master.service..impl.*ServiceImpl.*(..))")
      public void serviceLayerPublicMethods() {}
    
      @Pointcut("@annotation(de.scrum_master.core.annotation.SkipHydrationInterception)")
      public void skipHydrationInterception() {}
    
      @Pointcut("serviceLayerPublicMethods() && !skipHydrationInterception()")
      public void interceptMe() {}
    
      @Pointcut("if()")
      public static boolean noSelfInvocation(ProceedingJoinPoint thisJoinPoint) {
        return thisJoinPoint.getThis() != thisJoinPoint.getTarget();
      }
    
      @Around("interceptMe() && noSelfInvocation(thisJoinPoint)")
      public Object invoke(ProceedingJoinPoint thisJoinPoint, JoinPoint.EnclosingStaticPart thisEnclosingStaticPart) throws Throwable {
        System.out.println(thisJoinPoint);
        System.out.println("  called by: " + thisEnclosingStaticPart);
        return thisJoinPoint.proceed();
      }
    }
    

    And now the console log looks like this:

    call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
      called by: execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.main(String[]))
    method1
    method2
    -----
    call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
      called by: execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.main(String[]))
    method2
    -----
    method3
    method1
    method2
    -----
    call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
      called by: execution(void de.scrum_master.service.foo.bar.impl.AnotherClass.doSomething())
    method1
    method2
    -----
    call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
      called by: execution(void de.scrum_master.service.foo.bar.impl.AnotherClass.doSomething())
    method2
    -----
    method3
    method1
    method2
    -----
    

    In my opinion this is exactly how Spring AOP or JBoss AOP would behave due to their proxy nature. Maybe I forgot something, but I think I got the corner cases pretty much covered.

    Please let me know if you have problems understanding this solution. As for the meaning of the pointcut designators I use, please consult the AspectJ manual.