java - AspectJ 处理多个匹配建议

标签 java aop aspectj pointcut aspects

我在 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();
    ...
}

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

最佳答案

您的方法存在很大问题,因为您手动从另一个建议中调用一个建议。这不是应用 AOP 的方式。请让 AspectJ 根据它们各自的切入点来决定执行哪些建议。您将一个建议委托(delegate)给另一个建议的方式,您甚至可以调用一个本身不匹配的建议。没有 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();
    }
}

方面:

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

控制台日志:

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

你能看出你的方法有多不健康吗?一个不匹配的通知被匹配的通知调用。这会产生一些非常出乎意料的行为 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;
    }
}

新的控制台日志:

>>> 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())

如您所见,AspectJ 或 Spring AOP 在连接点周围包裹了多个匹配建议,例如洋葱皮,只有最里面的 proceed() 调用实际连接点,而外层调用内部连接点,确保每个连接点只执行一次。没有必要比 AOP 框架更聪明,可能会造成损害(参见我的第一个示例)。

还有一件事:如果多个切面有匹配的切入点,您可以通过 AspectJ 中的 @DeclarePrecedence 影响它们的执行顺序,但在单个切面中,您对执行顺序没有影响,或者至少没有影响你不应该依赖它。在 Spring AOP 中,您可以使用 @Order 注释来确定方面的优先级,但是对于来自同一方面的多个通知,顺序也未定义,另见 Spring manual .


2016 年 2 月 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);
    }
}

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

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

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

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

您可以很容易地识别出如何为每个连接点触发双重建议。

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

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

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

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

最后但并非最不重要的一点是,您可以有两个略有不同的切入点 - 一个带有空 args() 一个带有 args(request, ..) - 两者都有正如我在其中一条评论中所说,它可以将参数处理、日志记录和异常处理委托(delegate)给辅助方法以避免重复:

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

控制台日志应该和之前的完全一样。


更新 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;
    }
}

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

关于java - AspectJ 处理多个匹配建议,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35599765/

相关文章:

java - 除了最大长度之外,java中的字符串还有什么限制?

java - 有没有人在 Tomcat 中成功使用 JBoss AOP?

Spring AOP : What's the difference between JoinPoint and PointCut?

java - IntelliJ IDEA Aspectj(Ajc 编译器)在每次 Make 之后加载类需要花费太多时间

java - 注释字段的拦截分配的切入点

java - Streams API 出现奇怪的死锁

java - 如何将实际数字换算为美元值(value)

java - Selenium 和 xpath : finding a div with a class/id and verifying text inside

spring - 在 Java 7 中使用 AspectJ AOP 时出错

spring - 在 DAO 中执行 Spring AOP 环绕通知在服务中返回 null