From aa1ee77867aee19bedef1f7eb5451fe46fc1c5e4 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 20 May 2024 07:15:14 +0100 Subject: [PATCH] Document how to configure custom ExecutionStrategy Closes gh-832 --- .../modules/ROOT/pages/request-execution.adoc | 33 +++++++++++++++ .../DataFetcherExceptionResolver.java | 24 ++++++----- .../ExceptionResolversExceptionHandler.java | 41 +++++++++++-------- 3 files changed, 70 insertions(+), 28 deletions(-) diff --git a/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc b/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc index 5e6d7a17..c32dd300 100644 --- a/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc +++ b/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc @@ -196,6 +196,39 @@ https://github.com/graphql-java/graphql-java-extended-validation[Extended Valida library. +[[execution.graphqlsource.execution-strategy]] +=== `ExecutionStrategy` + +An `ExecutionStrategy` in GraphQL Java drives the fetching of requested fields. +To create an `ExecutionStrategy`, you need to provide a `DataFetcherExceptionHandler`. +By default, Spring for GraphQL creates the exception handler to use as described in +xref:request-execution.adoc#execution.exceptions[Exceptions] and sets it on the +`GraphQL.Builder`. GraphQL Java then uses that to create `AsyncExecutionStrategy` +instances with the configured exception handler. + +If you need to create a custom `ExecutionStrategy`, you can detect +``DataFetcherExceptionResolver``s and create an exception handler in the same way, and use +it to create the custom `ExecutionStrategy`. For example, in a Spring Boot application: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Bean +GraphQlSourceBuilderCustomizer sourceBuilderCustomizer( + ObjectProvider resolvers) { + + DataFetcherExceptionHandler exceptionHandler = + DataFetcherExceptionResolver.createExceptionHandler(resolvers.stream().toList()); + + AsyncExecutionStrategy strategy = new CustomAsyncExecutionStrategy(exceptionHandler); + + return sourceBuilder -> sourceBuilder.configureGraphQl(builder -> + builder.queryExecutionStrategy(strategy).mutationExecutionStrategy(strategy)); +} +---- + + + + [[execution.graphqlsource.schema-transformation]] === Schema Transformation diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java index 724e08ad..c5a3518c 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/DataFetcherExceptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -26,10 +26,12 @@ import reactor.core.publisher.Mono; /** * Contract to resolve exceptions from {@link graphql.schema.DataFetcher}s. - * Implementations are typically declared as beans in Spring configuration and - * are invoked sequentially until one emits a List of {@link GraphQLError}s. + * Resolves are typically declared as Spring beans and invoked in turn until one + * resolves the exception by emitting a (possibly empty) {@code GraphQLError} list. + * Use the static factory method {@link #createExceptionHandler} to create a + * {@link DataFetcherExceptionHandler} from a list of resolvers. * - *

Most resolver implementations can extend + *

