2
votes

I've made a simple web application using Spring Boot and Spring Initializr and tried to write @Aspect with @Around advice.

When I add my custom annotation @RetryOnFailure to the controllers' endpoint method - it works, but when I add this annotation to the controllers' method, that executed by controllers endpoint - it doesn't work. I spend a lot of time for understanding the reason for such behavior, but without any result. So please help.

Project located here: https://github.com/zalizko/spring-aop-playground

@Aspect
@Component
public final class MethodRepeater {

    @Around("execution(* *(..)) && @annotation(RetryOnFailure)")
    public Object wrap(final ProceedingJoinPoint joinPoint) throws Throwable {
        // code is here
    }
}

So, my goal is that:

@RequestMapping
public String index() {
    inTry();
    return "OK";
}


@RetryOnFailure(attempts = 3, delay = 2, unit = TimeUnit.SECONDS)
public void inTry() {
    throw new RuntimeException("Exception in try " + ++counter);
}
2
inTry() always throws an exception. Does that make any sense? - kriegaex
It's just for example. I have real project, where it needed. In case when some external resource is unavailable need to implement 'retry' functionality. - George Zalizko

2 Answers

10
votes

You made a typical Spring AOP beginners' mistake: You forgot that proxy-based AOP only works if proxy methods are called from outside, not via this (avoiding the proxy). But the internal call inTry() is the same as this.inTry(). Thus, the aspect never triggers for inTry and you have to rearrange your code like this:

package spring.aop;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController("/")
public class HomeController {

    static int counter = 0;

    @RequestMapping
    @RetryOnFailure(attempts = 3, delay = 2, unit = TimeUnit.SECONDS)
    public String index() {
        throw new RuntimeException("Exception in try " + ++counter);
    }
}

I also changed the aspect a little bit so as to

  • avoid reflection and bind the annotation to an advice parameter directly via @annotation(),
  • log the joinpoint when the advice is triggered and
  • return "OK" on try #3 (just for fun, not necessary).
package spring.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public final class MethodRepeater {

    @Around("execution(* spring.aop..*(..)) && @annotation(retryOnFailure)")
    public Object wrap(final ProceedingJoinPoint joinPoint, RetryOnFailure retryOnFailure) throws Throwable {
        System.out.println(joinPoint);
        return proceed(joinPoint, retryOnFailure);
    }

    private Object proceed(ProceedingJoinPoint joinPoint, RetryOnFailure retryOnFailure) throws Throwable {
        int attempt = 1;
        while (true) {
            try {
                return joinPoint.proceed();
            } catch (final Throwable ex) {
                System.out.println("Try #" + attempt + " failed: " + ex);
                if (++attempt >= retryOnFailure.attempts())
                    return "OK";
                if (retryOnFailure.delay() > 0L)
                    retryOnFailure.unit().sleep(retryOnFailure.delay());
            }
        }
    }
}

Now it works and the console log says:

execution(String spring.aop.HomeController.index())
Try #1 failed: java.lang.RuntimeException: Exception in try 1
Try #2 failed: java.lang.RuntimeException: Exception in try 2
0
votes

I've had a similar problem and I managed to solve it using AspectJ:

https://github.com/mdanetzky/tour-of-heroes-java

Also - it took me some time to find out, that my IDEA didn't rebuild aspects properly, so it might be worth trying to clean/rebuild the project before you try some more drastic measures.