Search code examples
javaspringaopspring-aop

Spring AOP: aspect @Around doesn't work


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

Solution

  • 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