3

I'm trying to get the value of an annotation via Spring Aop AspectJ-style, where the annotation can be on the class OR the method. I tried a lot of different things, but I can only get it to work when the annotation is on the method. I'd really like to annotate ONCE on the class - but advice all the methods of the class - and access the value of the class annotation in the advice. Here's where I've ended up:

Annotation:

@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value() default "";
}

Aspect:

@Aspect
public class MyAspect {
    @Pointcut("execution(@com.myco.MyAnnotation * com.myco.somepackage..*.*(..))")
    public void atExecution() { }

    @Before("atExecution() && @annotation(myAnnotation)")
    public void myAdvice(JoinPoint joinPoint, MyAnnotation myAnnotation) {
        ...
    }
}

Any thoughts? Thanks.

Michael Andrews
  • 828
  • 8
  • 13

1 Answers1

5

Short answer

While you can formulate a pointcut that will match both directly annotated methods and methods of annotated types at the same time, you cannot make a pointcut and/or advice where you bind the value of the annotation (i.e. use the annotation value in the advice code).

@Aspect
public class MyAspect {

    @Pointcut("execution(@com.myco.MyAnnotation * com.myco.somepackage..*.*(..))")
    public void atExecutionOfAnnotatedMethod() {}

    @Pointcut("execution(* (@com.myco.MyAnnotation com.myco.somepackage..*).*(..))")
    public void atExecutionOfMethodsOfAnnotatedClass() {}

    @Before("atExecutionOfAnnotatedMethod() && @annotation(myAnnotation)")
    public void myAdviceForMethodAnnotation(JoinPoint joinPoint, MyAnnotation myAnnotation) {
        System.out.println("myAdviceForMethodAnnotation: " + myAnnotation.value());
    }

    @Before("atExecutionOfMethodsOfAnnotatedClass() && @this(myAnnotation)")
    public void myAdviceForTypeAnnotation(JoinPoint joinPoint, MyAnnotation myAnnotation) {
        System.out.println("myAdviceForTypeAnnotation: " + myAnnotation.value());
    }

    //      /* the following pointcut will result in "inconsistent binding" errors */
    //      @Pointcut("(atExecutionOfAnnotatedMethod() && @annotation(myMethodAnnotation)) || (atExecutionOfMethodsOfAnnotatedClass() && @this(myTypeAnnotation))")
    //      public void combinedPointcut(MyAnnotation myMethodAnnotation, MyAnnotation myTypeAnnotation) {}

}

Some detail

To combine the two separate pointcuts (atExecutionOfAnnotatedMethod and atExecutionOfMethodsOfAnnotatedClass) we would have to use the OR (||) construct. Since the OR construct doesn't guarantee that either of the two annotation bindings will be present at advice execution, they will both result in a compile error (inconsistent binding). You can still handle both cases in separate advices, you may also delegate the actual advice code to a common method to avoid duplication. In that case you'll need to take care of the case where both the type and the method is annotated with @MyAnnotation because that would match both pointcuts and would result in your method doubly advised by both advices, hence your common advice handling code will execute twice.

Combining the two

If you need to combine the two cases while defending against doubly advising the target code, you need to set up a precedence between the method level annotation and the class level annotation. Based on the principle of specificity, I'd suggest to go on the route where the method level annotation takes precedence over the class level one. Your aspect would look like this:

@Aspect
public class MyCombinedAspect {

    @Pointcut("execution(@com.myco.MyAnnotation * com.myco.somepackage..*.*(..))")
    public void atExecutionOfAnnotatedMethod() {}

    @Pointcut("execution(* (@com.myco.MyAnnotation com.myco.somepackage..*).*(..))")
    public void atExecutionOfMethodsOfAnnotatedClass() {}

    @Before("atExecutionOfAnnotatedMethod() && !atExecutionOfMethodsOfAnnotatedClass() && @annotation(myAnnotation)")
    public void myAdviceForMethodAnnotation(JoinPoint joinPoint, MyAnnotation myAnnotation) {
        handleBeforeExecution(joinPoint, myAnnotation);
    }

    @Before("atExecutionOfMethodsOfAnnotatedClass() && !atExecutionOfAnnotatedMethod() && @this(myAnnotation)")
    public void myAdviceForTypeAnnotation(JoinPoint joinPoint, MyAnnotation myAnnotation) {
        handleBeforeExecution(joinPoint, myAnnotation);
    }

    @Before("atExecutionOfMethodsOfAnnotatedClass() && atExecutionOfAnnotatedMethod() && @annotation(myMethodAnnotation)")
    public void myAdviceForDoublyAnnotated(JoinPoint joinPoint, MyAnnotation myMethodAnnotation) {
        handleBeforeExecution(joinPoint, myMethodAnnotation);
    }

    protected void handleBeforeExecution(JoinPoint joinPoint, MyAnnotation myAnnotation) {
        System.out.println(myAnnotation.value());
    }
Nándor Előd Fekete
  • 6,988
  • 1
  • 22
  • 47
  • Thank you for the response. Very detailed. So to clarify. I COULD do this as you indicated with two different advices, but would need to take care not to annotate both the class and the method. (which would be hard to enforce). Thanks again! – Michael Andrews Jan 07 '16 at 17:26
  • No. Let me clarify. It's **enough** to annotate either the class or the method. But we can't enforce that the developer will not annotate both the class and the method. In which case we want the method annotation to take effect. So, we have 3 advices to handle 3 cases, in that order: 1-when only the method is annotated, 2-when only the class is annotated, **3-when both the method and class is annotated, in which case the value of the method annotation is passed** to the delegate method. All three advices delegate to the same method to do the work, to adhere to the DRY principle. – Nándor Előd Fekete Jan 07 '16 at 18:39
  • The second aspect in my answer works in the way I described it in my previous comment. Hope this clears up eventual confusion. – Nándor Előd Fekete Jan 07 '16 at 18:40
  • Got it. Thanks again for all the detail. Really helps. – Michael Andrews Jan 27 '16 at 17:24