xetra11
xetra11

Reputation: 8837

Parameter annotation is null when trying to access it via reflection 'getAnnotatedParameterTypes()'

I am weaving a method with AspectJ and having a Around advice applied to it. In the advice logic I then want to access all parameters of that method that are annotated. I do this so I can filter on the specific annotation I am looking for.

The problem is that after I call getAnnotatedParameterTypes() of java.lang.reflect I receive an array of AnnotatedType. I can find that expected parameter I was looking for in there. However when I want to access the annotation type of that parameter - because I want to filter by its type - there is no annotation present.

I expected it to be present - well since it says it's an AnnotatedType - so where is the annotation :D

Here is the code to look through

    @Around("@annotation(com.mystuff.client.annotation.Query)")
    public void doStuff(ProceedingJoinPoint joinPoint) {
        Method[] methods = joinPoint.getSignature().getDeclaringType().getMethods();
        Optional<Method> first = Arrays.stream(methods).findFirst();
        if (first.isPresent()) {
            Method method = first.get();
            AnnotatedType[] annotatedParameterTypes = method.getAnnotatedParameterTypes();
            AnnotatedType annotatedParameterType = annotatedParameterTypes[0];
            LOG.info(Arrays.toString(annotatedParameterType.getAnnotations()));
        }
    }

Log Output

2020-10-10 22:17:11.821 INFO 215068 --- [ Test worker] com.mystuff.Aspect : []

My Annotations

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Query{

}


@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Response {

}

The class where the whole magic is tested on

@Component
class TestCandidate {
    @Query
    public TestResponseModel useData(@Response TestResponseModel model){
        return model;
    }
}

Upvotes: 1

Views: 901

Answers (1)

kriegaex
kriegaex

Reputation: 67297

Your aspect code has several problems:

  • Your target method returns something but the advice method's return type is void, i.e. it implicitly will never match anything other than void methods. It will definitely not match your sample useData(..) method, though. So you need to make the return type Object or TestResponseModel if you want to limit the return type.

  • The @Around advice never calls joinPoint.proceed(), i.e. the target method will not be executed but skipped.

  • If you just want to log @Response parameters and not modify any parameters or the result before/after proceeding, actually a simple @Before advice would suffice. I am going to keep your around advice in my sample code, though, just in case you want to do something special with those parameters.

  • The first two lines in your advice method do the following:

    1. Get an array of all methods in the target class.
    2. Find the first method.

    This does not make much sense. Why would you always do something with the first method without regard to what method it is? You want to identify the parameter annotations on the target method being intercepted by the advice, don't you? Probably the first method's first parameter does not have any annotations, which is why none are being logged. You are actually lucky that the first method has a parameter at all, otherwise annotatedParameterTypes[0] would yield an "array index out of bounds" exception.

Here is what you want to do instead. BTW, I am presenting a full MCVE here, as you should have done in the first place. I am using plain AspectJ, not Spring AOP, so I do not use any @Component annotations. But if you are a Spring user, you can just make both the aspect and the target class Spring components/beans in order to make it work.

Annotations + dummy helper class:

package de.scrum_master.app;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(METHOD)
public @interface Query {}
package de.scrum_master.app;

import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(PARAMETER)
public @interface Response {}
package de.scrum_master.app;

public class TestResponseModel {}

Target class with positive/negative test cases + driver application

package de.scrum_master.app;

class TestCandidate {
  @Query
  public TestResponseModel useData(@Response TestResponseModel model) {
    return model;
  }

  @Query
  public TestResponseModel dummyOne(TestResponseModel model) {
    return model;
  }

  public TestResponseModel dummyTwo(@Response TestResponseModel model) {
    return model;
  }

  @Query
  public TestResponseModel multipleResponses(@Response TestResponseModel model, @Response String anotherResponse, int i) {
    return model;
  }
  public static void main(String[] args) {
    TestCandidate candidate = new TestCandidate();
    TestResponseModel model = new TestResponseModel();
    candidate.dummyOne(model);
    candidate.dummyTwo(model);
    candidate.useData(model);
    candidate.multipleResponses(model, "foo", 11);
  }
}

The expectation would be that the advice gets triggered for methods useData and multipleResponses and that the special case of multiple @Response parameters in the latter method is also handled correctly by the aspect.

@Around aspect variant:

package de.scrum_master.aspect;

import java.lang.annotation.Annotation;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;

import de.scrum_master.app.Response;

@Aspect
public class QueryResponseInterceptor {
  @Around(
    "@annotation(de.scrum_master.app.Query) && " +
    "execution(* *(.., @de.scrum_master.app.Response (*), ..))"
  ) 
  public Object doStuff(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println(joinPoint);
    Object[] args = joinPoint.getArgs();
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Annotation[][] annotationMatrix = methodSignature.getMethod().getParameterAnnotations();
    for (int i = 0; i < args.length; i++) {
      for (Annotation annotation : annotationMatrix[i]) {
        if (annotation.annotationType().equals(Response.class)) {
          System.out.println("  " + args[i]);
          break;
        }
      }
    } 
    return joinPoint.proceed();
  }
}

Please note how the execution() pointcut limits to methods with parameters carrying @Response annotations, wherever in the parameter list they might occur.

@Before aspect variant:

A simpler variant if you just want to log the annotated parameters would be this aspect with a @Before advice and less boilerplate:

package de.scrum_master.aspect;

import java.lang.annotation.Annotation;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;

import de.scrum_master.app.Response;

@Aspect
public class QueryResponseInterceptor {
  @Before(
    "@annotation(de.scrum_master.app.Query) && " +
    "execution(* *(.., @de.scrum_master.app.Response (*), ..))"
  ) 
  public void doStuff(JoinPoint joinPoint) {
    System.out.println(joinPoint);
    Object[] args = joinPoint.getArgs();
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Annotation[][] annotationMatrix = methodSignature.getMethod().getParameterAnnotations();
    for (int i = 0; i < args.length; i++) {
      for (Annotation annotation : annotationMatrix[i]) {
        if (annotation.annotationType().equals(Response.class)) {
          System.out.println("  " + args[i]);
          break;
        }
      }
    } 
  }
}

See? Now you really can use a void return type, do not need to call proceed() and hence also not throw Throwable.

Console log:

For both aspect variants the console log is the same.

execution(TestResponseModel de.scrum_master.app.TestCandidate.useData(TestResponseModel))
  de.scrum_master.app.TestResponseModel@71318ec4
execution(TestResponseModel de.scrum_master.app.TestCandidate.multipleResponses(TestResponseModel, String, int))
  de.scrum_master.app.TestResponseModel@71318ec4
  foo

Upvotes: 1

Related Questions