0

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.

Mr.J4mes
  • 9,168
  • 9
  • 48
  • 90
  • Spring AOP is based on pure Java. The vanilla Java Proxy pattern is limited to the interfaces. You should enable Spring to use some AOP libraries, like AspectJ. – sigur Apr 14 '20 at 12:51
  • @sigur: I'm aware of Spring AOP limitation. I updated my post with the technique I'm using to allow a bean to self-trigger through Proxy. I'm just confused why it doesn't work with inner static class. I'm not sure if there's something wrong with my pointcut or it's actually a limitation with Spring. – Mr.J4mes Apr 14 '20 at 12:56
  • @kriegaex: in my actual application, I wrote an annotation called `@Memoize` which can be annotated on a class or a method. When an annotated method or any methods of an annotated class get executed, I memoize the result into a Map. Subsequent invocation of the same method with the same arguments will not run the method body again. Instead, the result from Map is returned. I'm creating an inner interface extending an outer interface without overriding anything so that I have access to 2 interface: 1 memoized and 1 non-memoized. – Mr.J4mes Apr 14 '20 at 15:20
  • @kriegaex: That gives me the option to inject the right interface based on my actual need instead of putting `@Memoize` directly on a method and then I have to cache the result even if I don't need to. – Mr.J4mes Apr 14 '20 at 15:21
  • @kriegaex: This technique saved me a ton of query time when I use that annotation on Spring `@Repository` interface. I'm writing some common classes for my team at the moment and I think this'd be useful. However, when I ran some test, I found it doesn't work with inner static class at all. I hope my explanation is good enough to get a tip from you ;). Thanks in advance for your time! – Mr.J4mes Apr 14 '20 at 15:29
  • Your inner interface solution does not convince me or maybe I do not understand it. Why can the other annotation not be separate? IMO a sub-class or sub-interface should never be contained inside its parent because it is not a logical sub-unit but an extension. This is a design topic and unrelated to your AOP problems. I find it super ugly, e.g. you would import `a.b.c.Memoize`, but `a.b.c.Memoize.NoMemoize` - aargh! – kriegaex Apr 15 '20 at 06:23
  • @kriegaex: IMHO, we only need a separate file for an interface/subclass if we're actually extending the parent to add new functionalities or override existing ones by writing code. In this scenario, the child is like a Decorator, modifying the behavior of an existing parent using annotation without overriding methods or adding new code. Developers will only import `a.b.c.BaseClass` and then they can refer to the Decorated version using `a.b.c.BaseClass.Memoized`. – Mr.J4mes Apr 15 '20 at 08:17
  • @kriegaex: you're actually right that I can put the child in a separate file though :). This is my solution to keep the project structure tidy instead of having a ton of files for child classes/interfaces doing nothing special beside having an extra annotation on top of it. – Mr.J4mes Apr 15 '20 at 08:32

2 Answers2

2

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 {}
}
Mr.J4mes
  • 9,168
  • 9
  • 48
  • 90
2

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.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Regarding the last point, what I need is not to "inherit" annotation from from interface or parent class. What I need is for AOP aspect to trigger on ALL methods of a class X that is annotated with my custom annotation. Those methods may come directly from class X body or some of them may come from a parent class Y. It should work even if parent class Y is not annotated at all :). – Mr.J4mes Apr 15 '20 at 08:37
  • Yes, of course, as I said, for interfaces it does not work because annotations are not being inherited (i.e. transferred to the implementing class) unless you use my workaround from the other answer I linked to. Your marker interface is also a workaround. In the original answer you did not say that it was an option. You would not need it if you used my workaround. But as I said, your solution works if you are in control of your target classes and can easily make them implement a marker interface. – kriegaex Apr 15 '20 at 08:43
  • Side note: When I use AOP, usually I try to avoid working with marker annotations or interfaces altogether because I want to make the application as AOP-agnostic as possible and identify the target joinpoints via other characteristics if possible. Of course it is not always possible, but more often than you might think. – kriegaex Apr 15 '20 at 08:45
  • I get it now :D. Your comments just completed the full picture for me. I finally understand what you means by "annotation not being inherited from interface". Thanks a lot for the pointers :). I will remember everything you told me to apply in future problems. – Mr.J4mes Apr 15 '20 at 09:45