英文:
Parameter annotation is null when trying to access it via reflection 'getAnnotatedParameterTypes()'
问题
我正在使用AspectJ编写一个方法,并在其上应用了`Around`通知。在通知逻辑中,我希望访问所有带有注释的该方法的参数。我这样做是为了可以根据我要寻找的特定注释进行过滤。
问题是,在我调用`java.lang.reflect`的`getAnnotatedParameterTypes()`后,我收到一个`AnnotatedType`数组。我可以在其中找到我要寻找的预期参数。然而,当我想要访问该参数的注释类型 - 因为我想根据其类型进行过滤 - 没有找到注释。
我期望它应该存在 - 因为它说它是一个`AnnotatedType` - 那么注释在哪里呢 :D
以下是用于查看的代码
```java
@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 {
}
进行整个操作测试的类
@Component
class TestCandidate {
@Query
public TestResponseModel useData(@Response TestResponseModel model){
return model;
}
}
<details>
<summary>英文:</summary>
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;
}
}
</details>
# 答案1
**得分**: 1
你的方面代码存在几个问题:
* 你的目标方法返回了一些内容,但是advice方法的返回类型是`void`,也就是说它隐式地只会匹配除了`void`方法之外的任何内容。它肯定不会匹配你的示例`useData(..)`方法。所以,如果你想限制返回类型,你需要将返回类型设置为`Object`或`TestResponseModel`。
* `@Around`的advice从不调用`joinPoint.proceed()`,也就是说目标方法不会被执行,而是被跳过。
* 如果你只想记录`@Response`参数,而不修改任何参数或在继续之前/之后修改结果,实际上一个简单的`@Before` advice 就足够了。尽管如此,我将保留你的around advice在我的示例代码中,以防你想对这些参数做特殊处理。
* 你advice方法的前两行执行了以下操作:
1. 获取目标类中的所有方法的数组。
2. 找到第一个方法。
这没有多大意义。为什么你总是要对第一个方法做某些处理,而不考虑它是什么方法?你想要识别被advice拦截的目标方法上的参数注解,对吧?可能第一个方法的第一个参数没有任何注解,这就是为什么没有被记录任何注解。你实际上很幸运,第一个方法有一个参数,否则`annotatedParameterTypes[0]`将会导致"数组索引越界"异常。
相反,你应该这样做。顺便说一下,我在这里提供了一个完整的[MCVE](https://stackoverflow.com/help/mcve),就像你一开始应该做的那样。我使用的是纯粹的AspectJ,而不是Spring AOP,所以我没有使用任何`@Component`注解。但是如果你是Spring用户,你只需要将方面和目标类都变成Spring组件/beans,就可以让它工作。
**注解 + 虚拟帮助类:**
```java
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 {}
目标类带有正/负测试案例 + 驱动程序应用程序
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);
}
}
预期是advice将会对useData
和multipleResponses
方法触发,并且后者方法中多个@Response
参数的特殊情况也会被aspect正确处理。
@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
advice就足够了,并且代码量更少:
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.annotation
<details>
<summary>英文:</summary>
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](https://stackoverflow.com/help/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:**
```java
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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论