2

Is it possible to create reentrant aspects with Spring AOP (or AspectJ)?

Here is an example:

@Log
public int calcFibonacci(int n) {
    if(n <= 1) {
        return n;
    } else {
        return calcFibonacci(n - 1) + calcFibonacci(n - 2);
    }
}

And Aspect:

@Aspect
public class LoggingAspect {

@Around("@annotation(log)")
public Object measure(ProceedingJoinPoint pjp, Log log) throws Throwable {
    // log some relevant information from log annotation and pjp
    ...
    return pjp.proceed();
}

}

Now I'd like to know how many times calcFibonacci was called (counting in recurrent calls).

Is there any way to accomplish this?

sanastasiadis
  • 1,182
  • 1
  • 15
  • 23
Sebastian Łaskawiec
  • 2,667
  • 15
  • 33
  • In `calcFibonacci()` you need the internal call to be something like this: `((CalcFibonaciiInterface) AopContext.currentProxy()).calcFibonacci()`. You haven't posted the complete class, but I assumed here your class containing `calcFibonacci` implements an interface (I called it CalcFibonacciInterface). – Andrei Stefan Jun 20 '14 at 11:01

2 Answers2

4

You need the following:

<aop:aspectj-autoproxy expose-proxy="true"/>

and the class that computes the value:

@Component
public class CalcFibonacci implements CalcFibonacciInterface {

    @Log
    public int calcFibonacci(int n) {
        if(n <= 1) {
            return n;
        } else {
            return ((CalcFibonacciInterface) AopContext.currentProxy()).calcFibonacci(n - 1) 
                    + ((CalcFibonacciInterface) AopContext.currentProxy()).calcFibonacci(n - 2);
        }
    }
}

Relevant documentation section is here.

Andrei Stefan
  • 51,654
  • 6
  • 98
  • 89
  • I find it rather weird that non-aspect code should have knowledge of and even explicitly call aspect-related code. It contradicts everything AOP is about. – kriegaex Jun 22 '14 at 22:54
  • @kriegaex It's not weird, it's ugly and the Spring documentation link I provided above explicitly recommends against this. The only real solution to the question is to change the code so that there won't be internal calls (or, in this case, recursive method calls). There are [non-recursive solutions to Fibonacci algorithm](http://stackoverflow.com/questions/9122277/what-is-a-non-recursive-solution-for-fibonacci-like-sequence-in-java), but the question in this post was a generic one, Fibonacci being just an example. – Andrei Stefan Jun 23 '14 at 04:38
  • 1
    The question was: "Is it possible to create reentrant aspects with Spring AOP **(or AspectJ)**?" In a while I am going to present a solution in AspectJ, I just need to do a little errand before that. See you later. – kriegaex Jun 23 '14 at 05:06
  • That's true. Looking forward to your solution. – Andrei Stefan Jun 23 '14 at 06:20
  • 1
    @kriegaex The problem is not so much AOP but the approach Spring takes in applying AOP. It uses proxies which means only method calls INTO an object will have AOP applied. Internal method calls will not be intercepted, hence you need to get the actual object and call the method again. That or use load- or compile time weaving to apply the aspects. – M. Deinum Jun 23 '14 at 08:08
  • @M.Deinum: I am fully aware of Spring AOP vs. AspectJ compatibilities, differences, limitations and options to combine both or replace Spring AOP by the much more powerful AspectJ, which still nicely integrates with Spring. You can use compile-time weaving or load-time weaving, whatever you prefer. No need to lecture me. :-) – kriegaex Jun 23 '14 at 08:17
3

Okay, you cannot solve this elegantly in Spring AOP - see my first remark to Andrei Stefan's answer. If in AOP the application code needs to know about an aspect's existence or even call aspect-related code, this is bad design and anti-AOP. Thus, here I have an AspectJ solution for you.

First of all, in AspectJ there is more than just execution() pointcuts, e.g. call(). Thus, just counting joinpoints annotated by @Log would yield a result twice as big as the actual number of recursive calls to calcFibonacci(int). Because of this, the pointcut should not be just

@annotation(log)

but

execution(* *(..)) && @annotation(log)

Actually, this still is not enough because what if multiple methods contain @Log annotations? Should those calls all be counted? No, only those to calcFibonacci(int)! So we should restrict the "Fibonacci call counter" even more to something like:

execution(* *..Application.calcFibonacci(int)) && @annotation(log)

Here is some fully compileable sample code:

Annotation:

package de.scrum_master.app;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Log {}

Application with recursive Fibonacci method:

package de.scrum_master.app;