Resolver implementations can extend * {@link DataFetcherExceptionResolverAdapter} and override one of its * {@link DataFetcherExceptionResolverAdapter#resolveToSingleError resolveToSingleError} or * {@link DataFetcherExceptionResolverAdapter#resolveToMultipleErrors resolveToMultipleErrors} @@ -85,13 +87,13 @@ public interface DataFetcherExceptionResolver { } /** - * Factory method to create a {@link DataFetcherExceptionResolver} from a - * list of resolvers. Spring for GraphQL uses this method to set - * {@link graphql.GraphQL.Builder#defaultDataFetcherExceptionHandler(DataFetcherExceptionHandler)} - * from resolvers found in Spring configuration, and that default handler - * is used in turn to create each {@code ExecutionStrategy}. Applications - * may also find this factory method useful when creating a custom - * {@code ExecutionStrategy}. + * Factory method to create a {@link DataFetcherExceptionHandler} from a + * list of {@link DataFetcherExceptionResolver}'s. This is used internally + * in {@link AbstractGraphQlSourceBuilder} to set the exception handler on + * {@link graphql.GraphQL.Builder}, which in turn is used to create + * {@link graphql.execution.ExecutionStrategy}'s. Applications may also use + * this method to create an exception handler when they to need to initialize + * a custom {@code ExecutionStrategy}. *

Resolvers are invoked in turn until one resolves the exception by * emitting a (possibly empty) {@code GraphQLError} list. If the exception * remains unresolved, the handler creates a {@code GraphQLError} with diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java index 2ecd06e3..a4d4874b 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandler.java @@ -40,14 +40,19 @@ import org.springframework.util.Assert; * {@link DataFetcherExceptionHandler} that invokes {@link DataFetcherExceptionResolver}'s * in a sequence until one returns a list of {@link GraphQLError}'s. * + *

Use {@link DataFetcherExceptionResolver#createExceptionHandler(List)} to + * create an instance. + * * @author Rossen Stoyanchev */ class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler { private static final Log logger = LogFactory.getLog(ExceptionResolversExceptionHandler.class); + private final List resolvers; + /** * Create an instance. * @param resolvers the resolvers to use @@ -60,9 +65,11 @@ class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler @Override @SuppressWarnings("deprecation") - public CompletableFuture handleException(DataFetcherExceptionHandlerParameters params) { - Throwable exception = unwrapException(params); - DataFetchingEnvironment env = params.getDataFetchingEnvironment(); + public CompletableFuture handleException( + DataFetcherExceptionHandlerParameters handlerParameters) { + + Throwable exception = unwrapException(handlerParameters); + DataFetchingEnvironment env = handlerParameters.getDataFetchingEnvironment(); ContextSnapshot snapshot = ContextSnapshot.captureFrom(env.getGraphQlContext()); try { return Flux.fromIterable(this.resolvers) @@ -80,15 +87,6 @@ class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler } } - private DataFetcherExceptionHandlerResult handleResolverError( - Throwable resolverException, Throwable originalException, DataFetchingEnvironment environment) { - - if (logger.isWarnEnabled()) { - logger.warn("Failure while resolving " + originalException.getMessage(), resolverException); - } - return createInternalError(originalException, environment); - } - private Throwable unwrapException(DataFetcherExceptionHandlerParameters params) { Throwable ex = params.getException(); return ((ex instanceof CompletionException) ? ex.getCause() : ex); @@ -96,18 +94,27 @@ class ExceptionResolversExceptionHandler implements DataFetcherExceptionHandler private void logResolvedException(Throwable ex, DataFetcherExceptionHandlerResult result) { if (logger.isDebugEnabled()) { - logger.debug("Resolved " + ex.getClass().getSimpleName() + - " to GraphQL error(s): " + result.getErrors(), ex); + String name = ex.getClass().getSimpleName(); + logger.debug("Resolved " + name + " to GraphQL error(s): " + result.getErrors(), ex); } } - private DataFetcherExceptionHandlerResult createInternalError(Throwable ex, DataFetchingEnvironment environment) { - ExecutionId executionId = environment.getExecutionId(); + private DataFetcherExceptionHandlerResult handleResolverError( + Throwable resolverException, Throwable originalException, DataFetchingEnvironment env) { + + if (logger.isWarnEnabled()) { + logger.warn("Failure while resolving " + originalException.getMessage(), resolverException); + } + return createInternalError(originalException, env); + } + + private DataFetcherExceptionHandlerResult createInternalError(Throwable ex, DataFetchingEnvironment env) { + ExecutionId executionId = env.getExecutionId(); if (logger.isErrorEnabled()) { logger.error("Unresolved " + ex.getClass().getSimpleName() + " for executionId " + executionId, ex); } return DataFetcherExceptionHandlerResult - .newResult(GraphqlErrorBuilder.newError(environment) + .newResult(GraphqlErrorBuilder.newError(env) .errorType(ErrorType.INTERNAL_ERROR) .message(ErrorType.INTERNAL_ERROR + " for " + executionId) .build())