AspectJ AOP中如何排除对内部方法的拦截?

How to exclude interception of internal methods in AspectJ AOP?

我们正在将使用 Jboss AOP(基于代理)的应用程序迁移到具有编译时织入的 AspectJ AOP。 但是,我们不希望内部方法被 AspectJ 拦截,但这似乎是 AspectJ 的默认行为。

有多篇文章介绍了如何在 Spring AOP 中拦截内部方法调用。但是,找不到任何与使用 AspectJ 排除内部方法相关的帖子。我们希望使用 AspectJ 编译时编织来实现它承诺的 运行 时间性能改进。

如果另一个 class 的方法调用下面 class TestService 中的任何 public 方法,该调用应该被拦截。但是,不应拦截从 method1() 到 method2() 的内部调用。 我们只希望拦截器对每个对象只拦截一次。

public class TestService {
  public void method1() {
    …
    // We do not want the below internal call to be intercepted. 
    this.method2();
  }

  // If some other class's method calls this, intercept the call. But do not intercept the call from method1().
  public void method2() {
    ...     
  }
}

看点示例:

@Aspect
public class ServiceAspectJHydrationInterceptor {
    @Pointcut("execution(public * com.companyname.service..impl.*ServiceImpl.*(..))")
    public void serviceLayerPublicMethods() {}

    @Pointcut("@annotation(com.companyname.core.annotation.SkipHydrationInterception)")
    public void skipHydrationInterception() {}

    @Around("serviceLayerPublicMethods() && !skipHydrationInterception()")
    public Object invoke(ProceedingJoinPoint pjp) throws Throwable {
        …
    }
}

排除内部方法调用拦截的行为在Spring AOP 中是默认的,因为它是基于代理的。有没有办法使用 AspectJ 和编译时编织来实现排除内部方法拦截?

软件详情: Spring 版本:3.2.14。 JDK 版本:1.8。 maven 插件 codehaus “aspectj-maven-plugin” version 1.7 用于编译时编织。

您可以使用模式 execution(...) && !cflowbelow(execution(...))。这对性能不利,因为必须在 运行 期间而不是在编译期间检查执行路径(认为调用堆栈),但它可以满足您的要求。由于 AspectJ 的 non-proxy 性质以及与其他 AOP 框架相比可用的连接点和切入点的集合更大,例如拦截私有或静态方法,请注意一些关键差异。

现在这里有一个与您描述的内容一致的小例子:

package de.scrum_master.core.annotation;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(METHOD)
public @interface SkipHydrationInterception {}
package de.scrum_master.service.foo.bar.impl;

import de.scrum_master.core.annotation.SkipHydrationInterception;

public class MyServiceImpl {
  public void method1() {
    // We do not want the below internal call to be intercepted.
    method2();
  }

  public void method2() {
    // If some other class's method calls this, intercept the call. But do not
    // intercept the call from method1().
  }

  @SkipHydrationInterception
  public void method3() {
    // Always skip this method one due to the annotation.

    // Should this one be intercepted or not?
    // method1();
  }

  public static void main(String[] args) {
    MyServiceImpl service = new MyServiceImpl();
    service.method1();
    System.out.println("-----");
    service.method2();
    System.out.println("-----");
    service.method3();
  }
}
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 ServiceAspectJHydrationInterceptor {
  @Pointcut("execution(public !static * de.scrum_master.service..impl.*ServiceImpl.*(..))")
  public void serviceLayerPublicMethods() {}

  @Pointcut("@annotation(de.scrum_master.core.annotation.SkipHydrationInterception)")
  public void skipHydrationInterception() {}

  @Pointcut("serviceLayerPublicMethods() && !skipHydrationInterception()")
  public void interceptMe() {}

  @Around("interceptMe() && !cflowbelow(interceptMe())")
  public Object invoke(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println(pjp);
    return pjp.proceed();
  }
}

现在 运行 驱动程序应用程序,您将看到此控制台日志:

execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
-----
execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
-----

这正是您想要的。到目前为止,一切都很好。另请注意执行切入点中的 !static 限定符,否则 static main(..) 将被拦截。

但是现在取消注释 method3() 正文中的 method1() 调用。控制台日志变为:

execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
-----
execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
-----
execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())

问题是:这是你想要的吗? method1() 是由一个方法调用的,由于其注释而被排除在拦截之外,但另一方面它也是一个内部方法调用,我喜欢称它为 self-invocation。解决方案取决于您的回答。

另请注意,从同一 class 的私有或受保护方法调用的 public 方法也将被拦截。所以cflow()cflowbelow()不关心self-invocation,只关心指定的控制流。

另一种情况:如果被拦截的 public 方法由于某种原因会调用另一个 class 而那个 class 会再次调用第一个 [=] 的 public 方法90=], !cflowbelow(...) 仍然会排除此调用被拦截,因为第一个调用已经在控制流中。

