AspectJ-使用if()表达式的切入点导致无限递归

时间:2018-07-16 16:28:12

标签: java aspectj

我正在尝试编写一个方面,如果系统属性具有某些特定值,它将拦截所有方法调用。但是,我不需要从建议控制流中拦截任何方法。

我正在使用!cflow(adviceexecution())表达式来实现此目的,但是它似乎无法与if()表达式结合使用。结果,由于无限递归,我得到了StackOverflowError

方面代码:

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

1 个答案:

答案 0 :(得分:1)

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类。


更新:刚刚问了以下后续问题:

  

我不太明白为什么我们需要ThreadLocal而不是简单的boolean标志。你能解释一下吗?

简短的答案是:为了使方面成为线程安全的。如果有多个线程同时读写静态成员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,因为即使我们将其{{1 }}而不是callCounter,因为递增计数器时,我们总是必须创建一个新的Integer实例,该实例不同于同步的实例。我尝试过,有时算错了。相信我,我尝试过。
  • 通过向其中添加int使Integer变慢,从而引发并发问题。您自己说过,该方法的版本比您在问题中显示的方法复杂。
  • 引入一种新方法hasSpecificProperty(),该方法创建用户定义数量的线程,并根据用户调用Thread.sleep(25)的方式设置或取消设置系统属性static int doStuff(final int numThreads, final boolean specificPropertyState)并发运行它们。然后,该方法等待直到所有线程完成,然后打印持续时间并返回specificProperty的当前值。如果方面正常工作,则对于相同的方法参数,此返回值应始终相同。
  • doStuff(..)现在两次调用callCounter,首先使用无效的系统属性,然后使用有效的系统属性。两个变体之间应该有一个差异(增量),因为如果属性处于活动状态,则main(..)会更频繁地执行,因为从方面建议doStuff(..)内它会被调用,并且只有在系统处于执行状态时才会执行此建议属性由hasSpecificProperty()切入点确定。

现在,如果我们运行该程序,控制台日志会说(略):

logSomething(..)

调用计数器总是相差if(),因为在活动的系统属性下,每个线程将截获三个方法执行,因此建议运行3次,并且每次也调用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$0()) execution(void de.scrum_master.app.PropertyReader.lambda$0()) (...) execution(void de.scrum_master.app.PropertyReader.lambda$0()) execution(void de.scrum_master.app.PropertyReader.lambda$0()) 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

控制台日志更改为:

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 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$0()) 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的调用的运行时间从〜190 ms增加到〜800 ms,即比以前慢4倍:

public static synchronized boolean allMethodCalls(JoinPoint thisJoinPoint)

如果愿意,请尝试一下。现在,经过长时间的解释,我认为您同意doStuff(..)比简单的Doing something with active property (...) Doing something with active property Call counter = 40 Duration = 821 ms execution(void de.scrum_master.app.PropertyReader.lambda$0()) (...) execution(boolean de.scrum_master.app.PropertyReader.hasSpecificProperty()) Call counter = 70 Duration = 802 ms 更好,即使可以通过同步切入点方法使后者起作用。但是只有ThreadLocal没有同步会破坏该方面,使其成为线程不安全的。