Search code examples
springspring-bootaopspring-aop

AOP: Supporting @annotation and @within in one pointcut


I have an annotation which can be placed on a class or a method:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface TestAspectAnnotation {
    String[] tags() default {};
}

I want to have a single advising method to handle both class-level and method-level usages:

@Around(value = "@annotation(annotation) || @within(annotation)", argNames = "pjp,annotation")
public Object testAdvice(ProceedingJoinPoint pjp,
                         TestAspectAnnotation annotation) throws Throwable {

        String[] tags = annotation.tags();

        Stopwatch stopwatch = Stopwatch.createStarted();
        Object proceed = pjp.proceed();
        stopwatch.stop();
        long executionTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);

        sendMetrics(tags, executionTime);

        return proceed;

    }

This works fine when I annotate a class with TestAspectAnnotation(tags="foo").

However, if I annotate a method, annotation argument will be null.

Interestingly, if I reverse the order of the pointcut designators ("@within(annotation) || @annotation(annotation)"), then I will have the reverse problem: method-level annotations will work fine, but class-level annotations will result in null for for the annotation argument.

Is there a way have a single pointcut and advice to support the annotation at both a class-level and method-level?


Solution

  • Is there a way have a single pointcut and advice

    I had a similar problem recently and have tried various options, but to no avail. I ended up splitting the "or" pointcut into two separate pointcuts and calling the same method from both pieces of advice.

    I have set up a small demo project to illustrate the working solution that I had set up. I hope this will help you:

    @Component
    @Aspect
    public class SomeAspect {
        @Around(value = "@within(annotation)", argNames = "pjp,annotation")
        public Object methodAdvice(ProceedingJoinPoint pjp, SomeAnnotation annotation) throws Throwable {
            return logTags(pjp, annotation);
        }
    
        @Around(value = "@annotation(annotation)", argNames = "pjp,annotation")
        public Object classAdvice(ProceedingJoinPoint pjp, SomeAnnotation annotation) throws Throwable {
            return logTags(pjp, annotation);
        }
    
        private Object logTags(ProceedingJoinPoint pjp, SomeAnnotation annotation) throws Throwable {
            Stream.of(annotation.tags()).forEach(System.out::println);
            return pjp.proceed();
        }
    }
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD, ElementType.TYPE})
    public @interface SomeAnnotation {
        String[] tags() default {};
    }
    
    @Service
    @SomeAnnotation(tags = {"c", "d"})
    public class SomeService {
        @SomeAnnotation(tags = {"a", "b"})
        public void test() {
        }
    }
    
    @SpringBootApplication
    public class Application implements ApplicationRunner {
        @Autowired private SomeService someService;
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            someService.test();
        }
    }