23
23
import java .util .Collections ;
24
24
import java .util .HashMap ;
25
25
import java .util .LinkedHashSet ;
26
+ import java .util .List ;
26
27
import java .util .Map ;
27
28
import java .util .Set ;
28
29
import java .util .concurrent .Callable ;
29
30
import java .util .concurrent .Executor ;
30
31
import java .util .function .Consumer ;
31
32
import java .util .stream .Collectors ;
32
33
34
+ import graphql .execution .DataFetcherResult ;
33
35
import graphql .schema .DataFetcher ;
34
36
import graphql .schema .DataFetchingEnvironment ;
35
37
import graphql .schema .FieldCoordinates ;
38
40
import org .apache .commons .logging .Log ;
39
41
import org .apache .commons .logging .LogFactory ;
40
42
import org .dataloader .DataLoader ;
43
+ import org .reactivestreams .Publisher ;
41
44
import reactor .core .publisher .Flux ;
42
45
import reactor .core .publisher .Mono ;
43
46
62
65
import org .springframework .graphql .data .method .annotation .BatchMapping ;
63
66
import org .springframework .graphql .data .method .annotation .SchemaMapping ;
64
67
import org .springframework .graphql .execution .BatchLoaderRegistry ;
68
+ import org .springframework .graphql .execution .DataFetcherExceptionResolver ;
65
69
import org .springframework .graphql .execution .RuntimeWiringConfigurer ;
70
+ import org .springframework .graphql .execution .SubscriptionPublisherException ;
66
71
import org .springframework .lang .Nullable ;
67
72
import org .springframework .stereotype .Controller ;
68
73
import org .springframework .util .Assert ;
@@ -127,6 +132,9 @@ public class AnnotatedControllerConfigurer
127
132
@ Nullable
128
133
private ValidationHelper validationHelper ;
129
134
135
+ @ Nullable
136
+ private AnnotatedControllerExceptionResolver exceptionResolver ;
137
+
130
138
131
139
/**
132
140
* Add a {@code FormatterRegistrar} to customize the {@link ConversionService}
@@ -165,6 +173,25 @@ public void setApplicationContext(ApplicationContext applicationContext) {
165
173
this .applicationContext = applicationContext ;
166
174
}
167
175
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
+
168
195
@ Nullable
169
196
HandlerMethodArgumentResolverComposite getArgumentResolvers () {
170
197
return this .argumentResolvers ;
@@ -175,6 +202,11 @@ public void afterPropertiesSet() {
175
202
176
203
this .argumentResolvers = initArgumentResolvers ();
177
204
205
+ this .exceptionResolver = new AnnotatedControllerExceptionResolver (this .argumentResolvers );
206
+ if (this .applicationContext != null ) {
207
+ this .exceptionResolver .registerControllerAdvice (this .applicationContext );
208
+ }
209
+
178
210
if (beanValidationPresent ) {
179
211
this .validationHelper = ValidationHelper .createIfValidatorPresent (obtainApplicationContext ());
180
212
}
@@ -222,12 +254,13 @@ protected final ApplicationContext obtainApplicationContext() {
222
254
@ Override
223
255
public void configure (RuntimeWiring .Builder runtimeWiringBuilder ) {
224
256
Assert .state (this .argumentResolvers != null , "`argumentResolvers` is not initialized" );
257
+ Assert .state (this .exceptionResolver != null , "`exceptionResolver` is not initialized" );
225
258
226
259
findHandlerMethods ().forEach ((info ) -> {
227
260
DataFetcher <?> dataFetcher ;
228
261
if (!info .isBatchMapping ()) {
229
262
dataFetcher = new SchemaMappingDataFetcher (
230
- info , this .argumentResolvers , this .validationHelper , this .executor );
263
+ info , this .argumentResolvers , this .validationHelper , this .exceptionResolver , this . executor );
231
264
}
232
265
else {
233
266
String dataLoaderKey = registerBatchLoader (info );
@@ -493,19 +526,30 @@ static class SchemaMappingDataFetcher implements DataFetcher<Object> {
493
526
@ Nullable
494
527
private final Consumer <Object []> methodValidationHelper ;
495
528
529
+ private final AnnotatedControllerExceptionResolver exceptionResolver ;
530
+
496
531
@ Nullable
497
532
private final Executor executor ;
498
533
499
534
private final boolean subscription ;
500
535
501
536
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 ) {
504
540
505
541
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
+
509
553
this .executor = executor ;
510
554
this .subscription = this .info .getCoordinates ().getTypeName ().equalsIgnoreCase ("Subscription" );
511
555
}
@@ -517,17 +561,53 @@ public HandlerMethod getHandlerMethod() {
517
561
return this .info .getHandlerMethod ();
518
562
}
519
563
520
-
521
564
@ Override
522
- @ SuppressWarnings ("ConstantConditions" )
565
+ @ SuppressWarnings ({ "ConstantConditions" , "ReactiveStreamsUnusedPublisher" } )
523
566
public Object get (DataFetchingEnvironment environment ) throws Exception {
524
567
525
568
DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod (
526
569
getHandlerMethod (), this .argumentResolvers , this .methodValidationHelper ,
527
570
this .executor , this .subscription );
528
571
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 ;
530
595
}
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
+
531
611
}
532
612
533
613
0 commit comments