Search code examples
javaaopaspectj

Unclear advice precedence when combining before-, around- and after-advice operating on same joinpoint in one aspect


Please consider this simple Java-code

public class Application {

  public void m(int i) {
   System.out.println("M with argument " + i );
  }

  public static void main(String[] arg) {
   Application t = new Application();
   t.m(25);  
  }
}

I have defined the following Aspect to operate on this class:

public aspect Basics {
  public void output(String tag, Object o) {
    System.out.println(tag + ": " + o);
  }

  pointcut callM(int i): call(void Application.m(int)) && args(i);

  before(int i): callM(i) {
    output("before M", i);  
  }

  void around(int i): callM(i) {
      output("around-advice", i);
      proceed(1);
      output("after proceed", i);
  }

  after(int i): callM(i) {
    output("After M", i);  
  }
}

It's important to note that the around-advice changes the value of the argument that's passed to method M to 1. Running this code generates the following output:

before M: 25
around-advice: 25
M with argument 1
after proceed: 25
After M: 25

The entire output is as I would have expected, except for the last line. I expected the last line to print '1' instead of '25'. Can someone explain to me why this is the case?

While looking for an answer myself, I tried to change the ordering of the advices but this only made the confusion bigger in the end. If I put the after-advice first in the code, followed by the before-advice and then put the around-advice last (i.e. (1)after-(2)before-(3)around), I got the following output:

before M: 25
around-advice: 25
M with argument 1
After M: 1
after proceed: 25

To me, this is the only output that makes perfect sense.

However, if I put the after-advice first, followed by the around-advice while putting the before-advice last (i.e.(1)after-(2)around-(3)before), I get the following output which also makes little sense to me if I take the output of the previous orderings into account:

around-advice: 25
before M: 1
M with argument 1
After M: 1
after proceed: 25

In this case, the before-advice gets triggered with 'i' bound to 1. My guess is that this is due to the around-advice being triggered first (because of the ordering) and that the before-advice is actually triggered by the call to 'proceed' in the body of the around-advice. Following this logic, however, doesn't explain the output that was generated in the ordering that was discussed first in this question.

Finally, changing the ordering in such a way that we first have the before-advice, followed by the after-advice, and then followed by the around-advice (i.e. (1)before-(2)after-(3)around) is not valid according to the AspectJ-plugin of Eclipse because this generates a 'circular advice precedence'.

Can someone give me an explanation of the precedence being used between different advices within the same aspect that explains all the behaviour above?

I have been reading on the subject here but I think the explanation is inconclusive/doesn't match the implementation. It says

A piece of around advice controls whether advice of lower precedence will run by calling proceed. The call to proceed will run the advice with next precedence, or the computation under the join point if there is no further advice.

If I understand correctly, this means that the output that was discussed first in this question (i.e. (1)before-(2)around-(3)after-ordering) should have had '1' in the last line rather than '25'.


