AspectJ - 带有 if() 表达式的切入点导致无限递归
AspectJ - Pointcut with if() expression causes infinite recursion
我正在尝试编写一个方面,如果系统 属性 具有某些特定值,它将拦截所有方法调用。但是我不需要拦截建议控制流中的任何方法。
我正在使用 !cflow(adviceexecution())
表达式来实现此目的,但它似乎无法与 if()
表达式结合使用。结果,由于无限递归,我得到 WhosebugError
。
看点代码:
@Aspect
public class SomeAspect {
@Pointcut("execution(* *.*(..)) && if()")
public static boolean allMethodCalls() {
return PropertyReader.hasSpecificProperty();
}
@Pointcut("cflow(adviceexecution())")
public void aspectCalls() {
}
@Before("allMethodCalls() && !aspectCalls()")
public void logSomething() {
// logging behavior
}
}
PropertyReader代码:
public class PropertyReader {
public static boolean hasSpecificProperty() {
return System.getProperty("specificProperty") != null;
}
}
adviceexecution()
将不匹配,因为您的动态 if()
切入点在 执行建议之前 被评估。毕竟,这就是 if()
的全部意义所在:决定是否应执行建议。
假设情况是这样的:
package de.scrum_master.app;
public class PropertyReader {
public static boolean hasSpecificProperty() {
return System.getProperty("specificProperty") != null;
}
public void doSomething(String info) {
System.out.println("Doing something " + info);
hasSpecificProperty();
}
public static void main(String[] args) {
System.clearProperty("specificProperty");
new PropertyReader().doSomething("with inactive property");
System.setProperty("specificProperty", "true");
new PropertyReader().doSomething("with active property");
System.clearProperty("specificProperty");
}
}
现在最简单的解决方案是将 hasSpecificProperty()
逻辑直接拉入方面本身,因为它很简单。您可以定义一个局部静态方法,也可以将其内联到 if()
切入点中:
package de.scrum_master.app;
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 SomeAspect {
@Pointcut("execution(* *(..)) && if()")
public static boolean allMethodCalls() {
return System.getProperty("specificProperty") != null;
}
@Pointcut("cflow(adviceexecution())")
public void aspectCalls() {}
@Before("allMethodCalls() && !aspectCalls()")
public void logSomething(JoinPoint thisJoinPoint) {
System.out.println(thisJoinPoint);
PropertyReader.hasSpecificProperty();
}
}
这将产生以下控制台日志:
Doing something with inactive property
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
Doing something with active property
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
如您所见,从应用程序甚至方面的建议调用 hasSpecificProperty()
都没有问题,因为在一个有问题的地方它是内联的。
如果要避免将方法内联或复制到方面,则需要在方面内进行手动记账,恐怕:
package de.scrum_master.app;
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 SomeAspect {
private static ThreadLocal<Boolean> isInPointcut = new ThreadLocal<Boolean>() {
@Override protected Boolean initialValue() { return false; }
};
@Pointcut("execution(* *(..)) && if()")
public static boolean allMethodCalls() {
if (isInPointcut.get())
return false;
isInPointcut.set(true);
boolean result = PropertyReader.hasSpecificProperty();
isInPointcut.set(false);
return result;
}
@Pointcut("cflow(adviceexecution()) || within(SomeAspect)")
public void aspectCalls() {}
@Before("allMethodCalls() && !aspectCalls()")
public void logSomething(JoinPoint thisJoinPoint) {
System.out.println(thisJoinPoint);
PropertyReader.hasSpecificProperty();
}
}
控制台日志是相同的。请注意 || within(SomeAspect)
是必要的,以避免捕获匿名 ThreadLocal
class.
更新: 刚刚提出了这个后续问题:
I don't quite understand why do we need ThreadLocal
instead of simple boolean
flag. Could you please explain?
简短的回答是:为了使方面线程安全。如果多个线程同时读写静态成员isInPointcut
- 要么您需要同步访问,这将成为性能瓶颈,因为所有线程都必须等待方面执行其许多动态检查,
- 或者您使用
ThreadLocal
变量,为每个线程提供一个独立的标志实例,从而使线程能够并发进行。
如果两者都不做,您的方面将中断,读取其他线程设置的标志的错误值。我会演示给你看。让我们按如下方式更改演示应用程序:
package de.scrum_master.app;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class PropertyReader {
private static int callCounter = 0;
private static final Random RANDOM = new Random();
public static boolean hasSpecificProperty() {
synchronized (RANDOM) {
callCounter++;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
e.printStackTrace();
}
return System.getProperty("specificProperty") != null;
}
public void doSomething(String info) {
System.out.println("Doing something " + info);
hasSpecificProperty();
}
public static int doStuff(final int numThreads, final boolean specificPropertyState) throws InterruptedException {
if (specificPropertyState)
System.setProperty("specificProperty", "true");
else
System.clearProperty("specificProperty");
List<Thread> threads = new ArrayList<>();
long startTime = System.currentTimeMillis();
callCounter = 0;
for (int i = 0; i < numThreads; i++) {
Thread newThread = new Thread(() -> {
new PropertyReader().doSomething("with active property");
});
threads.add(newThread);
newThread.start();
}
for (Thread thread : threads)
thread.join();
System.clearProperty("specificProperty");
System.out.println("Call counter = " + callCounter);
System.out.println("Duration = " + (System.currentTimeMillis() - startTime) + " ms");
return callCounter;
}
public static void main(String[] args) throws InterruptedException {
final int NUM_THREADS = 10;
int callCounterInactiveProperty = doStuff(NUM_THREADS, false);
int callCounterActiveProperty = doStuff(NUM_THREADS, true);
int callCounterDelta = callCounterActiveProperty - callCounterInactiveProperty;
if (callCounterDelta != 3 * NUM_THREADS)
throw new RuntimeException("Call counter delta should be " + 3 * NUM_THREADS + ", not " + callCounterDelta);
}
}
您可能需要一段时间才能理解这段代码。基本上我做了以下事情:
- 引入一个新成员
static int callCounter
目的是方法static boolean hasSpecificProperty()
可以用它来统计它被调用的频率。
- 引入一些辅助对象
static final Random RANDOM
(也可以是任何其他对象类型)来同步 callCounter
因为我们无法同步 callCounter
本身,即使我们将其设为 Integer
而不是 int
,因为在递增计数器时,我们总是必须创建一个不同于同步的新 Integer
实例。我试过了,有时候算错了。相信我,我试过了。
- 通过向其添加
Thread.sleep(25)
使 hasSpecificProperty()
变慢,从而引发并发问题。你自己说你那个方法的版本比你在问题中展示的那个更复杂。
- 引入一种新方法
static int doStuff(final int numThreads, final boolean specificPropertyState)
,它创建用户定义数量的线程,并 运行 与系统并发 属性 specificProperty
设置或取消设置取决于用户如何调用 doStuff(..)
。然后该方法等待所有线程完成,打印持续时间和 returns callCounter
的当前值。如果方面工作正常,对于相同的方法参数,此 return 值应始终相同。
main(..)
现在调用 doStuff(..)
两次,第一次使用非活动系统 属性,然后使用活动系统。两个变体之间应该有差异(增量),因为如果 属性 处于活动状态,hasSpecificProperty()
会更频繁地执行,因为从方面建议 logSomething(..)
中它被调用并且该建议只会被如果系统 属性 根据 if()
切入点确定处于活动状态,则执行。
现在,如果我们 运行 控制台日志显示的程序(缩短了一点):
Doing something with active property
Doing something with active property
(...)
Doing something with active property
Doing something with active property
Call counter = 40
Duration = 151 ms
execution(void de.scrum_master.app.PropertyReader.lambda[=15=]())
execution(void de.scrum_master.app.PropertyReader.lambda[=15=]())
(...)
execution(void de.scrum_master.app.PropertyReader.lambda[=15=]())
execution(void de.scrum_master.app.PropertyReader.lambda[=15=]())
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
(...)
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
Doing something with active property
Doing something with active property
(...)
Doing something with active property
Doing something with active property
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
(...)
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
Call counter = 70
Duration = 180 ms
调用计数器总是相差 3 * NUM_THREADS
,因为在活动系统 属性 中,每个线程将拦截三个方法执行,因此建议 运行s 3 次并调用 hasSpecificProperty()
每次也是。
现在如果我们"simplify"(从而打破)这样的方面:
package de.scrum_master.app;
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 SomeAspect {
private static boolean isInPointcut = false;
@Pointcut("execution(* *(..)) && if()")
public static boolean allMethodCalls() {
if (isInPointcut)
return false;
isInPointcut = true;
boolean result = PropertyReader.hasSpecificProperty();
isInPointcut = false;
return result;
}
@Pointcut("cflow(adviceexecution()) || within(SomeAspect)")
public void aspectCalls() {}
@Before("allMethodCalls() && !aspectCalls()")
public void logSomething(JoinPoint thisJoinPoint) {
System.out.println(thisJoinPoint);
PropertyReader.hasSpecificProperty();
}
}
控制台日志更改为:
Doing something with active property
Doing something with active property
(...)
Doing something with active property
Doing something with active property
Call counter = 13
Duration = 161 ms
Doing something with active property
Doing something with active property
(...)
execution(void de.scrum_master.app.PropertyReader.lambda[=17=]())
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
Doing something with active property
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
Call counter = 16
Duration = 190 ms
Exception in thread "main" java.lang.RuntimeException: Call counter delta should be 30, not 3
at de.scrum_master.app.PropertyReader.main(PropertyReader.java:61)
呃哦!计数以意想不到的方式不同,您还看到建议 运行 仅一次,之后标志的状态被搞乱了。因此,您的日志记录、跟踪或该方面应该做的任何其他事情都会失败。
现在我们可以通过制作 if()
切入点方法 synchronized
:
来快速解决这个问题
public static synchronized boolean allMethodCalls(JoinPoint thisJoinPoint)
这有效,但是 运行每次调用 doStuff(..)
的时间从 ~190 毫秒增加到 ~800 毫秒,即比以前慢 4 倍:
Doing something with active property
(...)
Doing something with active property
Call counter = 40
Duration = 821 ms
execution(void de.scrum_master.app.PropertyReader.lambda[=19=]())
(...)
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
Call counter = 70
Duration = 802 ms
喜欢就自己试试吧。现在经过这么长的解释,我认为你同意 ThreadLocal
比简单的 boolean
更好,即使后者可以通过同步切入点方法来工作。但是只有 boolean
没有同步会破坏方面,使其线程不安全。
我正在尝试编写一个方面,如果系统 属性 具有某些特定值,它将拦截所有方法调用。但是我不需要拦截建议控制流中的任何方法。
我正在使用 !cflow(adviceexecution())
表达式来实现此目的,但它似乎无法与 if()
表达式结合使用。结果,由于无限递归,我得到 WhosebugError
。
看点代码:
@Aspect
public class SomeAspect {
@Pointcut("execution(* *.*(..)) && if()")
public static boolean allMethodCalls() {
return PropertyReader.hasSpecificProperty();
}
@Pointcut("cflow(adviceexecution())")
public void aspectCalls() {
}
@Before("allMethodCalls() && !aspectCalls()")
public void logSomething() {
// logging behavior
}
}
PropertyReader代码:
public class PropertyReader {
public static boolean hasSpecificProperty() {
return System.getProperty("specificProperty") != null;
}
}
adviceexecution()
将不匹配,因为您的动态 if()
切入点在 执行建议之前 被评估。毕竟,这就是 if()
的全部意义所在:决定是否应执行建议。
假设情况是这样的:
package de.scrum_master.app;
public class PropertyReader {
public static boolean hasSpecificProperty() {
return System.getProperty("specificProperty") != null;
}
public void doSomething(String info) {
System.out.println("Doing something " + info);
hasSpecificProperty();
}
public static void main(String[] args) {
System.clearProperty("specificProperty");
new PropertyReader().doSomething("with inactive property");
System.setProperty("specificProperty", "true");
new PropertyReader().doSomething("with active property");
System.clearProperty("specificProperty");
}
}
现在最简单的解决方案是将 hasSpecificProperty()
逻辑直接拉入方面本身,因为它很简单。您可以定义一个局部静态方法,也可以将其内联到 if()
切入点中:
package de.scrum_master.app;
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 SomeAspect {
@Pointcut("execution(* *(..)) && if()")
public static boolean allMethodCalls() {
return System.getProperty("specificProperty") != null;
}
@Pointcut("cflow(adviceexecution())")
public void aspectCalls() {}
@Before("allMethodCalls() && !aspectCalls()")
public void logSomething(JoinPoint thisJoinPoint) {
System.out.println(thisJoinPoint);
PropertyReader.hasSpecificProperty();
}
}
这将产生以下控制台日志:
Doing something with inactive property
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
Doing something with active property
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
如您所见,从应用程序甚至方面的建议调用 hasSpecificProperty()
都没有问题,因为在一个有问题的地方它是内联的。
如果要避免将方法内联或复制到方面,则需要在方面内进行手动记账,恐怕:
package de.scrum_master.app;
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 SomeAspect {
private static ThreadLocal<Boolean> isInPointcut = new ThreadLocal<Boolean>() {
@Override protected Boolean initialValue() { return false; }
};
@Pointcut("execution(* *(..)) && if()")
public static boolean allMethodCalls() {
if (isInPointcut.get())
return false;
isInPointcut.set(true);
boolean result = PropertyReader.hasSpecificProperty();
isInPointcut.set(false);
return result;
}
@Pointcut("cflow(adviceexecution()) || within(SomeAspect)")
public void aspectCalls() {}
@Before("allMethodCalls() && !aspectCalls()")
public void logSomething(JoinPoint thisJoinPoint) {
System.out.println(thisJoinPoint);
PropertyReader.hasSpecificProperty();
}
}
控制台日志是相同的。请注意 || within(SomeAspect)
是必要的,以避免捕获匿名 ThreadLocal
class.
更新: 刚刚提出了这个后续问题:
I don't quite understand why do we need
ThreadLocal
instead of simpleboolean
flag. Could you please explain?
简短的回答是:为了使方面线程安全。如果多个线程同时读写静态成员isInPointcut
- 要么您需要同步访问,这将成为性能瓶颈,因为所有线程都必须等待方面执行其许多动态检查,
- 或者您使用
ThreadLocal
变量,为每个线程提供一个独立的标志实例,从而使线程能够并发进行。
如果两者都不做,您的方面将中断,读取其他线程设置的标志的错误值。我会演示给你看。让我们按如下方式更改演示应用程序:
package de.scrum_master.app;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class PropertyReader {
private static int callCounter = 0;
private static final Random RANDOM = new Random();
public static boolean hasSpecificProperty() {
synchronized (RANDOM) {
callCounter++;
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
e.printStackTrace();
}
return System.getProperty("specificProperty") != null;
}
public void doSomething(String info) {
System.out.println("Doing something " + info);
hasSpecificProperty();
}
public static int doStuff(final int numThreads, final boolean specificPropertyState) throws InterruptedException {
if (specificPropertyState)
System.setProperty("specificProperty", "true");
else
System.clearProperty("specificProperty");
List<Thread> threads = new ArrayList<>();
long startTime = System.currentTimeMillis();
callCounter = 0;
for (int i = 0; i < numThreads; i++) {
Thread newThread = new Thread(() -> {
new PropertyReader().doSomething("with active property");
});
threads.add(newThread);
newThread.start();
}
for (Thread thread : threads)
thread.join();
System.clearProperty("specificProperty");
System.out.println("Call counter = " + callCounter);
System.out.println("Duration = " + (System.currentTimeMillis() - startTime) + " ms");
return callCounter;
}
public static void main(String[] args) throws InterruptedException {
final int NUM_THREADS = 10;
int callCounterInactiveProperty = doStuff(NUM_THREADS, false);
int callCounterActiveProperty = doStuff(NUM_THREADS, true);
int callCounterDelta = callCounterActiveProperty - callCounterInactiveProperty;
if (callCounterDelta != 3 * NUM_THREADS)
throw new RuntimeException("Call counter delta should be " + 3 * NUM_THREADS + ", not " + callCounterDelta);
}
}
您可能需要一段时间才能理解这段代码。基本上我做了以下事情:
- 引入一个新成员
static int callCounter
目的是方法static boolean hasSpecificProperty()
可以用它来统计它被调用的频率。 - 引入一些辅助对象
static final Random RANDOM
(也可以是任何其他对象类型)来同步callCounter
因为我们无法同步callCounter
本身,即使我们将其设为Integer
而不是int
,因为在递增计数器时,我们总是必须创建一个不同于同步的新Integer
实例。我试过了,有时候算错了。相信我,我试过了。 - 通过向其添加
Thread.sleep(25)
使hasSpecificProperty()
变慢,从而引发并发问题。你自己说你那个方法的版本比你在问题中展示的那个更复杂。 - 引入一种新方法
static int doStuff(final int numThreads, final boolean specificPropertyState)
,它创建用户定义数量的线程,并 运行 与系统并发 属性specificProperty
设置或取消设置取决于用户如何调用doStuff(..)
。然后该方法等待所有线程完成,打印持续时间和 returnscallCounter
的当前值。如果方面工作正常,对于相同的方法参数,此 return 值应始终相同。 main(..)
现在调用doStuff(..)
两次,第一次使用非活动系统 属性,然后使用活动系统。两个变体之间应该有差异(增量),因为如果 属性 处于活动状态,hasSpecificProperty()
会更频繁地执行,因为从方面建议logSomething(..)
中它被调用并且该建议只会被如果系统 属性 根据if()
切入点确定处于活动状态,则执行。
现在,如果我们 运行 控制台日志显示的程序(缩短了一点):
Doing something with active property
Doing something with active property
(...)
Doing something with active property
Doing something with active property
Call counter = 40
Duration = 151 ms
execution(void de.scrum_master.app.PropertyReader.lambda[=15=]())
execution(void de.scrum_master.app.PropertyReader.lambda[=15=]())
(...)
execution(void de.scrum_master.app.PropertyReader.lambda[=15=]())
execution(void de.scrum_master.app.PropertyReader.lambda[=15=]())
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
(...)
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
Doing something with active property
Doing something with active property
(...)
Doing something with active property
Doing something with active property
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
(...)
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
Call counter = 70
Duration = 180 ms
调用计数器总是相差 3 * NUM_THREADS
,因为在活动系统 属性 中,每个线程将拦截三个方法执行,因此建议 运行s 3 次并调用 hasSpecificProperty()
每次也是。
现在如果我们"simplify"(从而打破)这样的方面:
package de.scrum_master.app;
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 SomeAspect {
private static boolean isInPointcut = false;
@Pointcut("execution(* *(..)) && if()")
public static boolean allMethodCalls() {
if (isInPointcut)
return false;
isInPointcut = true;
boolean result = PropertyReader.hasSpecificProperty();
isInPointcut = false;
return result;
}
@Pointcut("cflow(adviceexecution()) || within(SomeAspect)")
public void aspectCalls() {}
@Before("allMethodCalls() && !aspectCalls()")
public void logSomething(JoinPoint thisJoinPoint) {
System.out.println(thisJoinPoint);
PropertyReader.hasSpecificProperty();
}
}
控制台日志更改为:
Doing something with active property
Doing something with active property
(...)
Doing something with active property
Doing something with active property
Call counter = 13
Duration = 161 ms
Doing something with active property
Doing something with active property
(...)
execution(void de.scrum_master.app.PropertyReader.lambda[=17=]())
execution(void de.scrum_master.app.PropertyReader.doSomething(String))
Doing something with active property
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
Call counter = 16
Duration = 190 ms
Exception in thread "main" java.lang.RuntimeException: Call counter delta should be 30, not 3
at de.scrum_master.app.PropertyReader.main(PropertyReader.java:61)
呃哦!计数以意想不到的方式不同,您还看到建议 运行 仅一次,之后标志的状态被搞乱了。因此,您的日志记录、跟踪或该方面应该做的任何其他事情都会失败。
现在我们可以通过制作 if()
切入点方法 synchronized
:
public static synchronized boolean allMethodCalls(JoinPoint thisJoinPoint)
这有效,但是 运行每次调用 doStuff(..)
的时间从 ~190 毫秒增加到 ~800 毫秒,即比以前慢 4 倍:
Doing something with active property
(...)
Doing something with active property
Call counter = 40
Duration = 821 ms
execution(void de.scrum_master.app.PropertyReader.lambda[=19=]())
(...)
execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty())
Call counter = 70
Duration = 802 ms
喜欢就自己试试吧。现在经过这么长的解释,我认为你同意 ThreadLocal
比简单的 boolean
更好,即使后者可以通过同步切入点方法来工作。但是只有 boolean
没有同步会破坏方面,使其线程不安全。