簡體   English   中英

AspectJ的多個匹配建議的處理

[英]AspectJ Handling of Multiple Matching Advices

我正在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根據它們各自的切入點來決定執行哪些建議。 從一個建議委派給另一個建議的方式,您甚至可以調用一個本身不匹配的建議。 不帶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手冊


經過評論中的一些討論后,歐洲中部時間2016年2月28日更新:

好的,我們稍微擴展一下驅動程序類,以便可以進行更多測試:

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中綁定第一個參數就像對一個或多個參數起作用的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;
    }
}

即使原始方法僅被調用一次,這也會使相同的建議觸發兩次,從而導致開銷,但是您可以在日志中看到開銷:

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

您可以輕松地識別出如何為每個聯接點激發雙重建議。

另外,您可以在運行時綁定參數,這不是很優雅,並且會給運行時帶來一些損失,但是效果很好:

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, ..) -兩者都可以將參數處理,日志記錄和異常處理委托給helper方法,以便避免重復,正如我在評論之一中所述:

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

這是可以接受的,也很優雅,因為它不會為每個連接點生成兩次字節碼。 這兩個切入點是互斥的,因此這是一件好事。 我推薦這種解決方案。

請參閱kriegaex的答案以了解更多詳細信息。 為了完整起見,請保留此處。

我最終實現了一個虛擬項目來對此進行測試。 答案是該方法將只執行一次 我已經實現了以下內容:

Aspects.java:

package base;

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 Aspects {
    @Pointcut("@annotation(java.lang.Deprecated)")
    public void hasErrorResponseMethod() {
    }

    @Around("hasErrorResponseMethod()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        System.out.println("In CHILD advice.");
        return around(point, null);
    }

    @Around("hasErrorResponseMethod() && args(request)")
    public Object around(ProceedingJoinPoint point, Object request) throws Throwable {
        System.out.println("In PARENT advice with request " + request);
        return point.proceed();
    }
}

Methods.java:

package base;

public class Methods {
    private static int COUNT = 1;
    @Deprecated
    public int method1(String param) {
        System.out.println(">>> In method1! Param: " + param);
        return COUNT++;
    }
}

applicationContext.xml中:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

    <context:component-scan base-package="base" annotation-config="true" />

    <aop:aspectj-autoproxy />

    <bean id="logAspect" class="base.Aspects"/>

    <bean id="methods" class="base.Methods"/>
    <bean id="main" class="base.Main"/>

</beans>

Main.java:

package base;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

        Methods methods = (Methods) context.getBean("methods");
        System.out.println("<<< Result: " + Methods.method1("REQUEST_VALUE"));
    }
}

輸出如下:

In PARENT advice with request REQUEST_VALUE
In CHILD advice.
In PARENT advice with request null
>>> In method1! Param: REQUEST_VALUE
<<< Result: 1

如您所見,該方法僅會被看起來更具體的建議調用一次。 知道AspectJ如何決定兩者中的哪一個將調用它仍然是很高興的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM