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 没有同步会破坏方面,使其线程不安全。