【问题标题】:Converting from Spring AOP to AspectJ从 Spring AOP 转换为 AspectJ
【发布时间】:2015-01-20 01:58:18
【问题描述】:

我正在将一些使用 Spring AOP 的代码迁移到 AspectJ 方面(在编译时编织)。我正在寻找有关如何修改切入点以使它们在迁移后表现相同的反馈?

目前 Spring AOP Aspects 仅用作“代理”,因此只能在公共/接口方法上由外部调用者工作。现在我已经切换到 AspectJ 编织;甚至从类内部对自身的方法调用也被编织。

这让我很头疼,我想知道我是否可以将切入点更改为以某种方式仍然表现得好像它仍然是代理? (即在类型层次结构中的任何点排除自身内部的调用,例如调用继承的函数)

【问题讨论】:

  • 切换到 AspectJ 的目标是什么?通常我看到用户切换是因为他们想要拦截内部调用的方法。你为什么要切换并保持旧的行为?请帮助我理解这一点,以便我提出好的建议。
  • @kriegaex :我想使用一些新方面,切入点应该拦截内部调用的方法。但是在迁移中我不想触及已经写好的方面。然而,编写的方面假设它们不会被内部调用的方法调用(即它们是使用 Spring AOP 开发的)

标签: java aspectj spring-aop aop


【解决方案1】:

我认为 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 调用 FooBar 方法,
  • 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);
    }
}

【讨论】:

  • 我刚刚用另一种方法更新了我的解决方案。看看吧。
  • 太棒了,我去看看。这可能有点复杂,因为我必须检查 superClass,因为我现在将切入点应用于抽象类。
  • 当你有一个类层次结构时,我的第一种方法可能比第二种方法更好,因为它实际上比较对象身份,即它应该可靠地检测this.doSomething(..) 类型的方法调用。新方法没有,因此在父方法从子类调用方法的情况下,它可能会失败,反之亦然。我在玩代码时忘了考虑这一点。
  • cflow 呢?如果我使用“!cflow”和“执行”会不允许重入吗?
  • 我仔细检查了抽象基类和接口:正如我所说,第一种方法有效,而第二种方法并非在所有情况下都有效。至于你关于cflow()的问题,太不准确了,我无法回答。给我一个具体的切入点,然后我可以告诉你它的作用。我不能说没有参数的 cflow()execution() 任何聪明的事情,因为参数会有所不同。
【解决方案2】:

合乎逻辑的解决方案似乎是混合 AspectJ AOP 和 Spring AOP,正如 section of Spring documentation 中提到的那样。您应该能够将 AspectJ AOP 用于特定的类,并保持 Spring AOP 的其余部分不变。

以下是相关文字:

您可以通过在声明中使用一个或多个元素来做到这一点。每个元素指定一个名称模式,只有名称与至少一种模式匹配的 bean 才会用于 Spring AOP 自动代理配置:

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>

请注意,我自己没有对此进行测试,但似乎非常适合您的情况。

【讨论】:

  • 我目前正在使用 AspectJ 编译时编织(方面库而不是 Spring AOP 附带的库)。我得看看这两个是否可以轻松互操作,我会报告!
猜你喜欢
  • 2015-12-17
  • 1970-01-01
  • 2010-12-09
  • 2012-07-25
  • 2012-03-28
  • 1970-01-01
  • 1970-01-01
  • 2015-12-01
  • 1970-01-01
相关资源
最近更新 更多