AspectJ处理多个匹配建议

Vla*_*zki 2 java aop aspectj aspects pointcut

我在Java中使用AspectJ来记录对某些方法的调用.我看过网上但无法找到答案:

当两个@Around建议与方法匹配时会发生什么?

具体来说,我正在使用两个@Around建议,如下所示:

@Around("condition1() && condition2() && condition3()")
public Object around(ProceedingJoinPoint point) {
    return around(point, null);
}

@Around("condition1() && condition2() && condition3() && args(request)")
public Object around(ProceedingJoinPoint point, Object request) {
    ...
    result = (Result) point.proceed();
    ...
}
Run Code Online (Sandbox Code Playgroud)

point.proceed()如果这两个建议都匹配,这会导致被调用两次(实际方法被调用两次)吗?

kri*_*aex 7

您的方法很成问题,因为您手动调用另一个建议.这不是应该如何应用AOP.请让AspectJ根据各自的切入点决定执行哪些建议.您从一个建议委托给另一个建议的方式甚至可以调用一个本身不匹配的建议.在没有Spring的简单AspectJ中的示例(尽管在Spring AOP中工作方式相同):

Java驱动应用程序:

package de.scrum_master.app;

public class Application {
    private static void doSomething() {
        System.out.println("Doing something");
    }

    public static void main(String[] args) {
        doSomething();
    }
}
Run Code Online (Sandbox Code Playgroud)

方面:

package de.scrum_master.aspect;

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

@Aspect
public class MyBogusAspect {
    @Around("execution(* doSomething(..))")
    public Object matchingAdvice(ProceedingJoinPoint thisJoinPoint) {
        System.out.println("matching advice called on joinpoint " + thisJoinPoint);
        return nonMatchingAdvice(thisJoinPoint);
    }

    @Around("execution(* doSomethingElse(..))")
    public Object nonMatchingAdvice(ProceedingJoinPoint thisJoinPoint) {
        System.out.println("non-matching advice called on joinpoint " + thisJoinPoint);
        return thisJoinPoint.proceed();
    }
}
Run Code Online (Sandbox Code Playgroud)

控制台日志:

matching advice called on joinpoint execution(void de.scrum_master.app.Application.doSomething())
non-matching advice called on joinpoint execution(void de.scrum_master.app.Application.doSomething())
Doing something
Run Code Online (Sandbox Code Playgroud)

你能看出你的方法有多不健康吗?否则将不匹配的建议由匹配的建议调用.这会产生一些非常意外的行为IMO.请不要这样做!

现在关于多个匹配建议的原始问题,这就是你应该如何做到的:

修改方面:

package de.scrum_master.aspect;

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

@Aspect
public class MyBetterAspect {
    @Around("execution(* doSomething(..))")
    public Object matchingAdvice(ProceedingJoinPoint thisJoinPoint) {
        System.out.println(">>> matching advice on " + thisJoinPoint);
        Object result = thisJoinPoint.proceed();
        System.out.println("<<< matching advice on " + thisJoinPoint);
        return result;
    }

    @Around("execution(* doSomething(..))")
    public Object anotherMatchingAdvice(ProceedingJoinPoint thisJoinPoint) {
        System.out.println(">>> another matching advice on " + thisJoinPoint);
        Object result = thisJoinPoint.proceed();
        System.out.println("<<< another matching advice on " + thisJoinPoint);
        return result;
    }
}
Run Code Online (Sandbox Code Playgroud)

新的控制台日志:

>>> matching advice on execution(void de.scrum_master.app.Application.doSomething())
>>> another matching advice on execution(void de.scrum_master.app.Application.doSomething())
Doing something
<<< another matching advice on execution(void de.scrum_master.app.Application.doSomething())
<<< matching advice on execution(void de.scrum_master.app.Application.doSomething())
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,AspectJ或Spring AOP包含多个匹配建议,如连接点周围的洋葱皮,只有最内层proceed()调用实际连接点,而外层调用内层连接,确保每个连接点只执行一次.你没有必要试图比AOP框架更聪明,可能造成损害(见我的第一个例子).

还有一件事:如果多个方面具有匹配的切入点,您可以通过@DeclarePrecedenceAspectJ 影响它们的执行顺序,但在一个方面,您不会影响执行顺序,或者至少您不应该依赖它.在Spring AOP中,您可以使用@Order注释来确定方面优先级,但是对于来自同一方面的多个建议,顺序也是未定义的,另请参阅Spring手册.


更新2016-02-28,欧洲中部时间18:30,经过评论中的一些讨论:

好的,我们稍微扩展了驱动程序类,以便我们可以测试更多:

package de.scrum_master.app;

public class Application {
    private static void doSomething() {
        System.out.println("Doing something");
    }

    private static String doSomethingElse(String text) {
        System.out.println("Doing something else");
        return text;
    }

    private static int doAnotherThing(int i, int j, int k) {
        System.out.println("Doing another thing");
        return (i + j) * k;
    }

