Skip to content

Commit da29846

Browse files
committed
Support annotated exception handler methods
See gh-160
1 parent 610a658 commit da29846

File tree

7 files changed

+907
-18
lines changed

7 files changed

+907
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.graphql.data.method.annotation;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
import java.util.List;
24+
25+
/**
26+
* Declares a method as a handler of exceptions raised while fetching data
27+
* for a field. When declared in an
28+
* {@link org.springframework.stereotype.Controller @Controller}, it applies to
29+
* {@code @SchemaMapping} methods of that controller only. When declared in an
30+
* {@link org.springframework.web.bind.annotation.ControllerAdvice @ControllerAdvice}
31+
* it applies across controllers.
32+
*
33+
* <p>You can also use annotated exception handler methods in
34+
* {@code @ControllerAdvice} beans to handle exceptions from non-controller
35+
* {@link graphql.schema.DataFetcher}s by obtaining
36+
* {@link org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer#getExceptionResolver()}
37+
* and registering it with
38+
* {@link org.springframework.graphql.execution.GraphQlSource.Builder#exceptionResolvers(List)
39+
* GraphQlSource.Builder}.
40+
*
41+
* @author Rossen Stoyanchev
42+
* @since 1.2
43+
*/
44+
@Target(ElementType.METHOD)
45+
@Retention(RetentionPolicy.RUNTIME)
46+
@Documented
47+
public @interface GraphQlExceptionHandler {
48+
49+
/**
50+
* Exceptions handled by the annotated method. If empty, defaults to
51+
* exception types declared in the method signature.
52+
*/
53+
Class<? extends Throwable>[] value() default {};
54+
55+
}

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323
import java.util.Collections;
2424
import java.util.HashMap;
2525
import java.util.LinkedHashSet;
26+
import java.util.List;
2627
import java.util.Map;
2728
import java.util.Set;
2829
import java.util.concurrent.Callable;
2930
import java.util.concurrent.Executor;
3031
import java.util.function.Consumer;
3132
import java.util.stream.Collectors;
3233

34+
import graphql.execution.DataFetcherResult;
3335
import graphql.schema.DataFetcher;
3436
import graphql.schema.DataFetchingEnvironment;
3537
import graphql.schema.FieldCoordinates;
@@ -38,6 +40,7 @@
3840
import org.apache.commons.logging.Log;
3941
import org.apache.commons.logging.LogFactory;
4042
import org.dataloader.DataLoader;
43+
import org.reactivestreams.Publisher;
4144
import reactor.core.publisher.Flux;
4245
import reactor.core.publisher.Mono;
4346

@@ -62,7 +65,9 @@
6265
import org.springframework.graphql.data.method.annotation.BatchMapping;
6366
import org.springframework.graphql.data.method.annotation.SchemaMapping;
6467
import org.springframework.graphql.execution.BatchLoaderRegistry;
68+
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
6569
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
70+
import org.springframework.graphql.execution.SubscriptionPublisherException;
6671
import org.springframework.lang.Nullable;
6772
import org.springframework.stereotype.Controller;
6873
import org.springframework.util.Assert;
@@ -127,6 +132,9 @@ public class AnnotatedControllerConfigurer
127132
@Nullable
128133
private ValidationHelper validationHelper;
129134

135+
@Nullable
136+
private AnnotatedControllerExceptionResolver exceptionResolver;
137+
130138

