2

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?

1 Answer 1

3

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(); } } 
Sign up to request clarification or add additional context in comments.

7 Comments

Thanks @Michiel - this is the approach we are considering, but was holding out hope that there might be a cleaner solution. We also tried using 2 different names for the arg (@annotation(methodAnnotation) || @within(classAnnotation)), hoping that either one or the other will be null, but this doesn't compile.
Sorry, correction- it fails to weave at runtime - java.lang.IllegalArgumentException: error at ::0 inconsistent binding
The error message explains what is going wrong: the binding is inconsistent as in ambiguous. What if both conditions of a logical OR condition are true? Which annotation instance should be bound? Imagine you use the annotation on class + method level in the same class. The result is ambiguous, thus binding two variables from different || branches to the same method parameter is doomed to fail. Not AspectJ is the problem, your expectation for this to work is kind of illogical.
If you have two different parameters, the one from the non-matching OR branch would have an undefined value (not the same as null or 0). The reason it compiles at all is that you compile with javac which only sees annotations with parameters. Only during load-time there can be a pointcut check by the AspectJ weaver. If you would use compile-time weaving via AspectJ compiler, you would get compile errors in both cases: ambiguous binding of parameter(s) annotation across '||' in pointcut for shared name and inconsistent binding for two different parameter names.
I do. It also makes the pointcuts more readable and, if necessary, re-usable. If you worry about duplicated code in your advice methods, you can always factor it out into a helper method taking parameters for the joinpoint and possibly also for the bound annotation.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.