在一个方面结合对同一连接点操作的前通知、环绕通知和后通知时,通知优先级不明确

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

请考虑这个简单的Java代码

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);  
  }
}

我定义了以下方面来对此 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);  
  }
}

请务必注意,around-advice 会将传递给方法 M 的参数值更改为 1。 运行 此代码生成以下输出:

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

除最后一行外,整个输出如我所料。我希望最后一行打印“1”而不是“25”。有人可以向我解释为什么会这样吗?

在自己寻找答案的过程中,我尝试更改建议的顺序,但这最终只会让混乱变得更大。如果我在代码中首先放置后通知,然后是前通知,然后最后放置周围通知(即 (1)after-(2)before-(3)around),我得到以下输出:

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

对我来说,这是唯一完全有意义的输出。

但是,如果我先放置后通知,然后是环绕通知,同时将前通知放在最后(即 (1)after-(2)around-(3)before),我得到如果我考虑到先前排序的输出,以下输出对我来说也没什么意义:

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

在这种情况下,before-advice 在 'i' 绑定到 1 时被触发。我的猜测是这是因为 around-advice 首先被触发(因为顺序),而 before -advice 实际上是由在 around-advice 主体中调用 'proceed' 触发的。然而,遵循这个逻辑并不能解释在这个问题中首先讨论的顺序中生成的输出。

最后,改变顺序,我们首先有前建议,然后是后建议,然后是周围建议(即 (1)before-(2)after- (3)around) 根据 Eclipse 的 AspectJ 插件无效,因为这会生成 'circular advice precedence'。

有人可以解释一下在解释上述所有行为的同一方面内不同建议之间使用的优先级吗?

我一直在阅读有关该主题的文章 here,但我认为解释 inconclusive/doesn 与实现不符。 它说

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.

如果我理解正确的话,这意味着在这个问题中首先讨论的输出(即 (1)before-(2)around-(3)after-ordering)在最后一个应该有'1'行而不是“25”。

在同一方面声明的建议的默认优先级在文档中是这样规定的:

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.

不过您需要了解的是,由于建议是在字节代码中编织的方式,因此您在第一次订购时得到的结果应该如下所示:

 ---------------
| Around()      |
|   ----------  |
|  | Before() | |
|   ----------  |
|  | Call()   | |
|   ----------  |
 ---------------
| After()       |
 ---------------

i的值只在Around的范围内变化,这就是为什么After会打印出25的值。

在你的第二种情况下,排序给出如下内容:

 ---------------
| Before()      |
 ---------------
| Around()      |
|   ----------  |
|  | Call()   | |
|   ----------  |
|  | After()  | |
|   ----------  |
 ---------------

在这种情况下,值会按照您的预期打印出来。

实际上@XGouchet 的第一个草图不太正确,因为 before advice 没有出现在 around 的范围内,但是已经完成了在执行 around 之前。让我使用带有大括号的伪代码表示法来表达这种“词法范围”:

场景A:词法排序before -> around -> after:

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

场景 B:词法排序 after -> before -> around:

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

场景C:词法排序after -> around -> before:

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

引自 AspectJ 手册中关于方面优先级的章节中的 Determining 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.

现在还要记住方面的行为方式以及在某些情况下优先级的真正含义。还引用了 AspectJ 手册关于方面优先级的章节中的 Effects of 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.

用简单的英语:

  • 一个around advice wraps

    • other around or before lower[=160 的建议=] 优先级以及
    • 之后 更高 优先级的建议。

    为什么会这样?想象一下 around 建议是一对 beforeafter 关于类固醇的建议(可以修改方法参数和 return 值,可以处理异常,可以在 beforeafter 代码之间共享状态)。因此,与单独的 beforeafter 建议相同的授权规则适用如下。

  • A before 建议在委派或继续之前先执行。

  • after 建议在执行前首先委托。特别是(也许有点违反直觉),这意味着:

    • 如果 after 建议的优先级高于 around 建议,那么如果 around advice 在继续之前修改任何方法参数,after advice 也会受到修改的影响,因为 around advice 已经进行了在 after 建议开始之前使用修改后的参数到目标方法。

    • 但是,相同的较高优先级 after (returning) 建议不受影响,如果较低优先级 around 建议修改 return 值,因为 after (returning) 建议在 around 建议之前执行post-由于优先级较高,请继续代码。

    • 相反,如果 after 建议的优先级低于 around 建议,那么如果 around 建议在继续之前修改任何方法参数,after 建议不受修改的影响,因为它在 around[= 的上下文之外运行154=] advice,不是被它包裹了。

    • 但是,相同的较低优先级 after (returning) 如果较高优先级 around 建议修改 return 值,因为它在后者已经完成 运行 之后运行。

下面是一个扩展示例,说明我刚刚写的内容:

驱动申请:

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);
    }
}

方面,变体 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);
    }

}

伪代码,变体1:

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

控制台输出,变体 1:

请注意,“after1”在“after2”之前打印,尽管它的优先级较低,因为 after 首先委托,然后执行,如上所述。

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

方面,变体 2:

基本上和之前一样,只有第一个after的advice具有最高的优先级。

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);
    }

}

伪代码,变体2:

这一次,因为after1的优先级高于around建议,它在joinpoint之后立即执行 returns,因此在 proceed() 之后发生的任何事情之前 around advices.

(自言自语:我对这个解释不满意,也许有更好的说法。也许答案可以改写,使用一个伪 before/after 建议pair per around advice 而不是包装隐喻。)

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

控制台输出,变体 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