从Spring AOP转换为AspectJ

时间:2014-11-21 20:15:47

标签: java aspectj spring-aop aop

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

目前Spring AOP Aspects仅作为' Proxies'因此只能在公共/接口方法上使用外部调用者。现在我已经切换到AspectJ编织;甚至从类内部到自身的方法调用都被编织。

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

2 个答案:

答案 0 :(得分:4)

我认为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()从执行建议中动态确定它们。这很好用,但这里需要注意的是,它可能会慢一点,即使在你想立即排除静态调用者/被调用者的情况下,也会执行建议。

这里我们选择一种混合方法,包括静态调用者,但不包括静态callees以演示两种变体:

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()的替代方法。因此,它不能依赖于封闭的连接点,而是需要分析当前的callstack。我没有根据上述解决方案对性能进行基准测试,但它肯定会编织更少的连接点。此外,它在其切入点内使用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);
    }
}

答案 1 :(得分:1)

逻辑解决方案似乎是按照section of Spring documentation中提到的方式混合AspectJ AOP和Spring AOP。您应该能够将AspectJ AOP用于特定的类,并按原样保留Spring AOP的其余部分。

以下是相关文字:

您可以使用声明中的一个或多个元素来完成此操作。每个元素都指定一个名称模式,只有名称与至少一个模式匹配的bean才会用于Spring AOP autoproxy配置:

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

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