diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolver.java
new file mode 100644
index 000000000..35807a2b9
--- /dev/null
+++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolver.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.graphql.data.method;
+
+import graphql.schema.DataFetchingEnvironment;
+import org.dataloader.BatchLoaderEnvironment;
+import org.springframework.core.MethodParameter;
+import org.springframework.lang.Nullable;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Strategy interface for resolving method parameters into argument values in
+ * the context of a given {@link BatchLoaderEnvironment} and parent/source list.
+ *
+ *
Most implementations will be synchronous, simply resolving values from the
+ * {@code BatchLoaderEnvironment} and parent/source list. However, a resolver may
+ * also return a {@link reactor.core.publisher.Mono} if it needs to be asynchronous.
+ *
+ * @author Genkui Du
+ * @since 1.0.0
+ */
+public interface BatchHandlerMethodArgumentResolver {
+
+ /**
+ * Whether this resolver supports the given {@link MethodParameter}.
+ *
+ * @param parameter the method parameter
+ */
+ boolean supportsParameter(MethodParameter parameter);
+
+ /**
+ * Resolve a method parameter into an argument value.
+ *
+ * @param parameter the method parameter to resolve. This parameter must
+ * have previously checked via {@link #supportsParameter}.
+ * @param keys the list of keys to load
+ * @param keyContexts keys and their context objects map
+ * @param environment the environment to use to resolve the value
+ * @param the type of parent/source type.
+ *
+ * @return the resolved value, which may be {@code null} if not resolved;
+ * the value may also be a {@link reactor.core.publisher.Mono} if it
+ * requires asynchronous resolution.
+ *
+ * @throws Exception in case of errors with the preparation of argument values
+ */
+ @Nullable
+ Object resolveArgument(MethodParameter parameter,
+ Collection keys,
+ Map keyContexts,
+ BatchLoaderEnvironment environment) throws Exception;
+
+}
diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolverComposite.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolverComposite.java
new file mode 100644
index 000000000..78e662351
--- /dev/null
+++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/BatchHandlerMethodArgumentResolverComposite.java
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.graphql.data.method;
+
+import graphql.schema.DataFetchingEnvironment;
+import org.dataloader.BatchLoaderEnvironment;
+import org.springframework.core.MethodParameter;
+import org.springframework.lang.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Resolves method parameters by delegating to a list of registered
+ * {@link BatchHandlerMethodArgumentResolver BatchHandlerMethodArgumentResolvers}.
+ * Previously resolved method parameters are cached for faster lookups.
+ *
+ * @author Genkui Du
+ * @since 1.0.0
+ */
+public class BatchHandlerMethodArgumentResolverComposite implements BatchHandlerMethodArgumentResolver {
+
+ private final List argumentResolvers = new ArrayList<>();
+
+ private final Map argumentResolverCache =
+ new ConcurrentHashMap<>(256);
+
+
+ /**
+ * Return a read-only list with the contained resolvers, or an empty list.
+ */
+ public List getArgumentResolvers() {
+ return Collections.unmodifiableList(argumentResolvers);
+ }
+
+ /**
+ * Add the given {@link BatchHandlerMethodArgumentResolver BatchHandlerMethodArgumentResolvers}.
+ */
+ public BatchHandlerMethodArgumentResolverComposite addResolvers(
+ @Nullable List extends BatchHandlerMethodArgumentResolver> resolvers) {
+
+ if (resolvers != null) {
+ this.argumentResolvers.addAll(resolvers);
+ }
+ return this;
+ }
+
+ /**
+ * Clear the list of configured resolvers and the resolver cache.
+ */
+ public void clear() {
+ this.argumentResolvers.clear();
+ this.argumentResolverCache.clear();
+ }
+
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return this.getArgumentResolver(parameter) != null;
+ }
+
+ @Override
+ public Object resolveArgument(MethodParameter parameter,
+ Collection keys,
+ Map keyContexts,
+ BatchLoaderEnvironment environment) throws Exception {
+
+ BatchHandlerMethodArgumentResolver argumentResolver = getArgumentResolver(parameter);
+ if (argumentResolver == null) {
+ throw new IllegalArgumentException("Unsupported parameter type [" +
+ parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
+ }
+ return argumentResolver.resolveArgument(parameter, keys, keyContexts, environment);
+
+ }
+
+ /**
+ * Find a registered {@link BatchHandlerMethodArgumentResolver} that supports
+ * the given method parameter.
+ */
+ @Nullable
+ private BatchHandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
+ BatchHandlerMethodArgumentResolver result = argumentResolverCache.get(parameter);
+ if (result == null) {
+ for (BatchHandlerMethodArgumentResolver argumentResolver : this.argumentResolvers) {
+ if (argumentResolver.supportsParameter(parameter)) {
+ result = argumentResolver;
+ this.argumentResolverCache.put(parameter, argumentResolver);
+ break;
+ }
+ }
+ }
+
+ return result;
+ }
+
+}
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..115fa1014 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.
@@ -49,6 +49,18 @@ public void addResolver(HandlerMethodArgumentResolver resolver) {
this.argumentResolvers.add(resolver);
}
+ /**
+ * Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
+ */
+ public HandlerMethodArgumentResolverComposite addResolvers(
+ @Nullable List extends HandlerMethodArgumentResolver> 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..6277b8af9 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;
@@ -38,6 +40,14 @@
import org.dataloader.DataLoader;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.BeanResolver;
+import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolver;
+import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolverComposite;
+import org.springframework.graphql.data.method.annotation.support.batch.BatchLoaderEnvironmentBatchMethodArgumentResolver;
+import org.springframework.graphql.data.method.annotation.support.batch.ContextValueBatchMethodArgumentResolver;
+import org.springframework.graphql.data.method.annotation.support.batch.ContinuationBatchMethodArgumentResolver;
+import org.springframework.graphql.data.method.annotation.support.batch.GraphQLContextBatchMethodArgumentResolver;
+import org.springframework.graphql.data.method.annotation.support.batch.PrincipalBatchMethodArgumentResolver;
+import org.springframework.graphql.data.method.annotation.support.batch.SourceBatchMethodArgumentResolver;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -74,6 +84,7 @@
*
* @author Rossen Stoyanchev
* @author Brian Clozel
+ * @author Genkui Du
* @since 1.0.0
*/
public class AnnotatedControllerConfigurer
@@ -111,6 +122,12 @@ public class AnnotatedControllerConfigurer
@Nullable
private HandlerMethodArgumentResolverComposite argumentResolvers;
+ @Nullable
+ private List customBatchMethodArgumentResolvers;
+
+ @Nullable
+ private BatchHandlerMethodArgumentResolverComposite batchMethodArgumentResolvers;
+
@Nullable
private HandlerMethodInputValidator validator;
@@ -138,6 +155,22 @@ public void setConversionService(ConversionService conversionService) {
this.conversionService = (FormattingConversionService) conversionService;
}
+ /**
+ * Provide resolvers for custom batch method argument types. Custom resolvers
+ * are ordered after built-in ones.
+ */
+ public void setCustomBatchMethodArgumentResolvers(@Nullable List customBatchMethodArgumentResolvers) {
+ this.customBatchMethodArgumentResolvers = customBatchMethodArgumentResolvers;
+ }
+
+ /**
+ * Return the custom custom batch method argument resolvers, or {@code null}.
+ */
+ @Nullable
+ public List getCustomBatchMethodArgumentResolvers() {
+ return this.customBatchMethodArgumentResolvers;
+ }
+
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
@@ -151,37 +184,69 @@ protected final ApplicationContext obtainApplicationContext() {
@Override
public void afterPropertiesSet() {
- this.argumentResolvers = new HandlerMethodArgumentResolverComposite();
+
+ List resolvers = getDefaultArgumentResolvers();
+ this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
+
+ List batchMethodResolvers = getBatchMethodDefaultArgumentResolvers();
+ batchMethodArgumentResolvers = new BatchHandlerMethodArgumentResolverComposite().addResolvers(batchMethodResolvers);
+
+ if (beanValidationPresent) {
+ this.validator = HandlerMethodInputValidatorFactory.create(obtainApplicationContext());
+ }
+ }
+
+ 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());
+ resolvers.add(new SourceMethodArgumentResolver());
- if (beanValidationPresent) {
- this.validator = HandlerMethodInputValidatorFactory.create(obtainApplicationContext());
+ return resolvers;
+ }
+
+ private List getBatchMethodDefaultArgumentResolvers() {
+ List resolvers = new ArrayList<>();
+
+ resolvers.add(new SourceBatchMethodArgumentResolver());
+ resolvers.add(new ContextValueBatchMethodArgumentResolver());
+ resolvers.add(new GraphQLContextBatchMethodArgumentResolver());
+ resolvers.add(new BatchLoaderEnvironmentBatchMethodArgumentResolver());
+ if (KotlinDetector.isKotlinPresent()) {
+ resolvers.add(new ContinuationBatchMethodArgumentResolver());
+ }
+ if (springSecurityPresent) {
+ resolvers.add(new PrincipalBatchMethodArgumentResolver());
+ }
+
+ // Custom batch method argument resolvers
+ if (getCustomBatchMethodArgumentResolvers() != null) {
+ resolvers.addAll(getCustomBatchMethodArgumentResolvers());
}
+ return resolvers;
}
@Override
@@ -355,7 +420,7 @@ private String registerBatchLoader(MappingInfo info) {
BatchLoaderRegistry registry = obtainApplicationContext().getBean(BatchLoaderRegistry.class);
HandlerMethod handlerMethod = info.getHandlerMethod();
- BatchLoaderHandlerMethod invocable = new BatchLoaderHandlerMethod(handlerMethod);
+ BatchLoaderHandlerMethod invocable = new BatchLoaderHandlerMethod(handlerMethod, this.batchMethodArgumentResolvers, this.validator);
Class> clazz = handlerMethod.getReturnType().getParameterType();
if (clazz.equals(Flux.class) || Collection.class.isAssignableFrom(clazz)) {
@@ -465,7 +530,7 @@ public Object get(DataFetchingEnvironment env) {
if (dataLoader == null) {
throw new IllegalStateException("No DataLoader for key '" + this.dataLoaderKey + "'");
}
- return dataLoader.load(env.getSource());
+ return dataLoader.load(env.getSource(), env);
}
}
diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/BatchLoaderHandlerMethod.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/BatchLoaderHandlerMethod.java
index cd5824321..0a763e721 100644
--- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/BatchLoaderHandlerMethod.java
+++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/BatchLoaderHandlerMethod.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.
@@ -15,23 +15,21 @@
*/
package org.springframework.graphql.data.method.annotation.support;
-import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
-import graphql.GraphQLContext;
import org.dataloader.BatchLoaderEnvironment;
+import org.springframework.graphql.data.method.BatchHandlerMethodArgumentResolverComposite;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
-import org.springframework.core.CollectionFactory;
import org.springframework.core.MethodParameter;
import org.springframework.graphql.data.method.HandlerMethod;
import org.springframework.graphql.data.method.InvocableHandlerMethodSupport;
-import org.springframework.graphql.data.method.annotation.ContextValue;
import org.springframework.lang.Nullable;
-import org.springframework.util.ClassUtils;
/**
* An extension of {@link HandlerMethod} for annotated handler methods adapted to
@@ -40,19 +38,48 @@
* {@link BatchLoaderEnvironment} as their input.
*
* @author Rossen Stoyanchev
+ * @author Genkui Du
* @since 1.0.0
*/
public class BatchLoaderHandlerMethod extends InvocableHandlerMethodSupport {
- private final static boolean springSecurityPresent = ClassUtils.isPresent(
- "org.springframework.security.core.context.SecurityContext",
- AnnotatedControllerConfigurer.class.getClassLoader());
+ private static final Object[] EMPTY_ARGS = new Object[0];
+ private final BatchHandlerMethodArgumentResolverComposite resolvers;
- public BatchLoaderHandlerMethod(HandlerMethod handlerMethod) {
+ @Nullable
+ private final HandlerMethodInputValidator validator;
+
+ /**
+ * Constructor with a parent handler method.
+ * @param handlerMethod the handler method
+ * @param resolvers the argument resolvers
+ * @param validator the input validator
+ */
+ public BatchLoaderHandlerMethod(HandlerMethod handlerMethod,
+ BatchHandlerMethodArgumentResolverComposite resolvers,
+ @Nullable HandlerMethodInputValidator validator) {
super(handlerMethod);
+ Assert.isTrue(!resolvers.getArgumentResolvers().isEmpty(), "No argument resolvers");
+ this.resolvers = resolvers;
+ this.validator = validator;
+ }
+
+
+ /**
+ * Return the configured argument resolvers.
+ */
+ public BatchHandlerMethodArgumentResolverComposite getResolvers() {
+ return resolvers;
}
+ /**
+ * Return the configured input validator.
+ */
+ @Nullable
+ public HandlerMethodInputValidator getValidator() {
+ return this.validator;
+ }
/**
* Invoke the underlying batch loader method with a collection of keys to
@@ -66,7 +93,16 @@ public BatchLoaderHandlerMethod(HandlerMethod handlerMethod) {
*/
@Nullable
public Mono