public class Application {
    public static void main(String[] args) {
        int fibonacciNumber = 6;
        System.out.println("Fibonacci #" + fibonacciNumber + " = " + new Application().calcFibonacci(fibonacciNumber));
    }

    @Log
    public int calcFibonacci(int n) {
        return n <= 1 ? n : calcFibonacci(n - 1) + calcFibonacci(n - 2);
    }
}

Aspect, version 1:

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

import de.scrum_master.app.Log;

@Aspect
public class LoggingAspect {
    int count = 0;

    @Before("execution(* *..Application.calcFibonacci(int)) && @annotation(log)")
    public void measure(JoinPoint thisJoinPoint, Log log) {
        System.out.println(thisJoinPoint + " - " + ++count);
    }
}

Output, version 1:

execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 1
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 2
(...)
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 24
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 25
Fibonacci #6 = 8

Now, what if we call the Fibonacci method twice?

int fibonacciNumber = 6;
System.out.println("Fibonacci #" + fibonacciNumber + " = " + new Application().calcFibonacci(fibonacciNumber));
fibonacciNumber = 4;
System.out.println("Fibonacci #" + fibonacciNumber + " = " + new Application().calcFibonacci(fibonacciNumber));
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 1
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 2
(...)
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 24
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 25
Fibonacci #6 = 8
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 26
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 27
(...)
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 33
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 34
Fibonacci #4 = 3

Uh-oh!!!

We need to either reset the counter in between calls (and also make sure the whole thing is thread-safe by using a ThreadLocal or so) or use an aspect instantiation per control flow instead of a singleton aspect, which is what I will use here just for the fun of it:

Aspect, version 2:

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

import de.scrum_master.app.Log;

@Aspect("percflow(execution(* *.calcFibonacci(int)) && !cflowbelow(execution(* *.calcFibonacci(int))))")
public class LoggingAspect {
    int count = 0;

    @Before("execution(* *.calcFibonacci(int)) && @annotation(log)")
    public void measure(JoinPoint thisJoinPoint, Log log) {
        System.out.println(thisJoinPoint + " - " + ++count);
    }
}

Note:

  • I have shortened the package and class specification to just *in order to make the source code more readable. You can just as well use de.scrum_master.app.Application or any abbreviation of it in order to avoid collisions with similar class/method names.
  • The @Aspect annotation now had a parameter which says: "Create one instance per execution of the Fibonacci method, but exclude recursive ones."

Output, version 2:

execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 1
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 2
(..)
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 24
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 25
Fibonacci #6 = 8
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 1
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 2
(..)
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 8
execution(int de.scrum_master.app.Application.calcFibonacci(int)) - 9
Fibonacci #4 = 3

Now, that looks much better. :)))

Enjoy!

Community
  • 1
  • 1
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • 1
    But this still won't work with Spring AOP as that uses a proxy based approach. For this to work you would need either load- or compile time weaving. With plain Spring AOP it will still fail. – M. Deinum Jun 23 '14 at 08:07
  • @M.Deinum: The original poster asked for Spring **or AspectJ** solutions. Spring AOP with its dynamic proxy approach is not only too limited but also too slow for my taste. But that is just personal opinion. Why use a musket if I can use a raygun? – kriegaex Jun 23 '14 at 08:08
  • 1
    Correct but Spring can also use AspectJ aspects (see the reference guide :) ). However when expecting this to work with Spring it will fail due to the proxy based approach. The main problem is not the aspect but the means of applying that aspect. – M. Deinum Jun 23 '14 at 08:10
  • No, if you use full AspectJ within Spring it will work because AspectJ does not need proxies. I am talking about AspectJ, not merely AspectJ-compatible syntax of Spring AOP. Furthermore, my introductory comment clearly says that it is an AspectJ solution and explains why. Spring AOP is just not made for this use case. – kriegaex Jun 23 '14 at 08:11
  • 1
    The only way you can use full AspectJ is with either load- or compile time weaving and IMHO you forgot to mention that in your answer (which is otherwise a great one BTW). I just wanted to make that clear and it would improve the answer, especially if you would add why it doesn't work with Spring AOP (proxies). – M. Deinum Jun 23 '14 at 09:18
  • +1, but I agree with @M.Deinum that you need to specify the missing bits. – Andrei Stefan Jun 23 '14 at 09:34
  • 1
    kriegaex, M.Deinum - Thank you for your comments and very helpful answers! I managed to implement the final solution based on your answers and compile-time aspect weaving (I prefer to enhance the code once and not using javaagents). – Sebastian Łaskawiec Jun 23 '14 at 10:55