From e083dc86151e9e4c54b9d8e462013d91232d6e0c Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 16 May 2022 08:47:58 +0100 Subject: [PATCH] Support Callable as a controller method return value Closes gh-316 --- .../src/docs/asciidoc/index.adoc | 13 +++- .../method/InvocableHandlerMethodSupport.java | 48 +++++++++++- .../AnnotatedControllerConfigurer.java | 52 +++++++++---- .../support/BatchLoaderHandlerMethod.java | 22 ++++-- .../support/DataFetcherHandlerMethod.java | 15 ++-- .../ContextDataFetcherDecorator.java | 13 +--- .../execution/ReactorContextManager.java | 21 ++++- .../support/BatchMappingDetectionTests.java | 15 +++- .../support/BatchMappingInvocationTests.java | 25 +++++- .../support/BatchMappingTestSupport.java | 10 ++- ...ntextValueMethodArgumentResolverTests.java | 2 +- .../DataFetcherHandlerMethodTests.java | 77 +++++++++++++++++++ .../support/SchemaMappingInvocationTests.java | 19 +++-- 13 files changed, 275 insertions(+), 57 deletions(-) create mode 100644 spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethodTests.java diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index 22339f6f..da4dd467 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -1227,8 +1227,13 @@ See <>. |=== -Schema mapping handler methods can return any value, including Reactor `Mono` and -`Flux` as described in <>. +Schema mapping handler methods can return: + +- A resolved value of any type. +- `Mono` and `Flux` for asynchronous value(s). Supported for controller methods and for + any `DataFetcher` as described in <>. +- `java.util.concurrent.Callable` to have the value(s) produced asynchronously. + For this to work, `AnnotatedControllerConfigurer` must be configured with an `Executor`. @@ -1574,6 +1579,10 @@ Batch mapping methods can return: | `Map`, `Collection` | Imperative variants, e.g. without remote calls to make. +| `Callable>`, `Callable>` +| Imperative variants to be invoked asynchronously. For this to work, + `AnnotatedControllerConfigurer` must be configured with an `Executor`. + |=== diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/InvocableHandlerMethodSupport.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/InvocableHandlerMethodSupport.java index d352934f..9ef9f8e5 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/InvocableHandlerMethodSupport.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/InvocableHandlerMethodSupport.java @@ -19,14 +19,20 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.stream.Collectors; import java.util.stream.Stream; +import graphql.GraphQLContext; import reactor.core.publisher.Mono; import org.springframework.core.CoroutinesUtils; import org.springframework.core.KotlinDetector; +import org.springframework.graphql.execution.ReactorContextManager; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Extension of {@link HandlerMethod} that adds support for invoking the @@ -40,8 +46,24 @@ public abstract class InvocableHandlerMethodSupport extends HandlerMethod { private static final Object NO_VALUE = new Object(); - protected InvocableHandlerMethodSupport(HandlerMethod handlerMethod) { + private final boolean hasCallableReturnValue; + + @Nullable + private final Executor executor; + + + /** + * Create an instance. + * @param handlerMethod the controller method + * @param executor an {@link Executor} to use for {@link Callable} return values + */ + protected InvocableHandlerMethodSupport(HandlerMethod handlerMethod, @Nullable Executor executor) { super(handlerMethod.createWithResolvedBean()); + this.hasCallableReturnValue = getReturnType().getParameterType().equals(Callable.class); + this.executor = executor; + Assert.isTrue(!this.hasCallableReturnValue || this.executor != null, + "Controller method declared with Callable return value, but no Executor configured: " + + handlerMethod.getBridgedMethod().toGenericString()); } @@ -51,8 +73,9 @@ public abstract class InvocableHandlerMethodSupport extends HandlerMethod { * @return the value returned from the method or a {@code Mono} * if the invocation fails. */ + @SuppressWarnings("ReactiveStreamsUnusedPublisher") @Nullable - protected Object doInvoke(Object... argValues) { + protected Object doInvoke(GraphQLContext graphQLContext, Object... argValues) { if (logger.isTraceEnabled()) { logger.trace("Arguments: " + Arrays.toString(argValues)); } @@ -61,7 +84,8 @@ public abstract class InvocableHandlerMethodSupport extends HandlerMethod { if (KotlinDetector.isSuspendingFunction(method)) { return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), argValues); } - return method.invoke(getBean(), argValues); + Object result = method.invoke(getBean(), argValues); + return handleReturnValue(graphQLContext, result); } catch (IllegalArgumentException ex) { assertTargetBean(method, getBean(), argValues); @@ -84,6 +108,24 @@ public abstract class InvocableHandlerMethodSupport extends HandlerMethod { } } + @Nullable + private Object handleReturnValue(GraphQLContext graphQLContext, @Nullable Object result) { + if (this.hasCallableReturnValue && result != null) { + return CompletableFuture.supplyAsync( + () -> { + try { + return ReactorContextManager.invokeCallable((Callable) result, graphQLContext); + } + catch (Exception ex) { + throw new IllegalStateException( + "Failure in Callable returned from " + getBridgedMethod().toGenericString(), ex); + } + }, + this.executor); + } + return result; + } + /** * Use this method to resolve the arguments asynchronously. This is only * useful when at least one of the values is a {@link Mono} 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 9518cfe2..1dc67894 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 @@ -25,6 +25,8 @@ import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; import java.util.stream.Collectors; import javax.validation.Validator; @@ -110,6 +112,12 @@ public class AnnotatedControllerConfigurer "javax.validation.executable.ExecutableValidator", AnnotatedControllerConfigurer.class.getClassLoader()); + + private final FormattingConversionService conversionService = new DefaultFormattingConversionService(); + + @Nullable + private Executor executor; + @Nullable private ApplicationContext applicationContext; @@ -119,8 +127,6 @@ public class AnnotatedControllerConfigurer @Nullable private HandlerMethodInputValidator validator; - private FormattingConversionService conversionService = new DefaultFormattingConversionService(); - /** * Add a {@code FormatterRegistrar} to customize the {@link ConversionService} @@ -132,6 +138,17 @@ public class AnnotatedControllerConfigurer registrar.registerFormatters(this.conversionService); } + /** + * Configure an {@link Executor} to use for asynchronous handling of + * {@link Callable} return values from controller methods. + *

By default, this is not set in which case controller methods with a + * {@code Callable} return value cannot be registered. + * @param executor the executor to use + */ + public void setExecutor(Executor executor) { + this.executor = executor; + } + @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; @@ -195,7 +212,7 @@ public class AnnotatedControllerConfigurer findHandlerMethods().forEach((info) -> { DataFetcher dataFetcher; if (!info.isBatchMapping()) { - dataFetcher = new SchemaMappingDataFetcher(info, this.argumentResolvers, this.validator); + dataFetcher = new SchemaMappingDataFetcher(info, this.argumentResolvers, this.validator, this.executor); } else { String dataLoaderKey = registerBatchLoader(info); @@ -359,13 +376,16 @@ public class AnnotatedControllerConfigurer BatchLoaderRegistry registry = obtainApplicationContext().getBean(BatchLoaderRegistry.class); HandlerMethod handlerMethod = info.getHandlerMethod(); - BatchLoaderHandlerMethod invocable = new BatchLoaderHandlerMethod(handlerMethod); + BatchLoaderHandlerMethod invocable = new BatchLoaderHandlerMethod(handlerMethod, this.executor); - Class clazz = handlerMethod.getReturnType().getParameterType(); - if (clazz.equals(Flux.class) || Collection.class.isAssignableFrom(clazz)) { + MethodParameter returnType = handlerMethod.getReturnType(); + Class clazz = returnType.getParameterType(); + Class nestedClass = (clazz.equals(Callable.class) ? returnType.nested().getNestedParameterType() : clazz); + + if (clazz.equals(Flux.class) || Collection.class.isAssignableFrom(nestedClass)) { registry.forName(dataLoaderKey).registerBatchLoader(invocable::invokeForIterable); } - else if (clazz.equals(Mono.class) || clazz.equals(Map.class)) { + else if (clazz.equals(Mono.class) || nestedClass.equals(Map.class)) { registry.forName(dataLoaderKey).registerMappedBatchLoader(invocable::invokeForMap); } else { @@ -425,7 +445,7 @@ public class AnnotatedControllerConfigurer @Override public String toString() { - return this.coordinates + " -> " + this.handlerMethod.toString(); + return this.coordinates + " -> " + this.handlerMethod; } } @@ -442,25 +462,23 @@ public class AnnotatedControllerConfigurer @Nullable private final HandlerMethodInputValidator validator; + @Nullable + private final Executor executor; + private final boolean subscription; public SchemaMappingDataFetcher( MappingInfo info, HandlerMethodArgumentResolverComposite resolvers, - @Nullable HandlerMethodInputValidator validator) { + @Nullable HandlerMethodInputValidator validator, + @Nullable Executor executor) { this.info = info; this.argumentResolvers = resolvers; this.validator = validator; + this.executor = executor; this.subscription = this.info.getCoordinates().getTypeName().equalsIgnoreCase("Subscription"); } - /** - * Return the {@link FieldCoordinates} the HandlerMethod is mapped to. - */ - public FieldCoordinates getCoordinates() { - return this.info.getCoordinates(); - } - /** * Return the {@link HandlerMethod} used to fetch data. */ @@ -474,7 +492,7 @@ public class AnnotatedControllerConfigurer public Object get(DataFetchingEnvironment environment) throws Exception { DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod( - getHandlerMethod(), this.argumentResolvers, this.validator, this.subscription); + getHandlerMethod(), this.argumentResolvers, this.validator, this.executor, this.subscription); return handlerMethod.invoke(environment); } 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 26ab88ac..0be60b94 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 @@ -19,6 +19,9 @@ import java.security.Principal; import java.util.Arrays; import java.util.Collection; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Function; import graphql.GraphQLContext; import org.dataloader.BatchLoaderEnvironment; @@ -50,8 +53,8 @@ public class BatchLoaderHandlerMethod extends InvocableHandlerMethodSupport { AnnotatedControllerConfigurer.class.getClassLoader()); - public BatchLoaderHandlerMethod(HandlerMethod handlerMethod) { - super(handlerMethod); + public BatchLoaderHandlerMethod(HandlerMethod handlerMethod, @Nullable Executor executor) { + super(handlerMethod, executor); } @@ -69,11 +72,11 @@ public class BatchLoaderHandlerMethod extends InvocableHandlerMethodSupport { public Mono> invokeForMap(Collection keys, BatchLoaderEnvironment environment) { Object[] args = getMethodArgumentValues(keys, environment); if (doesNotHaveAsyncArgs(args)) { - Object result = doInvoke(args); + Object result = doInvoke(environment.getContext(), args); return toMonoMap(result); } return toArgsMono(args).flatMap(argValues -> { - Object result = doInvoke(argValues); + Object result = doInvoke(environment.getContext(), argValues); return toMonoMap(result); }); } @@ -90,11 +93,11 @@ public class BatchLoaderHandlerMethod extends InvocableHandlerMethodSupport { public Flux invokeForIterable(Collection keys, BatchLoaderEnvironment environment) { Object[] args = getMethodArgumentValues(keys, environment); if (doesNotHaveAsyncArgs(args)) { - Object result = doInvoke(args); + Object result = doInvoke(environment.getContext(), args); return toFlux(result); } return toArgsMono(args).flatMapMany(resolvedArgs -> { - Object result = doInvoke(resolvedArgs); + Object result = doInvoke(environment.getContext(), resolvedArgs); return toFlux(result); }); } @@ -165,6 +168,9 @@ public class BatchLoaderHandlerMethod extends InvocableHandlerMethodSupport { else if (result instanceof Mono) { return (Mono>) result; } + else if (result instanceof CompletableFuture) { + return Mono.fromFuture((CompletableFuture>) result); + } return Mono.error(new IllegalStateException("Unexpected return value: " + result)); } @@ -176,6 +182,10 @@ public class BatchLoaderHandlerMethod extends InvocableHandlerMethodSupport { else if (result instanceof Flux) { return (Flux) result; } + else if (result instanceof CompletableFuture) { + return Mono.fromFuture((CompletableFuture>) result) + .flatMapIterable(Function.identity()); + } return Flux.error(new IllegalStateException("Unexpected return value: " + result)); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethod.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethod.java index abd8646e..fe21a7df 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethod.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethod.java @@ -16,6 +16,7 @@ package org.springframework.graphql.data.method.annotation.support; import java.util.Arrays; +import java.util.concurrent.Executor; import graphql.schema.DataFetchingEnvironment; import org.reactivestreams.Publisher; @@ -65,9 +66,9 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport { */ public DataFetcherHandlerMethod(HandlerMethod handlerMethod, HandlerMethodArgumentResolverComposite resolvers, @Nullable HandlerMethodInputValidator validator, - boolean subscription) { + @Nullable Executor executor, boolean subscription) { - super(handlerMethod); + super(handlerMethod, executor); Assert.isTrue(!resolvers.getResolvers().isEmpty(), "No argument resolvers"); this.resolvers = resolvers; this.validator = validator; @@ -118,17 +119,17 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport { } if (Arrays.stream(args).noneMatch(arg -> arg instanceof Mono)) { - return validateAndInvoke(args); + return validateAndInvoke(args, environment); } return this.subscription ? toArgsMono(args).flatMapMany(argValues -> { - Object result = validateAndInvoke(argValues); + Object result = validateAndInvoke(argValues, environment); Assert.state(result instanceof Publisher, "Expected a Publisher from a Subscription response"); return Flux.from((Publisher) result); }) : toArgsMono(args).flatMap(argValues -> { - Object result = validateAndInvoke(argValues); + Object result = validateAndInvoke(argValues, environment); if (result instanceof Mono) { return (Mono) result; } @@ -183,11 +184,11 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport { } @Nullable - private Object validateAndInvoke(Object[] args) { + private Object validateAndInvoke(Object[] args, DataFetchingEnvironment environment) { if (this.validator != null) { this.validator.validate(this, args); } - return doInvoke(args); + return doInvoke(environment.getGraphQlContext(), args); } } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java index 184a5cdd..52def63e 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java @@ -59,16 +59,11 @@ final class ContextDataFetcherDecorator implements DataFetcher { @Override public Object get(DataFetchingEnvironment environment) throws Exception { - ContextView contextView = ReactorContextManager.getReactorContext(environment.getGraphQlContext()); - Object value; - try { - ReactorContextManager.restoreThreadLocalValues(contextView); - value = this.delegate.get(environment); - } - finally { - ReactorContextManager.resetThreadLocalValues(contextView); - } + Object value = ReactorContextManager.invokeCallable(() -> + this.delegate.get(environment), environment.getGraphQlContext()); + + ContextView contextView = ReactorContextManager.getReactorContext(environment.getGraphQlContext()); if (this.subscription) { return (!contextView.isEmpty() ? Flux.from((Publisher) value).contextWrite(contextView) : value); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ReactorContextManager.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ReactorContextManager.java index bad8c762..14480a8a 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ReactorContextManager.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ReactorContextManager.java @@ -18,6 +18,7 @@ package org.springframework.graphql.execution; import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.Callable; import graphql.GraphQLContext; import reactor.util.context.Context; @@ -70,7 +71,7 @@ public abstract class ReactorContextManager { * Use the given accessor to extract ThreadLocal values and save them in a * sub-map in the given {@link Context}, so those can be restored later * around the execution of data fetchers and exception resolvers. The accessor - * instance is also saved in the Reactor Context so it can be used to + * instance is also saved in the Reactor Context, so it can be used to * actually restore and reset ThreadLocal values. * @param accessor the accessor to use * @param context the context to write to if there are ThreadLocal values @@ -89,6 +90,24 @@ public abstract class ReactorContextManager { THREAD_ID, Thread.currentThread().getId())); } + /** + * Restore {@code ThreadLocal} values, invoke the given {@code Callable}, + * and reset the {@code ThreadLocal} values. + * @param callable the callable to invoke + * @param graphQlContext the current {@code GraphQLContext} + * @return the return value from the invocation + */ + public static T invokeCallable(Callable callable, GraphQLContext graphQlContext) throws Exception { + ContextView contextView = getReactorContext(graphQlContext); + try { + ReactorContextManager.restoreThreadLocalValues(contextView); + return callable.call(); + } + finally { + ReactorContextManager.resetThreadLocalValues(contextView); + } + } + /** * Look up saved ThreadLocal values and restore them if any are found. * This is a no-op if invoked on the thread that values were extracted on. diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingDetectionTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingDetectionTests.java index 8bf389bf..f05d1770 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingDetectionTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingDetectionTests.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. @@ -17,6 +17,7 @@ package org.springframework.graphql.data.method.annotation.support; import java.util.List; import java.util.Map; +import java.util.concurrent.Callable; import graphql.GraphQLContext; import graphql.schema.DataFetcher; @@ -28,6 +29,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.graphql.Author; import org.springframework.graphql.Book; import org.springframework.graphql.data.method.annotation.BatchMapping; @@ -59,13 +61,14 @@ public class BatchMappingDetectionTests { assertThat(dataFetcherMap).containsOnlyKeys("Book"); assertThat(dataFetcherMap.get("Book")).containsOnlyKeys( - "authorFlux", "authorList", "authorMonoMap", "authorMap", "authorEnvironment"); + "authorFlux", "authorList", "authorMonoMap", "authorMap", "authorCallableMap", "authorEnvironment"); DataLoaderRegistry registry = new DataLoaderRegistry(); this.batchLoaderRegistry.registerDataLoaders(registry, GraphQLContext.newContext().build()); assertThat(registry.getDataLoadersMap()).containsOnlyKeys( - "Book.authorFlux", "Book.authorList", "Book.authorMonoMap", "Book.authorMap", "Book.authorEnvironment"); + "Book.authorFlux", "Book.authorList", "Book.authorMonoMap", "Book.authorMap", + "Book.authorCallableMap", "Book.authorEnvironment"); } @Test @@ -87,6 +90,7 @@ public class BatchMappingDetectionTests { context.refresh(); AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer(); + configurer.setExecutor(new SimpleAsyncTaskExecutor()); configurer.setApplicationContext(context); configurer.afterPropertiesSet(); @@ -120,6 +124,11 @@ public class BatchMappingDetectionTests { return null; } + @BatchMapping + public Callable> authorCallableMap(List books) { + return null; + } + @BatchMapping public List authorEnvironment(BatchLoaderEnvironment environment, List books) { return null; diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingInvocationTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingInvocationTests.java index 6d23982d..971ebe26 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingInvocationTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingInvocationTests.java @@ -18,6 +18,7 @@ package org.springframework.graphql.data.method.annotation.support; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Callable; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -51,7 +52,8 @@ public class BatchMappingInvocationTests extends BatchMappingTestSupport { arguments(named("Returning Mono>", new BatchMonoMapController())), arguments(named("Returning Map", new BatchMapController())), arguments(named("Returning Flux", new BatchFluxController())), - arguments(named("Returning List", new BatchListController())) + arguments(named("Returning List", new BatchListController())), + arguments(named("Returning Callable>", new BatchCallableMapController())) ); } @@ -141,6 +143,7 @@ public class BatchMappingInvocationTests extends BatchMappingTestSupport { } } + @Controller private static class BatchMapController extends CourseController { @@ -153,8 +156,10 @@ public class BatchMappingInvocationTests extends BatchMappingTestSupport { public Map> students(List courses) { return courses.stream().collect(Collectors.toMap(Function.identity(), Course::students)); } + } + @Controller private static class BatchFluxController extends CourseController { @@ -169,6 +174,7 @@ public class BatchMappingInvocationTests extends BatchMappingTestSupport { } } + @Controller private static class BatchListController extends CourseController { @@ -183,4 +189,21 @@ public class BatchMappingInvocationTests extends BatchMappingTestSupport { } } + + @Controller + private static class BatchCallableMapController extends CourseController { + + @BatchMapping + public Callable> instructor(List courses) { + return () -> courses.stream().collect(Collectors.toMap(Function.identity(), Course::instructor)); + } + + @BatchMapping + public Callable>> students(List courses) { + return () -> courses.stream().collect(Collectors.toMap(Function.identity(), Course::students)); + } + + } + + } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingTestSupport.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingTestSupport.java index a63e9b00..790e66f1 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingTestSupport.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/BatchMappingTestSupport.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. @@ -28,6 +28,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.graphql.ExecutionGraphQlService; import org.springframework.graphql.GraphQlSetup; import org.springframework.graphql.data.method.annotation.QueryMapping; @@ -87,8 +88,13 @@ public class BatchMappingTestSupport { context.registerBean(BatchLoaderRegistry.class, () -> registry); context.refresh(); + AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer(); + configurer.setExecutor(new SimpleAsyncTaskExecutor()); + configurer.setApplicationContext(context); + configurer.afterPropertiesSet(); + return GraphQlSetup.schemaContent(schema) - .runtimeWiringForAnnotatedControllers(context) + .runtimeWiring(configurer) .dataLoaders(registry) .toGraphQlService(); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolverTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolverTests.java index 1ce6be64..d4c0a434 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolverTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/ContextValueMethodArgumentResolverTests.java @@ -119,7 +119,7 @@ public class ContextValueMethodArgumentResolverTests { DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod( new HandlerMethod(new TestController(), TestController.class.getMethod("handleMono", Mono.class)), - resolvers, null, false); + resolvers, null, null, false); GraphQLContext graphQLContext = new GraphQLContext.Builder().build(); diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethodTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethodTests.java new file mode 100644 index 00000000..7ccade17 --- /dev/null +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethodTests.java @@ -0,0 +1,77 @@ +/* + * 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.annotation.support; + + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; + +import graphql.GraphQLContext; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.DataFetchingEnvironmentImpl; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.graphql.data.method.HandlerMethod; +import org.springframework.graphql.data.method.HandlerMethodArgumentResolver; +import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DataFetcherHandlerMethod}. + * + * @author Rossen Stoyanchev + */ +public class DataFetcherHandlerMethodTests { + + + @Test + void callableReturnValue() throws Exception { + + HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); + resolvers.addResolver(Mockito.mock(HandlerMethodArgumentResolver.class)); + + DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod( + new HandlerMethod(new TestController(), TestController.class.getMethod("handleAndReturnCallable")), + resolvers, null, new SimpleAsyncTaskExecutor(), false); + + GraphQLContext graphQLContext = new GraphQLContext.Builder().build(); + + DataFetchingEnvironment environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment() + .graphQLContext(graphQLContext) + .build(); + + Object result = handlerMethod.invoke(environment); + + assertThat(result).isInstanceOf(CompletableFuture.class); + CompletableFuture future = (CompletableFuture) result; + assertThat(future.get()).isEqualTo("A"); + } + + + private static class TestController { + + @Nullable + public Callable handleAndReturnCallable() { + return () -> "A"; + } + + } + +} 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 07e01b60..290f9104 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 @@ -16,6 +16,7 @@ package org.springframework.graphql.data.method.annotation.support; import java.util.List; +import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; @@ -28,6 +29,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.data.web.ProjectedPayload; import org.springframework.graphql.Author; import org.springframework.graphql.Book; @@ -213,8 +215,13 @@ public class SchemaMappingInvocationTests { context.registerBean(BatchLoaderRegistry.class, () -> registry); context.refresh(); + AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer(); + configurer.setExecutor(new SimpleAsyncTaskExecutor()); + configurer.setApplicationContext(context); + configurer.afterPropertiesSet(); + return GraphQlSetup.schemaResource(BookSource.schema) - .runtimeWiringForAnnotatedControllers(context) + .runtimeWiring(configurer) .dataLoaders(registry) .toGraphQlService(); } @@ -255,10 +262,12 @@ public class SchemaMappingInvocationTests { } @QueryMapping - public Author authorById(DataFetchingEnvironment environment, GraphQLContext context) { - context.put("key", "value"); - String id = environment.getArgument("id"); - return BookSource.getAuthor(Long.parseLong(id)); + public Callable authorById(DataFetchingEnvironment environment, GraphQLContext context) { + return () -> { + context.put("key", "value"); + String id = environment.getArgument("id"); + return BookSource.getAuthor(Long.parseLong(id)); + }; } @MutationMapping