尝试通过反射访问时参数注释为 null 'getAnnotatedParameterTypes()'

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

我正在使用 AspectJ 编织一个方法并应用 Around 建议。然后在建议逻辑中,我想访问该方法的所有带注释的参数。我这样做是为了过滤我正在寻找的特定注释。

问题是,在我调用 java.lang.reflectgetAnnotatedParameterTypes() 之后,我收到一个 AnnotatedType 的数组。我可以在那里找到我正在寻找的预期参数。但是,当我想访问该参数的注释类型时 - 因为我想按其类型进行过滤 - 不存在注释。

我预计它会出现 - 既然它说它是一个 AnnotatedType - 那么注释在哪里 :D

下面是要查看的代码

    @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()));
        }
    }

日志输出

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

我的批注

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

}


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

}

测试整个魔法的class

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

您的方面代码有几个问题:

  • 你的目标方法 return 是某种东西,但建议方法的 return 类型是 void,即它隐式地永远不会匹配 [=17= 以外的任何东西] 方法。不过,它肯定不符合您的示例 useData(..) 方法。所以如果你想限制return类型,你需要让return类型ObjectTestResponseModel

  • @Around 建议永远不会调用 joinPoint.proceed(),即目标方法不会被执行,而是被跳过。

  • 如果您只想记录 @Response 参数而不修改任何参数或结果 before/after 继续,实际上一个简单的 @Before 建议就足够了。不过,我会在我的示例代码中保留你的建议,以防万一你想用这些参数做一些特别的事情。

  • 建议方法的前两行执行以下操作:

    1. 获取目标中所有方法的数组class。
    2. 找到第一个方法。

    这没有多大意义。为什么你总是用第一种方法做某事而不考虑它是什么方法?你想识别被建议拦截的目标方法的参数注释,不是吗?可能第一个方法的第一个参数没有任何注释,这就是 none 被记录的原因。您实际上很幸运,第一个方法根本没有参数,否则 annotatedParameterTypes[0] 会产生“数组索引越界”异常。

这是您想改为执行的操作。顺便说一句,我在这里展示了一个完整的 MCVE,正如你一开始就应该做的那样。我使用的是普通的 AspectJ,而不是 Spring AOP,所以我不使用任何 @Component 注释。但是,如果您是 Spring 用户,则只需将方面和目标都设置为 class Spring components/beans 即可使其正常工作。

注释 + 虚拟助手 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 {}

目标 class 与 positive/negative 测试用例 + 驱动程序应用程序

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);
  }
}

预期会触发方法 useDatamultipleResponses 的建议,并且后一种方法中多个 @Response 参数的特殊情况也由方面

@Around 方面变体:

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();
  }
}

请注意 execution() 切入点如何限制带有 @Response 注释的参数的方法,无论它们在参数列表中可能出现的任何位置。

@Before 方面变体:

如果您只想记录带注释的参数,一个更简单的变体将是这方面的 @Before 建议和更少的样板文件:

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;
        }
      }
    } 
  }
}

看到了吗?现在你真的可以使用 void return 类型,不需要调用 proceed() 因此也不会抛出 Throwable.

控制台日志:

对于两个方面的变体,控制台日志是相同的。

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