Mr.J4mes
Mr.J4mes

Reputation: 9266

Spring AOP pointcut for custom annotation not working inner static class

At the moment, I have the following Pointcut.

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    @Aspect
    @Component
    public static class MyAnnotationAspect {
        @Pointcut("execution(* (@com.test.MyAnnotation *).*(..))")
        public void methodInMyAnnotationType() {}

        @Around("methodInMyAnnotationType()")
        public Object annotate(ProceedingJoinPoint pjp) throws Throwable {
            System.out.println("AOP WORKING");
            return pjp.proceed();
        }
    }
}

It's working fine when I add @MyAnnotation on root level classes as following.

@MyAnnotation
@Service
public class ShiftModule {
    @Resource
    private ShiftModule self;

    /* Executing anything using self.method() triggers the Aspect
     * for @MyAnnotation perfectly
     */
}

It's also not working if I add the annotation on an inner static class.

@Service
public class ShiftModule {
    @Service
    @MyAnnotation
    public class AnnotatedShiftModule extends ShiftModule {}

    @Resource
    private AnnotatedShiftModule self;

    /* Executing anything using self.method() does NOT trigger the 
     * Aspect for @MyAnnotation or even framework's annotations
     * like @Async
     */
}

If I use this technique on an interface, it works.

@Repository
public interface OrderRepo extends JpaRepository<Order,Long> {
    @Repository("annotatedOrderRepo")
    @MyAnnotation
    public interface AnnotatedOrderRepo extends OrderRepo {}
}

I'd be very grateful if you could show me how to make it work with classes and Spring beans.

Upvotes: 0

Views: 3349

Answers (2)

Mr.J4mes
Mr.J4mes

Reputation: 9266

After digging deeper into the topic of AOP, I finally found a working solution.

Originally, I'm using the following pointcuts.

@Aspect
@Component
public static class MyAnnotationAspect {
    /**
     * Matches the execution of any methods in a type annotated with @MyAnnotation.
     */
    @Pointcut("execution(* (@com.test.MyAnnotation *).*(..))")
    public void methodInMyAnnotationType() {}

    /**
     * Matches the execution of any methods annotated with @MyAnnotation.
     */
    @Pointcut("execution(@com.test.MyAnnotation * *.*(..))")
    public void methodAnnotatedWithMyAnnotation() {}

    @Around("methodInMyAnnotationType() || methodAnnotatedWithMyAnnotation()")
    public Object aop(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("AOP IS WORKING");
        return pjp.proceed;
    }
}

What I learned is that the methodInMyAnnotationType pointcut will only work if I put @MyAnnotation on the class that actually owns the method. However, if I put the annotation on class B that extends class A, the AOP cannot intercept methods from class A.

One potential solution I found is as following.

@Pointcut("execution(* *(..)) && @this(com.test.MyAnnotation)")

It means the pointcut is for ALL methods from current class AND parent class and the current class must be annotated with @MyAnnotation. It looks promising. Unfortunately, Spring AOP doesn't support @this pointcut primitive which produces UnsupportedPointcutPrimitiveException.

After a bit more digging into the topic of this, I found the existence of target primitive and came up with the following solution.

@Pointcut("execution(@com.test.MyAnnotation * *.*(..))")
public void annotatedMethod() {}

@Pointcut("execution(* (@com.test.MyAnnotation *).*(..))")
public void annotatedClass() {}

@Pointcut("execution(* *(..)) && target(com.test.MyAnnotable)")
public void implementedInterface() {}

@Around("annotatedMethod() || annotatedClass() || implementedInterface()")
public Object aop(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("AOP IS WORKING");
    return pjp.proceed;
}

It means the pointcut is for ALL methods from current class AND parent class. In addition, the method must be annotated with @MyAnnotation or the class containing the method is annotated with @MyAnnotation or the object that has this method must be an instance of the marker interface MyAnnotable. It looks nice and it works.

My final class implementation looks like this.

@Service
public class ShiftModule {
    @Service
    public class Annotated extends ShiftModule implements MyAnnotable {}

    @Resource
    private ShiftModule.Annotated self;
}

Add-on information:

I did give the following pointcut a try during my experimentation.

@Pointcut("@annotation(com.test.MyAnnotation)")
public void annotatedMethod() {}

@Pointcut("@within(com.test.MyAnnotation)")
public void annotatedClass() {}

@Pointcut("target(com.test.MyAnnotable)")
public void implementedInterface() {}

@Around("execution(* *(..)) && (annotatedMethod() || annotatedClass() || implementedInterface()")
public Object aop(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("AOP IS WORKING");
    return pjp.proceed;
}

What I found is it does NOT work with annotated inner interface, meaning the code below will stop working. The AOP aspect doesn't have any effects at all.

@Repository
public interface OrderRepo extends JpaRepository<Order,Long> {
    @Repository("annotatedOrderRepo")
    @MyAnnotation
    public interface Annotated extends OrderRepo {}
}

Upvotes: 2

kriegaex
kriegaex

Reputation: 67317

This is not an answer, but comments are too limited to say what I want to say. This is actually feedback to the OP's own answer:

  • execution(* (@com.test.MyAnnotation *).*(..)) can also be written more readably as @within(com.test.MyAnnotation) in Spring AOP because Spring AOP only knows execution joinpoints anyway. In AspectJ you would add && execution(* *(..)) to the pointcut.

  • execution(@com.test.MyAnnotation * *.*(..)) can also be written more readably as @annotation(com.test.MyAnnotation) in Spring AOP because Spring AOP only knows execution joinpoints anyway. In AspectJ you would add && execution(* *(..)) to the pointcut.

  • What I learned is that the methodInMyAnnotationType pointcut will only work if I put @MyAnnotation on the class that actually owns the method.

    Of course, because this is a general limitation of Java annotations. They are never inherited to subclasses, from interfaces to classes or methods or from parent class methods to overwritten subclass methods. The only exception is if you use @Inherited as a meta annotation for annotation type itself, then it gets inherited by subclasses (but again not from interface to implementing class). This is documented here.

  • As for this() vs target() and @this() vs @target, as you said the "this" versions are only supported by AspectJ (which you can optionally also use from within a Spring application). The reason is that "this" only makes a difference from "target" in a call() pointcut where "this" is the calling method and "target" is the called method. Because call() is also unavailable in Spring AOP, it would not make sense to support the corresponding "this" type pointcuts.

  • If you are willing to switch to AspectJ, I have a workaround for making implementing classes "inherit" annotations from interfaces and for making specific methods "inherit" annotations too, see this answer.

I am just mentioning all this for educational purposes, not in order to replace your own solution as you seem to be happy with the mix of marker annotations and marker interfaces.

Upvotes: 2

Related Questions