Skip to content

Commit a116505

Browse files
committed
Support method parameter annotations on interface
Closes gh-480
1 parent a7a78d9 commit a116505

File tree

2 files changed

+119
-5
lines changed

2 files changed

+119
-5
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/method/HandlerMethod.java

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
import java.lang.annotation.Annotation;
1919
import java.lang.reflect.Method;
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
22+
import java.util.List;
2023
import java.util.stream.Collectors;
2124
import java.util.stream.IntStream;
2225

@@ -26,6 +29,7 @@
2629
import org.springframework.beans.factory.BeanFactory;
2730
import org.springframework.core.BridgeMethodResolver;
2831
import org.springframework.core.MethodParameter;
32+
import org.springframework.core.ResolvableType;
2933
import org.springframework.core.annotation.AnnotatedElementUtils;
3034
import org.springframework.core.annotation.SynthesizingMethodParameter;
3135
import org.springframework.lang.Nullable;
@@ -67,6 +71,9 @@ public class HandlerMethod {
6771

6872
private final MethodParameter[] parameters;
6973

74+
@Nullable
75+
private volatile List<Annotation[][]> interfaceParameterAnnotations;
76+
7077

7178
/**
7279
* Constructor with a handler instance and a method.
@@ -241,6 +248,40 @@ public String getShortLogMessage() {
241248
return getBeanType().getSimpleName() + "#" + this.method.getName() + "[" + args + " args]";
242249
}
243250

251+
private List<Annotation[][]> getInterfaceParameterAnnotations() {
252+
List<Annotation[][]> parameterAnnotations = this.interfaceParameterAnnotations;
253+
if (parameterAnnotations == null) {
254+
parameterAnnotations = new ArrayList<>();
255+
for (Class<?> ifc : ClassUtils.getAllInterfacesForClassAsSet(this.method.getDeclaringClass())) {
256+
for (Method candidate : ifc.getMethods()) {
257+
if (isOverrideFor(candidate)) {
258+
parameterAnnotations.add(candidate.getParameterAnnotations());
259+
}
260+
}
261+
}
262+
this.interfaceParameterAnnotations = parameterAnnotations;
263+
}
264+
return parameterAnnotations;
265+
}
266+
267+
private boolean isOverrideFor(Method candidate) {
268+
if (!candidate.getName().equals(this.method.getName()) ||
269+
candidate.getParameterCount() != this.method.getParameterCount()) {
270+
return false;
271+
}
272+
Class<?>[] paramTypes = this.method.getParameterTypes();
273+
if (Arrays.equals(candidate.getParameterTypes(), paramTypes)) {
274+
return true;
275+
}
276+
for (int i = 0; i < paramTypes.length; i++) {
277+
if (paramTypes[i] !=
278+
ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).resolve()) {
279+
return false;
280+
}
281+
}
282+
return true;
283+
}
284+
244285

245286
@Override
246287
public boolean equals(@Nullable Object other) {
@@ -323,6 +364,9 @@ protected String formatInvokeError(String text, Object[] args) {
323364
*/
324365
protected class HandlerMethodParameter extends SynthesizingMethodParameter {
325366

367+
@Nullable
368+
private volatile Annotation[] combinedAnnotations;
369+
326370
public HandlerMethodParameter(int index) {
327371
super(HandlerMethod.this.bridgedMethod, index);
328372
}
@@ -347,8 +391,38 @@ public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationTyp
347391
}
348392

349393
@Override
350-
public HandlerMethodParameter clone() {
351-
return new HandlerMethodParameter(this);
394+
public Annotation[] getParameterAnnotations() {
395+
Annotation[] anns = this.combinedAnnotations;
396+
if (anns == null) {
397+
anns = super.getParameterAnnotations();
398+
int index = getParameterIndex();
399+
if (index >= 0) {
400+
for (Annotation[][] ifcAnns : getInterfaceParameterAnnotations()) {
401+
if (index < ifcAnns.length) {
402+
Annotation[] paramAnns = ifcAnns[index];
403+
if (paramAnns.length > 0) {
404+
List<Annotation> merged = new ArrayList<>(anns.length + paramAnns.length);
405+
merged.addAll(Arrays.asList(anns));
406+
for (Annotation paramAnn : paramAnns) {
407+
boolean existingType = false;
408+
for (Annotation ann : anns) {
409+
if (ann.annotationType() == paramAnn.annotationType()) {
410+
existingType = true;
411+
break;
412+
}
413+
}
414+
if (!existingType) {
415+
merged.add(adaptAnnotation(paramAnn));
416+
}
417+
}
418+
anns = merged.toArray(new Annotation[0]);
419+
}
420+
}
421+
}
422+
}
423+
this.combinedAnnotations = anns;
424+
}
425+
return anns;
352426
}
353427
}
354428

spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethodTests.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package org.springframework.graphql.data.method.annotation.support;
1717

1818

19+
import java.lang.reflect.Method;
20+
import java.util.Collections;
1921
import java.util.concurrent.Callable;
2022
import java.util.concurrent.CompletableFuture;
2123

@@ -26,10 +28,14 @@
2628
import org.mockito.Mockito;
2729

2830
import org.springframework.core.task.SimpleAsyncTaskExecutor;
31+
import org.springframework.graphql.data.GraphQlArgumentBinder;
2932
import org.springframework.graphql.data.method.HandlerMethod;
3033
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
3134
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
35+
import org.springframework.graphql.data.method.annotation.Argument;
36+
import org.springframework.graphql.data.method.annotation.QueryMapping;
3237
import org.springframework.lang.Nullable;
38+
import org.springframework.util.ClassUtils;
3339

3440
import static org.assertj.core.api.Assertions.assertThat;
3541

@@ -40,6 +46,22 @@
4046
*/
4147
public class DataFetcherHandlerMethodTests {
4248

49+
@Test
50+
void annotatedMethodsOnInterface() {
51+
52+
HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
53+
resolvers.addResolver(new ArgumentMethodArgumentResolver(new GraphQlArgumentBinder()));
54+
55+
DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod(
56+
handlerMethodFor(new TestController(), "hello"), resolvers, null, null, false);
57+
58+
Object result = handlerMethod.invoke(
59+
DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
60+
.arguments(Collections.singletonMap("name", "Neil"))
61+
.build());
62+
63+
assertThat(result).isEqualTo("Hello, Neil");
64+
}
4365

4466
@Test
4567
void callableReturnValue() throws Exception {
@@ -48,8 +70,8 @@ void callableReturnValue() throws Exception {
4870
resolvers.addResolver(Mockito.mock(HandlerMethodArgumentResolver.class));
4971

5072
DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod(
51-
new HandlerMethod(new TestController(), TestController.class.getMethod("handleAndReturnCallable")),
52-
resolvers, null, new SimpleAsyncTaskExecutor(), false);
73+
handlerMethodFor(new TestController(), "handleAndReturnCallable"), resolvers, null,
74+
new SimpleAsyncTaskExecutor(), false);
5375

5476
GraphQLContext graphQLContext = new GraphQLContext.Builder().build();
5577

@@ -64,8 +86,26 @@ void callableReturnValue() throws Exception {
6486
assertThat(future.get()).isEqualTo("A");
6587
}
6688

89+
private static HandlerMethod handlerMethodFor(Object controller, String methodName) {
90+
Method method = ClassUtils.getMethod(controller.getClass(), methodName, (Class<?>[]) null);
91+
return new HandlerMethod(controller, method);
92+
}
93+
94+
95+
interface TestInterface {
96+
97+
@QueryMapping
98+
String hello(@Argument String name);
6799

68-
private static class TestController {
100+
}
101+
102+
@SuppressWarnings("unused")
103+
private static class TestController implements TestInterface {
104+
105+
@Override
106+
public String hello(String name) {
107+
return "Hello, " + name;
108+
}
69109

70110
@Nullable
71111
public Callable<String> handleAndReturnCallable() {

0 commit comments

Comments
 (0)