Spring AOP: aspect @Around doesn't work
Asked Answered
D

3

6

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);
}
Dodger answered 11/3, 2017 at 10:1 Comment(2)
inTry() always throws an exception. Does that make any sense?Taco
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.Dodger
T
18

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
Taco answered 11/3, 2017 at 12:55 Comment(1)
Thanks so much for explanation. Yes, you're right. The question's point was 'WHY?!?', and you described it well. Cheers!Dodger
C
1

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.

Cuneate answered 11/3, 2017 at 10:35 Comment(5)
So, you thing, that problem in the missing aspectj-maven-plugin?Dodger
If I add aspectj-maven-plugin it work. Many thanks for your answer, but it's compile time weaving, I try to implement pure spring-aop - runtime weaving. docs.spring.io/spring/docs/current/spring-framework-reference/…Dodger
No, that is not the solution. AspectJ Maven Plugin is for full-blown AspectJ compile-time or subsequent load-time weaving. It is absolutely not necessary for proxy-based Spring AOP. If it helped in your case then just because you implicitly switched from Spring AOP to AspectJ, not solving but just avoiding your real problem.Taco
You could also try to write out the annotation's name with package in your pointcut: @Around("execution(* *(..)) && @annotation(com.my.package.RetryOnFailure)")Cuneate
@MatthiasDanetzky, thank you so much. I decided, that I'll switch to compile time weaving due to application design. Thanks!Dodger
S
0

5/2023 for AspectJ

MyApplication.java

@SpringBootApplication
public class AopjApplication {
  public static void main(String[] args) {
    ConfigurableApplicationContext cntx = SpringApplication.run(AopjApplication.class, args);
    A a=cntx.getBean(A.class);
    a.chirp();      
  }
}

adding AspectJ to build.gradle

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter'
  implementation 'org.aspectj:aspectjweaver:1.9.19'  //currently last version
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

A.java :

public class A {
  public void chirp() {
    System.out.print(11111);
  }
}

MyConfig.java

@Configuration
@EnableAspectJAutoProxy
public class MyConfig {
  @Bean
  public A a() {
    return new A();
  }
  @Bean
  public B b() {
    return new B();
  }
. . .
  @Bean
  public MyAspect myAspect() {
    return new MyAspect();
  }
}

MyAspect.java

@Aspect
public class MyAspect {
  @Before("execution(* mypackage.A.chirp())")
  public void aa() {
    System.out.print(2222);
    }   
}

I ve run that and now console shows :

2023-05-21T22:47:34.035+05:00  INFO 9332 --- [           main] mypackage.AopjApplication                    : Starting AopjApplication using Java 17.0.6 with PID 9332 (C:\Users\erics\eclipse-workspace\aopj\bin\main started by erics in C:\Users\erics\eclipse-workspace\aopj)
2023-05-21T22:47:34.045+05:00  INFO 9332 --- [           main] mypackage.AopjApplication                    : No active profile set, falling back to 1 default profile: "default"
2023-05-21T22:47:35.566+05:00  INFO 9332 --- [           main] mypackage.AopjApplication                    : Started AopjApplication in 2.33 seconds (process running for 3.303)
2222
11111
Spiers answered 21/5, 2023 at 17:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.