下一种情况:一个public*ServiceImpl方法调用另一个public*ServiceImpl方法。结果也将是第二个调用的方法不会被拦截,因为与其执行切入点匹配的东西已经在控制流(调用堆栈)中。

所以我的解决方案,即使我们调整切入点以涵盖一些极端情况,也与 proxy-based 解决方案的本质不同。如果在您的环境中可能会发生上述情况,您真的应该重构这些方面,以便做一些 book-keeping(保存状态)and/or 使用另一个实例化模型,例如 percflowbelow(但没有考虑过这一点,因为我不知道你的确切要求)。但是 SO 不是讨论论坛,我无法在这里为您提供增量帮助。请随时查看我的 SO 个人资料中的联系数据(例如 Telegram),如果您需要更多 in-depth 支持,请雇用我。但也许你也可以从这里拿走,我只是提一下。


更新:

好的,我想到了一种通过 AspectJ 模拟 proxy-based AOP 行为的方法。我不喜欢它,它要求您从 execution() 切入点切换到 call() 切入点,即您不再需要控制 (aspect-weave) 被调用方(执行的代码),而是调用方 (要拦截的方法调用的来源)。

您还需要在 if() 切入点的两个对象 this()target() 之间进行 运行时间检查。我也不喜欢那样,因为它会使您的代码变慢并且必须在许多地方进行检查。如果与要摆脱的 proxy-based 解决方案相比,您仍然可以达到性能改进的目标,则需要自己检查一下。记住,你现在正在模仿你想要废除的东西,哈哈。

让我们添加另一个 class 来模拟外部 class 调用目标 class 的相互作用,而不仅仅是从静态方法调用它,这不是足够的测试用例。

package de.scrum_master.service.foo.bar.impl;

public class AnotherClass {
  public void doSomething() {
    MyServiceImpl service = new MyServiceImpl();
    service.method1();
    System.out.println("-----");
    service.method2();
    System.out.println("-----");
    service.method3();
    System.out.println("-----");
  }
}

原始 MyServiceImpl class 我们通过记录更多内容并调用 AnotherClass.doSomething().

来扩展一点
package de.scrum_master.service.foo.bar.impl;

import de.scrum_master.core.annotation.SkipHydrationInterception;

public class MyServiceImpl {
  public void method1() {
    System.out.println("method1");
    method2();
  }

  public void method2() {
    System.out.println("method2");
  }

  @SkipHydrationInterception
  public void method3() {
    System.out.println("method3");
    method1();
  }

  public static void main(String[] args) {
    MyServiceImpl service = new MyServiceImpl();
    service.method1();
    System.out.println("-----");
    service.method2();
    System.out.println("-----");
    service.method3();
    System.out.println("-----");
    new AnotherClass().doSomething();
  }
}

改进后的外观是这样的:

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
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 ServiceAspectJHydrationInterceptor {
  @Pointcut("call(public !static * de.scrum_master.service..impl.*ServiceImpl.*(..))")
  public void serviceLayerPublicMethods() {}

  @Pointcut("@annotation(de.scrum_master.core.annotation.SkipHydrationInterception)")
  public void skipHydrationInterception() {}

  @Pointcut("serviceLayerPublicMethods() && !skipHydrationInterception()")
  public void interceptMe() {}

  @Pointcut("if()")
  public static boolean noSelfInvocation(ProceedingJoinPoint thisJoinPoint) {
    return thisJoinPoint.getThis() != thisJoinPoint.getTarget();
  }

  @Around("interceptMe() && noSelfInvocation(thisJoinPoint)")
  public Object invoke(ProceedingJoinPoint thisJoinPoint, JoinPoint.EnclosingStaticPart thisEnclosingStaticPart) throws Throwable {
    System.out.println(thisJoinPoint);
    System.out.println("  called by: " + thisEnclosingStaticPart);
    return thisJoinPoint.proceed();
  }
}

现在控制台日志如下所示:

call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
  called by: execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.main(String[]))
method1
method2
-----
call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
  called by: execution(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.main(String[]))
method2
-----
method3
method1
method2
-----
call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method1())
  called by: execution(void de.scrum_master.service.foo.bar.impl.AnotherClass.doSomething())
method1
method2
-----
call(void de.scrum_master.service.foo.bar.impl.MyServiceImpl.method2())
  called by: execution(void de.scrum_master.service.foo.bar.impl.AnotherClass.doSomething())
method2
-----
method3
method1
method2
-----

在我看来,这正是 Spring AOP 或 JBoss AOP 的行为方式,因为它们具有代理性质。也许我忘了什么,但我想我几乎已经涵盖了边角案例。

如果您在理解此解决方案时遇到问题,请告诉我。至于我使用的切入点指示符的含义,请查阅AspectJ手册。