0
votes

I'm migrating some code that uses Spring AOP to AspectJ aspects (weaved at compile time). I'm looking for feedback on how I can modify the pointcut so that that they behave the same after the migration?

Currently Spring AOP Aspects only function as 'Proxies' and therefore only work from external callers on public/interface methods. Now that I've switched to AspectJ weaving; even method calls from within the class to itself are being weaved.

This is causing me a big headache and I was wondering if i can change the pointcut to somehow still behave as if it was still a proxy ? (i.e. exclude calls from within itself at any point in the type hierarchy such as calling an inherited function)

2
What is your goal when switching to AspectJ? Usually I see users switching because they want to intercept internally called methods. Why would you want to switch and keep the old behaviour? Please help me understand this in order to enable me to make a good suggestion.kriegaex
@kriegaex : I'd like to use some new aspects where the pointcut should intercept internally called methods. However in the migration I'd like to not touch the aspects already written. The aspects written however make assumptions that they will not be called on internally called methods (i.e. they were developed with Spring AOP)Setheron

2 Answers

4
votes

I think Jigish's idea to mix aspect styles is a good way to migrate or even to continue on a permanent basis, as long as you do not have any compelling issues (e.g. performance) with Spring.

Now, having said that, I have a workaround for you just in case you do need to exclude internal calls in full-blown AspectJ for any reason. I do not think it is elegant, but it works. Here is a proof of concept:

Two sample application classes:

These classes call their own methods internally, but also other classes' methods. E.g. Foo.fooOne(Bar) calls Bar.doSomethingBarish() externally, but also Foo.fooTwo(int) internally.

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

Driver application with main method:

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

Sample aspect including internal calls:

This is the usual way to write aspects. It reproduces your problem. I am using a call() pointcut here instead of execution() in order to have access to both caller (JoinPoint.EnclosingStaticPart) and callee (JoinPoint) joinpoints and be able to print them for illustration. In an execution() pointcut both values would be identical.

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

Console output:

Here you can see nicely how

  • Application calls both Foo and Bar methods,
  • Foo calls a Bar method, but also its own ones internally,
  • Bar calls a Foo method, but also its own ones internally.
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))

Improved aspect dynamically excluding internal calls:

Now we need to compare if the caller (this() pointcut in native aspectJ syntax) is identical to the callee (target() pointcut). If so, we want to skip advice execution. There are two ways to get caller/callee references in AspectJ:

  • binding them to parameters in the poinctut via this() and/or target(). There is one caveat here, though: If this() or target() are null the poinctut will not match, i.e. static methods as callers or callees are ruled out. In my example I want to see the calls by Application.main(..) though, so I will only bind the target/callee in the pointcut, but not the caller/this object.
  • determining them dynamically from within an executing advice via JoinPoint.getThis() and/or JoinPoint.getTarget(). This works nicely, but the caveat here is that it is probably a bit slower and that the advice will execute even in cases in which you would like to exclude static callers/callees right away.

Here we choose a mixed approach, including static callers, but excluding static callees in order to demonstrate both variants:

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

Console output:

As you can see, we have reduced the output to only the calls we are interested in. If you also want to exclude the static caller Application.main(..), just bind this() directly.

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

If in special cases you know the exact class name targeted by an advice, you might be able to use cflow() in order to exclude internal calls without ugly if constructs, but I have not thought it through and not tried either. It does not work in general cases anyway.


Update 1:

I played around some more. Here is an alternative using execution() instead of call(). Thus, it cannot rely on an enclosing joinpoint but needs to analyse the current callstack. I have not benchmarked the performance against the solution described above, but it will definitely weave fewer joinpoints. Furthermore, it uses if() right within its pointcut instead of using an if statement inside the advice. The condition is still determined dynamically during runtime, not statically while weaving the code, but I guess that is impossible here anyway because it depends on the control flow.

Just for the fun of it I am presenting the solution in both native and annotation-based syntax. I personally prefer native syntax because it it more expressive IMHO).

Alternative solution in native AspectJ syntax:

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

Alternative solution in @AspectJ syntax:

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

Console output for alternative solution (identical for both syntax variants):

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())

Update 2:

Here is another variant using execution() plus some reflection via Class.isAssignableFrom(Class) which should also work with class hierarchies and interfaces:

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
votes

The logical solution seems to be to mix AspectJ AOP and Spring AOP as mentioned in this section of Spring documentation. You should be able to use AspectJ AOP for specific classes and keep the rest of Spring AOP as is.

Here's the relevant text:

You can do this by using one or more elements inside the declaration. Each element specifies a name pattern, and only beans with names matched by at least one of the patterns will be used for Spring AOP autoproxy configuration:

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

Note that I haven't tested this myself but seems to be the perfect fit for your situation.