    public static void main(String[] args) {
        doSomething();
        doSomethingElse("foo");
        doAnotherThing(11, 22, 33);
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,绑定AspectJ中的第一个参数就像args(request, ..)使用一个或多个参数一样简单.唯一的例外是零参数,在这种情况下切入点不会触发.所以要么我最终得到类似你所做的事情:

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 BoundFirstParameterAspect {
    @Pointcut("execution(* do*(..))")
    public static void myPointcut() {}

    @Around("myPointcut()")
    public Object matchingAdvice(ProceedingJoinPoint thisJoinPoint) {
        return anotherMatchingAdvice(thisJoinPoint, null);
    }

    @Around("myPointcut() && args(request, ..)")
    public Object anotherMatchingAdvice(ProceedingJoinPoint thisJoinPoint, Object request) {
        System.out.println(">>> another matching advice on " + thisJoinPoint);
        Object result = thisJoinPoint.proceed();
        System.out.println("<<< another matching advice on " + thisJoinPoint);
        return result;
    }
}
Run Code Online (Sandbox Code Playgroud)

哪个会使同一个建议触发两次,从而导致开销,即使原始方法只调用一次,但您可以看到日志中的开销:

>>> another matching advice on execution(void de.scrum_master.app.Application.doSomething())
Doing something
<<< another matching advice on execution(void de.scrum_master.app.Application.doSomething())
>>> another matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
>>> another matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
Doing something else
<<< another matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
<<< another matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
>>> another matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
>>> another matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
Doing another thing
<<< another matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
<<< another matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
Run Code Online (Sandbox Code Playgroud)

您可以轻松识别为每个连接点触发的双重建议.

或者,您可以在运行时绑定参数,这不是很优雅,并且会产生一点运行时损失,但效果非常好:

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 BoundFirstParameterAspect {
    @Pointcut("execution(* do*(..))")
    public static void myPointcut() {}

    @Around("myPointcut()")
    public Object matchingAdvice(ProceedingJoinPoint thisJoinPoint) {
        System.out.println(">>> matching advice on " + thisJoinPoint);
        Object[] args = thisJoinPoint.getArgs();
        Object request =  args.length > 0 ? args[0] : null;
        System.out.println("First parameter = " + request);
        Object result = thisJoinPoint.proceed();
        System.out.println("<<< matching advice on " + thisJoinPoint);
        return result;
    }
}
Run Code Online (Sandbox Code Playgroud)

这避免了双重建议执行以及代码重复,并产生以下控制台输出:

>>> matching advice on execution(void de.scrum_master.app.Application.doSomething())
First parameter = null
Doing something
<<< matching advice on execution(void de.scrum_master.app.Application.doSomething())
>>> matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
First parameter = foo
Doing something else
<<< matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
>>> matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
First parameter = 11
Doing another thing
<<< matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
Run Code Online (Sandbox Code Playgroud)

最后,但并非最不重要的是,你可以有两个稍微不同的切入点 - 一个用空args(),一个用args(request, ..)- 都可以将参数处理,日志记录和异常处理委托给一个帮助方法,以避免重复,正如我在其中一个中所说的那样评论:

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 BoundFirstParameterAspect {
    @Pointcut("execution(* do*(..))")
    public static void myPointcut() {}

    @Around("myPointcut() && args()")
    public Object myAdvice(ProceedingJoinPoint thisJoinPoint) {
        return myAdviceHelper(thisJoinPoint, null);
    }

    @Around("myPointcut() && args(request, ..)")
    public Object myAdviceWithParams(ProceedingJoinPoint thisJoinPoint, Object request) {
        return myAdviceHelper(thisJoinPoint, request);
    }

    private Object myAdviceHelper(ProceedingJoinPoint thisJoinPoint, Object request) {
        System.out.println(">>> matching advice on " + thisJoinPoint);
        System.out.println("First parameter = " + request);
        Object result = thisJoinPoint.proceed();
        System.out.println("<<< matching advice on " + thisJoinPoint);
        return result;
    }
}
Run Code Online (Sandbox Code Playgroud)

控制台日志应与前一个完全相同.


更新2:

好吧,我刚才意识到空args()技巧也适用于你原来的想法,避免双重执行以及辅助方法:

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 BoundFirstParameterAspect {
    @Pointcut("execution(* do*(..))")
    public static void myPointcut() {}

    @Around("myPointcut() && args()")
    public Object myAdvice(ProceedingJoinPoint thisJoinPoint) {
        return myAdviceWithParams(thisJoinPoint, null);
    }

    @Around("myPointcut() && args(request, ..)")
    public Object myAdviceWithParams(ProceedingJoinPoint thisJoinPoint, Object request) {
        System.out.println(">>> matching advice on " + thisJoinPoint);
        System.out.println("First parameter = " + request);
        Object result = thisJoinPoint.proceed();
        System.out.println("<<< matching advice on " + thisJoinPoint);
        return result;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是可以接受的,因为它不会为每个连接点生成两次字节代码.这两个切入点是互斥的,所以这是一件好事.我推荐这个解决方案.