我认为 Jigish 向 mix aspect styles 提出的想法是迁移甚至永久继续的好方法,只要您对 Spring 没有任何令人信服的问题(例如性能)。
现在,话虽如此,我为您提供了一个解决方法,以防您出于任何原因确实需要在成熟的 AspectJ 中排除内部调用。我不认为它很优雅,但它确实有效。这是一个概念证明:
两个示例应用程序类:
这些类在内部调用它们自己的方法,但也调用其他类的方法。例如。 Foo.fooOne(Bar) 在外部调用 Bar.doSomethingBarish(),但也在内部调用 Foo.fooTwo(int)。
package de.scrum_master.app;
public class Foo {
public void doSomethingFooish() {
fooTwo(22);
}
public void fooOne(Bar bar) {
bar.doSomethingBarish();
fooTwo(11);
}
public String fooTwo(int number) {
return fooThree("xxx");
}
public String fooThree(String text) {
return text + " " + text + " " + text;
}
}
package de.scrum_master.app;
public class Bar {
public void doSomethingBarish() {
barTwo(22);
}
public void barOne(Foo foo) {
foo.doSomethingFooish();
barTwo(11);
}
public String barTwo(int number) {
return barThree("xxx");
}
public String barThree(String text) {
return text + " " + text + " " + text;
}
}
带有main方法的驱动程序应用:
package de.scrum_master.app;
public class Application {
public static void main(String[] args) {
Foo foo = new Foo();
Bar bar = new Bar();
foo.fooOne(bar);
bar.barOne(foo);
}
}
示例方面包括内部调用:
这是编写方面的常用方法。它重现了您的问题。我在这里使用call() 切入点而不是execution(),以便访问调用者(JoinPoint.EnclosingStaticPart)和被调用者(JoinPoint)连接点并能够打印它们以进行说明。在execution() 切入点中,两个值将相同。
package de.scrum_master.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SampleAspect {
@Pointcut("call(public * de.scrum_master..*(..)) && !within(SampleAspect)")
public static void publicMethodCalls() {}
@Before("publicMethodCalls()")
public void myPointcut(
JoinPoint thisJoinPoint,
JoinPoint.EnclosingStaticPart thisEnclosingJoinPointStaticPart
) {
System.out.println(thisEnclosingJoinPointStaticPart + " -> " + thisJoinPoint);
}
}
控制台输出:
在这里你可以很好地看到如何
-
Application 调用 Foo 和 Bar 方法,
-
Foo 调用 Bar 方法,但在内部也调用它自己的方法,
-
Bar 调用 Foo 方法,但在内部也调用它自己的方法。
execution(void de.scrum_master.app.Application.main(String[])) -> call(void de.scrum_master.app.Foo.fooOne(Bar))
execution(void de.scrum_master.app.Foo.fooOne(Bar)) -> call(void de.scrum_master.app.Bar.doSomethingBarish())
execution(void de.scrum_master.app.Bar.doSomethingBarish()) -> call(String de.scrum_master.app.Bar.barTwo(int))
execution(String de.scrum_master.app.Bar.barTwo(int)) -> call(String de.scrum_master.app.Bar.barThree(String))
execution(void de.scrum_master.app.Foo.fooOne(Bar)) -> call(String de.scrum_master.app.Foo.fooTwo(int))
execution(String de.scrum_master.app.Foo.fooTwo(int)) -> call(String de.scrum_master.app.Foo.fooThree(String))
execution(void de.scrum_master.app.Application.main(String[])) -> call(void de.scrum_master.app.Bar.barOne(Foo))
execution(void de.scrum_master.app.Bar.barOne(Foo)) -> call(void de.scrum_master.app.Foo.doSomethingFooish())
execution(void de.scrum_master.app.Foo.doSomethingFooish()) -> call(String de.scrum_master.app.Foo.fooTwo(int))
execution(String de.scrum_master.app.Foo.fooTwo(int)) -> call(String de.scrum_master.app.Foo.fooThree(String))
execution(void de.scrum_master.app.Bar.barOne(Foo)) -> call(String de.scrum_master.app.Bar.barTwo(int))
execution(String de.scrum_master.app.Bar.barTwo(int)) -> call(String de.scrum_master.app.Bar.barThree(String))
动态改进方面排除内部调用:
现在我们需要比较调用者(原生 aspectJ 语法中的this() 切入点)是否与被调用者(target() 切入点)相同。如果是这样,我们想跳过建议执行。在 AspectJ 中有两种获取调用者/被调用者引用的方法:
- 通过
this() 和/或target() 将它们绑定到指针中的参数。不过,这里有一个警告:如果 this() 或 target() 是 null,则点将不匹配,即排除作为调用者或被调用者的静态方法。在我的示例中,我希望看到 Application.main(..) 的调用,所以我只会在切入点中绑定目标/被调用者,而不是调用者/此对象。
- 通过
JoinPoint.getThis() 和/或JoinPoint.getTarget() 从正在执行的建议中动态确定它们。这很好用,但需要注意的是,它可能会慢一些,即使在您想立即排除静态调用者/被调用者的情况下,建议也会执行。
这里我们选择混合方法,包括静态调用者,但不包括静态调用者,以演示两种变体:
package de.scrum_master.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SampleAspect {
@Pointcut("call(public * de.scrum_master..*(..)) && !within(SampleAspect)")
public static void publicMethodCalls() {}
@Before("publicMethodCalls() && target(callee)")
public void myPointcut(
Object callee,
JoinPoint thisJoinPoint,
JoinPoint.EnclosingStaticPart thisEnclosingJoinPointStaticPart
) {
Object caller = thisJoinPoint.getThis();
if (caller == callee)
return;
System.out.println(thisEnclosingJoinPointStaticPart + " -> " + thisJoinPoint);
System.out.println(" caller = " + caller);
System.out.println(" callee = " + callee);
}
}
控制台输出:
如您所见,我们已将输出减少为仅我们感兴趣的调用。如果您还想排除静态调用者Application.main(..),只需直接绑定this()。
execution(void de.scrum_master.app.Application.main(String[])) -> call(void de.scrum_master.app.Foo.fooOne(Bar))
caller = null
callee = de.scrum_master.app.Foo@6a5c2445
execution(void de.scrum_master.app.Foo.fooOne(Bar)) -> call(void de.scrum_master.app.Bar.doSomethingBarish())
caller = de.scrum_master.app.Foo@6a5c2445
callee = de.scrum_master.app.Bar@47516490
execution(void de.scrum_master.app.Application.main(String[])) -> call(void de.scrum_master.app.Bar.barOne(Foo))
caller = null
callee = de.scrum_master.app.Bar@47516490
execution(void de.scrum_master.app.Bar.barOne(Foo)) -> call(void de.scrum_master.app.Foo.doSomethingFooish())
caller = de.scrum_master.app.Bar@47516490
callee = de.scrum_master.app.Foo@6a5c2445
如果在特殊情况下您知道通知所针对的确切类名,您可能可以使用cflow() 来排除内部调用而没有丑陋的if 构造,但我没有考虑过,也没有尝试过任何一个。无论如何,它在一般情况下不起作用。
更新 1:
我又玩了一些。这是使用execution() 而不是call() 的替代方法。因此,它不能依赖封闭的连接点,而是需要分析当前的调用堆栈。我没有针对上述解决方案对性能进行基准测试,但它肯定会编织更少的连接点。此外,它在其切入点内使用if(),而不是在建议中使用if 语句。条件仍然是在运行时动态确定的,而不是在编织代码时静态确定的,但我想这无论如何在这里是不可能的,因为它取决于控制流。
只是为了好玩,我以本机和基于注释的语法来介绍解决方案。我个人更喜欢原生语法,因为它更具表现力恕我直言)。
原生 AspectJ 语法的替代解决方案:
package de.scrum_master.aspect;
public aspect DemoAspect {
pointcut publicMethodCalls() :
execution(public !static * de.scrum_master..*(..)) &&
if(Thread.currentThread().getStackTrace()[3].getClassName() != thisJoinPointStaticPart.getSignature().getDeclaringTypeName());
before() : publicMethodCalls() {
System.out.println(thisJoinPoint);
}
}
@AspectJ 语法中的替代解决方案:
package de.scrum_master.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SampleAspect {
@Pointcut("execution(public !static * de.scrum_master..*(..)) && if()")
public static boolean publicMethodCalls(JoinPoint thisJoinPoint) {
return Thread.currentThread().getStackTrace()[3].getClassName() != thisJoinPoint.getSignature().getDeclaringTypeName();
}
@Before("publicMethodCalls(thisJoinPoint)")
public void myPointcut(JoinPoint thisJoinPoint) {
System.out.println(thisJoinPoint);
}
}
替代解决方案的控制台输出(两种语法变体相同):
execution(void de.scrum_master.app.Foo.fooOne(Bar))
execution(void de.scrum_master.app.Bar.doSomethingBarish())
execution(void de.scrum_master.app.Bar.barOne(Foo))
execution(void de.scrum_master.app.Foo.doSomethingFooish())
更新 2:
这是另一个使用execution() 的变体加上通过Class.isAssignableFrom(Class) 进行的一些反射,它也适用于类层次结构和接口:
package de.scrum_master.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.SoftException;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SampleAspect {
@Pointcut("execution(public !static * de.scrum_master..*(..)) && target(callee) && if()")
public static boolean publicMethodCalls(Object callee, JoinPoint thisJoinPoint) {
Class<?> callerClass;
try {
callerClass = Class.forName(
Thread.currentThread().getStackTrace()[3].getClassName()
);
} catch (Exception e) {
throw new SoftException(e);
}
Class<?> calleeClass = callee.getClass();
return !callerClass.isAssignableFrom(calleeClass);
}
@Before("publicMethodCalls(callee, thisJoinPoint)")
public void myPointcut(Object callee, JoinPoint thisJoinPoint) {
System.out.println(thisJoinPoint);
}
}