131139
/**
132140
* Add a {@code FormatterRegistrar} to customize the {@link ConversionService}
@@ -165,6 +173,25 @@ public void setApplicationContext(ApplicationContext applicationContext) {
165173
this.applicationContext = applicationContext;
166174
}
167175

176+
/**
177+
* Return a {@link DataFetcherExceptionResolver} that resolves exceptions with
178+
* {@code @GraphQlExceptionHandler} methods in {@code @ControllerAdvice}
179+
* classes declared in Spring configuration. This is useful primarily for
180+
* exceptions from non-controller {@link DataFetcher}s since exceptions from
181+
* {@code @SchemaMapping} controller methods are handled automatically at
182+
* the point of invocation.
183+
*
184+
* @return a resolver instance that can be plugged into
185+
* {@link org.springframework.graphql.execution.GraphQlSource.Builder#exceptionResolvers(List)
186+
* GraphQlSource.Builder}
187+
*
188+
* @since 1.2
189+
*/
190+
public DataFetcherExceptionResolver getExceptionResolver() {
191+
Assert.notNull(this.exceptionResolver, "ExceptionResolver is not initialized, was afterPropertiesSet called?");
192+
return (ex, env) -> this.exceptionResolver.resolveException(ex, env, null);
193+
}
194+
168195
@Nullable
169196
HandlerMethodArgumentResolverComposite getArgumentResolvers() {
170197
return this.argumentResolvers;
@@ -175,6 +202,11 @@ public void afterPropertiesSet() {
175202

176203
this.argumentResolvers = initArgumentResolvers();
177204

205+
this.exceptionResolver = new AnnotatedControllerExceptionResolver(this.argumentResolvers);
206+
if (this.applicationContext != null) {
207+
this.exceptionResolver.registerControllerAdvice(this.applicationContext);
208+
}
209+
178210
if (beanValidationPresent) {
179211
this.validationHelper = ValidationHelper.createIfValidatorPresent(obtainApplicationContext());
180212
}
@@ -222,12 +254,13 @@ protected final ApplicationContext obtainApplicationContext() {
222254
@Override
223255
public void configure(RuntimeWiring.Builder runtimeWiringBuilder) {
224256
Assert.state(this.argumentResolvers != null, "`argumentResolvers` is not initialized");
257+
Assert.state(this.exceptionResolver != null, "`exceptionResolver` is not initialized");
225258

226259
findHandlerMethods().forEach((info) -> {
227260
DataFetcher<?> dataFetcher;
228261
if (!info.isBatchMapping()) {
229262
dataFetcher = new SchemaMappingDataFetcher(
230-
info, this.argumentResolvers, this.validationHelper, this.executor);
263+
info, this.argumentResolvers, this.validationHelper, this.exceptionResolver, this.executor);
231264
}
232265
else {
233266
String dataLoaderKey = registerBatchLoader(info);
@@ -493,19 +526,30 @@ static class SchemaMappingDataFetcher implements DataFetcher<Object> {
493526
@Nullable
494527
private final Consumer<Object[]> methodValidationHelper;
495528

529+
private final AnnotatedControllerExceptionResolver exceptionResolver;
530+
496531
@Nullable
497532
private final Executor executor;
498533

499534
private final boolean subscription;
500535

501536
SchemaMappingDataFetcher(
502-
MappingInfo info, HandlerMethodArgumentResolverComposite resolvers,
503-
@Nullable ValidationHelper validationHelper, @Nullable Executor executor) {
537+
MappingInfo info, HandlerMethodArgumentResolverComposite argumentResolvers,
538+
@Nullable ValidationHelper helper, AnnotatedControllerExceptionResolver exceptionResolver,
539+
@Nullable Executor executor) {
504540

505541
this.info = info;
506-
this.argumentResolvers = resolvers;
507-
this.methodValidationHelper = (validationHelper != null ?
508-
validationHelper.getValidationHelperFor(info.getHandlerMethod()) : null);
542+
this.argumentResolvers = argumentResolvers;
543+
544+
this.methodValidationHelper =
545+
(helper != null ? helper.getValidationHelperFor(info.getHandlerMethod()) : null);
546+
547+
// Register controllers early to validate exception handler return types
548+
Class<?> controllerType = info.getHandlerMethod().getBeanType();
549+
exceptionResolver.registerController(controllerType);
550+
551+
this.exceptionResolver = exceptionResolver;
552+
509553
this.executor = executor;
510554
this.subscription = this.info.getCoordinates().getTypeName().equalsIgnoreCase("Subscription");
511555
}
@@ -517,17 +561,53 @@ public HandlerMethod getHandlerMethod() {
517561
return this.info.getHandlerMethod();
518562
}
519563

520-
521564
@Override
522-
@SuppressWarnings("ConstantConditions")
565+
@SuppressWarnings({"ConstantConditions", "ReactiveStreamsUnusedPublisher"})
523566
public Object get(DataFetchingEnvironment environment) throws Exception {
524567

525568
DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod(
526569
getHandlerMethod(), this.argumentResolvers, this.methodValidationHelper,
527570
this.executor, this.subscription);
528571

529-
return handlerMethod.invoke(environment);
572+
try {
573+
Object result = handlerMethod.invoke(environment);
574+
return applyExceptionHandling(environment, handlerMethod, result);
575+
}
576+
catch (Throwable ex) {
577+
return handleException(ex, environment, handlerMethod);
578+
}
579+
}
580+
581+
@SuppressWarnings({"unchecked", "ReactiveStreamsUnusedPublisher"})
582+
private <T> Object applyExceptionHandling(
583+
DataFetchingEnvironment env, DataFetcherHandlerMethod handlerMethod, Object result) {
584+
585+
if (this.subscription && result instanceof Publisher<?> publisher) {
586+
result = Flux.from(publisher).onErrorResume(ex -> handleSubscriptionError(ex, env, handlerMethod));
587+
}
588+
else if (result instanceof Mono) {
589+
result = ((Mono<T>) result).onErrorResume(ex -> (Mono<T>) handleException(ex, env, handlerMethod));
590+
}
591+
else if (result instanceof Flux<?>) {
592+
result = ((Flux<T>) result).onErrorResume(ex -> (Mono<T>) handleException(ex, env, handlerMethod));
593+
}
594+
return result;
530595
}
596+
597+
private Mono<DataFetcherResult<?>> handleException(
598+
Throwable ex, DataFetchingEnvironment env, DataFetcherHandlerMethod handlerMethod) {
599+
600+
return this.exceptionResolver.resolveException(ex, env, handlerMethod.getBean())
601+
.map(errors -> DataFetcherResult.newResult().errors(errors).build());
602+
}
603+
604+
private <T> Publisher<T> handleSubscriptionError(
605+
Throwable ex, DataFetchingEnvironment env, DataFetcherHandlerMethod handlerMethod) {
606+
607+
return this.exceptionResolver.resolveException(ex, env, handlerMethod.getBean())
608+
.flatMap(errors -> Mono.error(new SubscriptionPublisherException(errors, ex)));
609+
}
610+
531611
}
532612

533613

0 commit comments

Comments
 (0)