diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/HandlerMethodArgumentResolverComposite.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/HandlerMethodArgumentResolverComposite.java index b6b7c7f5c..faa01c215 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/HandlerMethodArgumentResolverComposite.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/HandlerMethodArgumentResolverComposite.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ * Previously resolved method parameters are cached for faster lookups. * * @author Rossen Stoyanchev + * @author Genkui Du * @since 1.0.0 */ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { @@ -49,6 +50,18 @@ public void addResolver(HandlerMethodArgumentResolver resolver) { this.argumentResolvers.add(resolver); } + /** + * Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. + */ + public HandlerMethodArgumentResolverComposite addResolvers( + @Nullable List resolvers) { + + if (resolvers != null) { + this.argumentResolvers.addAll(resolvers); + } + return this; + } + /** * Return a read-only list with the contained resolvers, or an empty list. */ diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java index b0fa2de8b..ea72949e0 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java @@ -18,11 +18,13 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -74,6 +76,7 @@ * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Genkui Du * @since 1.0.0 */ public class AnnotatedControllerConfigurer @@ -108,6 +111,9 @@ public class AnnotatedControllerConfigurer @Nullable private ApplicationContext applicationContext; + @Nullable + private List customArgumentResolvers; + @Nullable private HandlerMethodArgumentResolverComposite argumentResolvers; @@ -143,6 +149,22 @@ public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } + /** + * Provide resolvers for custom argument types. Custom resolvers are ordered + * after built-in ones. + */ + public void setCustomArgumentResolvers(@Nullable List argumentResolvers) { + this.customArgumentResolvers = argumentResolvers; + } + + /** + * Return the custom argument resolvers, or {@code null}. + */ + @Nullable + public List getCustomArgumentResolvers() { + return this.customArgumentResolvers; + } + protected final ApplicationContext obtainApplicationContext() { Assert.state(this.applicationContext != null, "No ApplicationContext"); return this.applicationContext; @@ -151,37 +173,58 @@ protected final ApplicationContext obtainApplicationContext() { @Override public void afterPropertiesSet() { - this.argumentResolvers = new HandlerMethodArgumentResolverComposite(); + List resolvers = getDefaultArgumentResolvers(); + this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); + + if (beanValidationPresent) { + this.validator = HandlerMethodInputValidatorFactory.create(obtainApplicationContext()); + } + + if (beanValidationPresent) { + this.validator = HandlerMethodInputValidatorFactory.create(obtainApplicationContext()); + } + } + + /** + * Return the list of argument resolvers to use including built-in resolvers + * and custom resolvers provided via {@link #setCustomArgumentResolvers}. + */ + private List getDefaultArgumentResolvers() { + List resolvers = new ArrayList<>(); + // Annotation based if (springDataPresent) { // Must be ahead of ArgumentMethodArgumentResolver - this.argumentResolvers.addResolver(new ProjectedPayloadMethodArgumentResolver()); + resolvers.add(new ProjectedPayloadMethodArgumentResolver()); } - this.argumentResolvers.addResolver(new ArgumentMapMethodArgumentResolver()); + resolvers.add(new ArgumentMapMethodArgumentResolver()); GraphQlArgumentInitializer initializer = new GraphQlArgumentInitializer(this.conversionService); - this.argumentResolvers.addResolver(new ArgumentMethodArgumentResolver(initializer)); - this.argumentResolvers.addResolver(new ArgumentsMethodArgumentResolver(initializer)); - this.argumentResolvers.addResolver(new ContextValueMethodArgumentResolver()); + resolvers.add(new ArgumentMethodArgumentResolver(initializer)); + resolvers.add(new ArgumentsMethodArgumentResolver(initializer)); + resolvers.add(new ContextValueMethodArgumentResolver()); // Type based - this.argumentResolvers.addResolver(new DataFetchingEnvironmentMethodArgumentResolver()); - this.argumentResolvers.addResolver(new DataLoaderMethodArgumentResolver()); + resolvers.add(new DataFetchingEnvironmentMethodArgumentResolver()); + resolvers.add(new DataLoaderMethodArgumentResolver()); if (springSecurityPresent) { - this.argumentResolvers.addResolver(new PrincipalMethodArgumentResolver()); + resolvers.add(new PrincipalMethodArgumentResolver()); BeanResolver beanResolver = new BeanFactoryResolver(obtainApplicationContext()); - this.argumentResolvers.addResolver(new AuthenticationPrincipalArgumentResolver(beanResolver)); + resolvers.add(new AuthenticationPrincipalArgumentResolver(beanResolver)); } if (KotlinDetector.isKotlinPresent()) { - this.argumentResolvers.addResolver(new ContinuationHandlerMethodArgumentResolver()); + resolvers.add(new ContinuationHandlerMethodArgumentResolver()); } - // This works as a fallback, after other resolvers - this.argumentResolvers.addResolver(new SourceMethodArgumentResolver()); + // This works as a fallback, after other resolvers, but before custom resolvers + resolvers.add(new SourceMethodArgumentResolver()); - if (beanValidationPresent) { - this.validator = HandlerMethodInputValidatorFactory.create(obtainApplicationContext()); + // Custom argument resolvers + if (getCustomArgumentResolvers() != null) { + resolvers.addAll(getCustomArgumentResolvers()); } + + return resolvers; } @Override diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingInvocationTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingInvocationTests.java index 346e915d2..fcabbe2ff 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingInvocationTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/SchemaMappingInvocationTests.java @@ -15,6 +15,12 @@ */ package org.springframework.graphql.data.method.annotation.support; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; @@ -23,6 +29,9 @@ import graphql.schema.DataFetchingEnvironment; import org.dataloader.DataLoader; import org.junit.jupiter.api.Test; +import org.springframework.core.MethodParameter; +import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; +import org.springframework.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -54,13 +63,14 @@ * * @author Rossen Stoyanchev * @author Mark Paluch + * @author Genkui Du */ public class SchemaMappingInvocationTests { @Test void queryWithScalarArgument() { String query = "{ " + - " bookById(id:\"1\") { " + + " bookById { " + " id" + " name" + " author {" + @@ -215,7 +225,7 @@ private GraphQlService graphQlService() { context.refresh(); return GraphQlSetup.schemaResource(BookSource.schema) - .runtimeWiringForAnnotatedControllers(context) + .runtimeWiringForAnnotatedControllers(context, Collections.singletonList(new DefaultValueArgumentResolver())) .dataLoaders(registry) .toGraphQlService(); } @@ -231,7 +241,7 @@ public BookController(BatchLoaderRegistry batchLoaderRegistry) { } @QueryMapping - public Book bookById(@Argument Long id) { + public Book bookById(@DefaultLongValue(value = 1) long id) { return BookSource.getBookWithoutAuthor(id); } @@ -280,4 +290,27 @@ interface BookProjection { } + private static class DefaultValueArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(DefaultLongValue.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) { + DefaultLongValue annotation = parameter.getParameterAnnotation(DefaultLongValue.class); + Assert.state(annotation != null, "Expected @DefaultLongValue annotation"); + return annotation.value(); + } + } + + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @Documented + private @interface DefaultLongValue { + + long value() default 0; + + } + } diff --git a/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java b/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java index 8402428c6..850389772 100644 --- a/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java +++ b/spring-graphql/src/testFixtures/java/org/springframework/graphql/GraphQlSetup.java @@ -28,6 +28,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; +import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer; import org.springframework.graphql.execution.DataFetcherExceptionResolver; import org.springframework.graphql.execution.DataLoaderRegistrar; @@ -85,8 +86,13 @@ public GraphQlSetup runtimeWiring(RuntimeWiringConfigurer configurer) { } public GraphQlSetup runtimeWiringForAnnotatedControllers(ApplicationContext context) { + return this.runtimeWiringForAnnotatedControllers(context, new ArrayList<>()); + } + + public GraphQlSetup runtimeWiringForAnnotatedControllers(ApplicationContext context, List argumentResolverList) { AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer(); configurer.setApplicationContext(context); + configurer.setCustomArgumentResolvers(argumentResolverList); configurer.afterPropertiesSet(); return runtimeWiring(configurer); }