Solution

  • Actually @XGouchet's first sketch is not quite correct because the before advice does not occur within the scope of around, but has finished before around is executed. Let me use pseudo code notation with curly braces expressing this kind of "lexical scope":

    Scenario A: lexical ordering before -> around -> after:

    before(25)
    around(25 → 1) {
        joinpoint(1)
    }
    after(25)
    

    Scenario B: lexical ordering after -> before -> around:

    before(25)
    around(25 → 1) {
        joinpoint(1)
        after(1)
    }
    

    Scenario C: lexical ordering after -> around -> before:

    around(25 → 1) {
        before(1)
        joinpoint(1)
        after(1)
    }
    

    Quote from Determining precedence paragraph from the AspectJ manual's chapter on aspect precedence:

    If the two pieces of advice are defined in the same aspect, then there are two cases:

    • If either are after advice, then the one that appears later in the aspect has precedence over the one that appears earlier.
    • Otherwise, then the one that appears earlier in the aspect has precedence over the one that appears later.

    Now also bear in mind how aspects behave and what precedence really means under certain circumstances. Also quoting the Effects of precedence paragraph from the AspectJ manual's chapter on aspect precedence:

    At a particular join point, advice is ordered by precedence.

    • A piece of around advice controls whether advice of lower precedence will run by calling proceed. The call to proceed will run the advice with next precedence, or the computation under the join point if there is no further advice.
    • A piece of before advice can prevent advice of lower precedence from running by throwing an exception. If it returns normally, however, then the advice of the next precedence, or the computation under the join point if there is no further advice, will run.
    • Running after returning advice will run the advice of next precedence, or the computation under the join point if there is no further advice. Then, if that computation returned normally, the body of the advice will run.
    • Running after throwing advice will run the advice of next precedence, or the computation under the join point if there is no further advice. Then, if that computation threw an exception of an appropriate type, the body of the advice will run.
    • Running after advice will run the advice of next precedence, or the computation under the join point if there is no further advice. Then the body of the advice will run.

    In plain English:

    • An around advice wraps

      • other around or before advices of lower precedence as well as
      • after advices of higher precedence.

      Why is that so? Imagine the around advice to be a pair of before and after advices on steroids (can modify method parameters and return value, can handle exceptions, can share state between before and after code). Consequently, the same delegation rules as for separate before and after advices apply as follows.

    • A before advice executes first before delegating or proceeding.

    • An after advice delegates first before executing. In particular (and maybe somewhat counter-intuitively), this means:

      • If an after advice has higher precedence than an around advice, then if the around advice modifies any method arguments before proceeding, the after advice is affected by the modification too, because the around advice already proceeded to the target method with the modified arguments before the after advice kicks in.

      • However, the same higher precedence after (returning) advice is unaffected if the lower precedence around advice modifies the return value because the after (returning) advice is executed before the around advice's post-proceed code due to its higher precedence.

      • Conversely, if an after advice has lower precedence than an around advice, then if the around advice modifies any method arguments before proceeding, the after advice is unaffected by the modification because it runs outside the context of the around advice, it is not being wrapped by it.

      • However, the same lower precedence after (returning) advice is affected if the higher precedence around advice modifies the return value because it runs after the latter has finished running already.

    Now here is an extended example illustrating what I just wrote:

    Driver application:

    package de.scrum_master.app;
    
    public class Application {
        public void doSomething(int i) {
            System.out.println("Doing something with " + i);
        }
    
        public static void main(String[] arg) {
            Application application = new Application();
            application.doSomething(99);
        }
    }
    

    Aspect, variant 1:

    package de.scrum_master.aspect;
    
    import de.scrum_master.app.Application;
    
    public aspect IntraAspectPrecedence {
    
        pointcut methodCall(int i) :
            call(void Application.doSomething(int)) && args(i);
    
        void around(int i): methodCall(i) {
            System.out.println("around1 (pre-proceed) -> " + i);
            proceed(11);
            System.out.println("around1 (post-proceed) -> " + i);
        }
    
        void around(int i): methodCall(i) {
            System.out.println("around2 (pre-proceed) -> " + i);
            proceed(22);
            System.out.println("around2 (post-proceed) -> " + i);
        }
    
        before(int i): methodCall(i) {
            System.out.println("before1 -> " + i);
        }
    
        before(int i): methodCall(i) {
            System.out.println("before2 -> " + i);
        }
    
        after(int i): methodCall(i) {
            System.out.println("after1 -> " + i);
        }
    
        after(int i): methodCall(i) {
            System.out.println("after2 -> " + i);
        }
    
    }
    

    Pseudo code, variant 1:

    around1(99 → 11) {
        around2(11 → 22) {
            before1(22) {
                before2(22) {
                    joinpoint(22) {}
                }
            }
        }
    }
    after2(99) {
        after1(99) {}
    }
    

    Console output, variant 1:

    Please note that "after1" is printed before "after2" even though it has lower precedence because after delegates first, then executes, as explained above.

    around1 (pre-proceed) -> 99
    around2 (pre-proceed) -> 11
    before1 -> 22
    before2 -> 22
    Doing something with 22
    around2 (post-proceed) -> 11
    around1 (post-proceed) -> 99
    after1 -> 99
    after2 -> 99
    

    Aspect, variant 2:

    Basically it is the same as before, only the first after advice has the highest precedence.

    package de.scrum_master.aspect;
    
    import de.scrum_master.app.Application;
    
    public aspect IntraAspectPrecedence {
    
        pointcut methodCall(int i) :
            call(void Application.doSomething(int)) && args(i);
    
        after(int i): methodCall(i) {
            System.out.println("after1 -> " + i);
        }
    
        void around(int i): methodCall(i) {
            System.out.println("around1 (pre-proceed) -> " + i);
            proceed(11);
            System.out.println("around1 (post-proceed) -> " + i);
        }
    
        void around(int i): methodCall(i) {
            System.out.println("around2 (pre-proceed) -> " + i);
            proceed(22);
            System.out.println("around2 (post-proceed) -> " + i);
        }
    
        before(int i): methodCall(i) {
            System.out.println("before1 -> " + i);
        }
    
        before(int i): methodCall(i) {
            System.out.println("before2 -> " + i);
        }
    
        after(int i): methodCall(i) {
            System.out.println("after2 -> " + i);
        }
    
    }
    

    Pseudo code, variant 2:

    This time, because after1 has higher precedence than both around advices, it is executed right after the joinpoint returns and thus before whatever happens after proceed() in the wrapping around advices.

    (Note to myself: I am not happy with this explanation, maybe there is a better way to say it. Maybe the answer could be rephrased, working with one pseudo before/after advice pair per around advice instead of the wrapping metaphor.)

    around1(99 → 11) {
        around2(11 → 22) {
            before1(22) {
                before2(22) {
                    joinpoint(22) {}
                }
            }
            after1(22) {}
        }
    }
    after2(99) {}
    

    Console output, variant 2:

    around1 (pre-proceed) -> 99
    around2 (pre-proceed) -> 11
    before1 -> 22
    before2 -> 22
    Doing something with 22
    after1 -> 22
    around2 (post-proceed) -> 11
    around1 (post-proceed) -> 99
    after2 -> 99