Skip to content

Commit 038217a

Browse files
committed
Move AnnotatedControllerExceptionResolver to base class
See gh-864
1 parent 9aa506a commit 038217a

File tree

4 files changed

+112
-59
lines changed

4 files changed

+112
-59
lines changed

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

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
import org.springframework.graphql.data.pagination.CursorStrategy;
5959
import org.springframework.graphql.data.query.SortStrategy;
6060
import org.springframework.graphql.execution.BatchLoaderRegistry;
61-
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
6261
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
6362
import org.springframework.graphql.execution.SelfDescribingDataFetcher;
6463
import org.springframework.graphql.execution.SubscriptionPublisherException;
@@ -115,9 +114,6 @@ public class AnnotatedControllerConfigurer
115114
@Nullable
116115
private ValidationHelper validationHelper;
117116

118-
@Nullable
119-
private AnnotatedControllerExceptionResolver exceptionResolver;
120-
121117

122118
/**
123119
* Add a {@link HandlerMethodArgumentResolver} for custom controller method
@@ -131,26 +127,6 @@ public void addCustomArgumentResolver(HandlerMethodArgumentResolver resolver) {
131127
this.customArgumentResolvers.add(resolver);
132128
}
133129

134-
/**
135-
* Return a {@link DataFetcherExceptionResolver} that resolves exceptions with
136-
* {@code @GraphQlExceptionHandler} methods in {@code @ControllerAdvice}
137-
* classes declared in Spring configuration. This is useful primarily for
138-
* exceptions from non-controller {@link DataFetcher}s since exceptions from
139-
* {@code @SchemaMapping} controller methods are handled automatically at
140-
* the point of invocation.
141-
*
142-
* @return a resolver instance that can be plugged into
143-
* {@link org.springframework.graphql.execution.GraphQlSource.Builder#exceptionResolvers(List)
144-
* GraphQlSource.Builder}
145-
*
146-
* @since 1.2.0
147-
*/
148-
public DataFetcherExceptionResolver getExceptionResolver() {
149-
Assert.notNull(this.exceptionResolver,
150-
"DataFetcherExceptionResolver is not yet initialized, was afterPropertiesSet called?");
151-
return (ex, env) -> this.exceptionResolver.resolveException(ex, env, null);
152-
}
153-
154130
/**
155131
* Configure an initializer that configures the {@link DataBinder} before the binding process.
156132
* @param consumer the data binder initializer
@@ -167,11 +143,6 @@ public void setDataBinderInitializer(@Nullable Consumer<DataBinder> consumer) {
167143
public void afterPropertiesSet() {
168144
super.afterPropertiesSet();
169145

170-
this.exceptionResolver = new AnnotatedControllerExceptionResolver(getArgumentResolvers());
171-
if (getApplicationContext() != null) {
172-
this.exceptionResolver.registerControllerAdvice(getApplicationContext());
173-
}
174-
175146
if (beanValidationPresent) {
176147
this.validationHelper = ValidationHelper.createIfValidatorPresent(obtainApplicationContext());
177148
}
@@ -251,13 +222,11 @@ private void addSortMethodArgumentResolver(HandlerMethodArgumentResolverComposit
251222

252223
@Override
253224
public void configure(RuntimeWiring.Builder runtimeWiringBuilder) {
254-
Assert.state(this.exceptionResolver != null, "`exceptionResolver` is not initialized");
255-
256225
detectHandlerMethods().forEach(info -> {
257226
DataFetcher<?> dataFetcher;
258227
if (!info.isBatchMapping()) {
259228
dataFetcher = new SchemaMappingDataFetcher(
260-
info, getArgumentResolvers(), this.validationHelper, this.exceptionResolver, getExecutor());
229+
info, getArgumentResolvers(), this.validationHelper, getExceptionResolver(), getExecutor());
261230
}
262231
else {
263232
dataFetcher = registerBatchLoader(info);
@@ -415,7 +384,7 @@ static class SchemaMappingDataFetcher implements SelfDescribingDataFetcher<Objec
415384
@Nullable
416385
private final BiConsumer<Object, Object[]> methodValidationHelper;
417386

418-
private final AnnotatedControllerExceptionResolver exceptionResolver;
387+
private final HandlerDataFetcherExceptionResolver exceptionResolver;
419388

420389
@Nullable
421390
private final Executor executor;
@@ -424,7 +393,7 @@ static class SchemaMappingDataFetcher implements SelfDescribingDataFetcher<Objec
424393

425394
SchemaMappingDataFetcher(
426395
DataFetcherMappingInfo info, HandlerMethodArgumentResolverComposite argumentResolvers,
427-
@Nullable ValidationHelper helper, AnnotatedControllerExceptionResolver exceptionResolver,
396+
@Nullable ValidationHelper helper, HandlerDataFetcherExceptionResolver exceptionResolver,
428397
@Nullable Executor executor) {
429398

430399
this.mappingInfo = info;
@@ -433,10 +402,6 @@ static class SchemaMappingDataFetcher implements SelfDescribingDataFetcher<Objec
433402
this.methodValidationHelper =
434403
(helper != null ? helper.getValidationHelperFor(info.getHandlerMethod()) : null);
435404

436-
// Register controllers early to validate exception handler return types
437-
Class<?> controllerType = info.getHandlerMethod().getBeanType();
438-
exceptionResolver.registerController(controllerType);
439-
440405
this.exceptionResolver = exceptionResolver;
441406

442407
this.executor = executor;

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

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
import java.util.Collection;
2222
import java.util.Collections;
2323
import java.util.LinkedHashSet;
24+
import java.util.List;
2425
import java.util.Map;
2526
import java.util.Set;
2627
import java.util.concurrent.Callable;
2728
import java.util.concurrent.Executor;
2829
import java.util.stream.Collectors;
2930

31+
import graphql.schema.DataFetcher;
3032
import org.apache.commons.logging.Log;
3133
import org.apache.commons.logging.LogFactory;
3234

@@ -42,6 +44,7 @@
4244
import org.springframework.format.support.FormattingConversionService;
4345
import org.springframework.graphql.data.method.HandlerMethod;
4446
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
47+
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
4548
import org.springframework.lang.Nullable;
4649
import org.springframework.stereotype.Controller;
4750
import org.springframework.util.Assert;
@@ -81,6 +84,9 @@ public abstract class AnnotatedControllerDetectionSupport<M> implements Applicat
8184

8285
private boolean fallBackOnDirectFieldAccess;
8386

87+
@Nullable
88+
private AnnotatedControllerExceptionResolver exceptionResolver;
89+
8490
@Nullable
8591
private Executor executor;
8692

@@ -120,6 +126,25 @@ protected boolean isFallBackOnDirectFieldAccess() {
120126
return this.fallBackOnDirectFieldAccess;
121127
}
122128

129+
/**
130+
* Return a {@link DataFetcherExceptionResolver} that resolves exceptions with
131+
* {@code @GraphQlExceptionHandler} methods in {@code @ControllerAdvice}
132+
* classes declared in Spring configuration. This is useful primarily for
133+
* exceptions from non-controller {@link DataFetcher}s since exceptions from
134+
* {@code @SchemaMapping} controller methods are handled automatically at
135+
* the point of invocation.
136+
*
137+
* @return a resolver instance that can be plugged into
138+
* {@link org.springframework.graphql.execution.GraphQlSource.Builder#exceptionResolvers(List)
139+
* GraphQlSource.Builder}
140+
*
141+
* @since 1.2.0
142+
*/
143+
public HandlerDataFetcherExceptionResolver getExceptionResolver() {
144+
Assert.notNull(this.exceptionResolver, "afterPropertiesSet not called yet");
145+
return this.exceptionResolver;
146+
}
147+
123148
/**
124149
* Configure an {@link Executor} to use for asynchronous handling of
125150
* {@link Callable} return values from controller methods.
@@ -140,7 +165,7 @@ public Executor getExecutor() {
140165
* Return the configured argument resolvers.
141166
*/
142167
protected HandlerMethodArgumentResolverComposite getArgumentResolvers() {
143-
Assert.notNull(this.argumentResolvers, "Not yet initialized, was afterPropertiesSet called?");
168+
Assert.notNull(this.argumentResolvers, "afterPropertiesSet not called yet");
144169
return this.argumentResolvers;
145170
}
146171

@@ -163,6 +188,11 @@ protected final ApplicationContext obtainApplicationContext() {
163188
@Override
164189
public void afterPropertiesSet() {
165190
this.argumentResolvers = initArgumentResolvers();
191+
192+
this.exceptionResolver = new AnnotatedControllerExceptionResolver(this.argumentResolvers);
193+
if (getApplicationContext() != null) {
194+
this.exceptionResolver.registerControllerAdvice(getApplicationContext());
195+
}
166196
}
167197

168198
protected abstract HandlerMethodArgumentResolverComposite initArgumentResolvers();
@@ -192,17 +222,7 @@ protected Set<M> detectHandlerMethods() {
192222
continue;
193223
}
194224
Class<?> beanClass = context.getType(beanName);
195-
findHandlerMethods(beanName, beanClass).forEach(info -> {
196-
HandlerMethod handlerMethod = getHandlerMethod(info);
197-
M existing = results.stream().filter(o -> o.equals(info)).findFirst().orElse(null);
198-
if (existing != null && !getHandlerMethod(existing).equals(handlerMethod)) {
199-
throw new IllegalStateException(
200-
"Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" +
201-
handlerMethod + "\n" + ": There is already '" +
202-
getHandlerMethod(existing).getBean() + "' bean method\n" + existing + " mapped.");
203-
}
204-
results.add(info);
205-
});
225+
findHandlerMethods(beanName, beanClass).forEach(info -> registerHandlerMethod(info, results));
206226
}
207227
return results;
208228
}
@@ -230,13 +250,6 @@ private Collection<M> findHandlerMethods(Object handler, @Nullable Class<?> hand
230250
@Nullable
231251
protected abstract M getMappingInfo(Method method, Object handler, Class<?> handlerType);
232252

233-
protected HandlerMethod createHandlerMethod(Method originalMethod, Object handler, Class<?> handlerType) {
234-
Method method = AopUtils.selectInvocableMethod(originalMethod, handlerType);
235-
return (handler instanceof String beanName ?
236-
new HandlerMethod(beanName, obtainApplicationContext().getAutowireCapableBeanFactory(), method) :
237-
new HandlerMethod(handler, method));
238-
}
239-
240253
private String formatMappings(Class<?> handlerType, Collection<M> infos) {
241254
String formattedType = Arrays.stream(ClassUtils.getPackageName(handlerType).split("\\."))
242255
.map(p -> p.substring(0, 1))
@@ -252,4 +265,25 @@ private String formatMappings(Class<?> handlerType, Collection<M> infos) {
252265
.collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", ""));
253266
}
254267

268+
private void registerHandlerMethod(M info, Set<M> results) {
269+
Assert.state(this.exceptionResolver != null, "afterPropertiesSet not called");
270+
HandlerMethod handlerMethod = getHandlerMethod(info);
271+
M existing = results.stream().filter(o -> o.equals(info)).findFirst().orElse(null);
272+
if (existing != null && !getHandlerMethod(existing).equals(handlerMethod)) {
273+
throw new IllegalStateException(
274+
"Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" +
275+
handlerMethod + "\n" + ": There is already '" +
276+
getHandlerMethod(existing).getBean() + "' bean method\n" + existing + " mapped.");
277+
}
278+
results.add(info);
279+
this.exceptionResolver.registerController(handlerMethod.getBeanType());
280+
}
281+
282+
protected HandlerMethod createHandlerMethod(Method originalMethod, Object handler, Class<?> handlerType) {
283+
Method method = AopUtils.selectInvocableMethod(originalMethod, handlerType);
284+
return (handler instanceof String beanName ?
285+
new HandlerMethod(beanName, obtainApplicationContext().getAutowireCapableBeanFactory(), method) :
286+
new HandlerMethod(handler, method));
287+
}
288+
255289
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
* @author Rossen Stoyanchev
7474
* @since 1.2.0
7575
*/
76-
final class AnnotatedControllerExceptionResolver {
76+
final class AnnotatedControllerExceptionResolver implements HandlerDataFetcherExceptionResolver {
7777

7878
private static final Log logger = LogFactory.getLog(AnnotatedControllerExceptionResolver.class);
7979

@@ -121,7 +121,7 @@ public void registerControllerAdvice(ApplicationContext context) {
121121
}
122122
if (logger.isDebugEnabled()) {
123123
logger.debug("@GraphQlException methods in ControllerAdvice beans: " +
124-
(this.controllerAdviceCache.size() == 0 ? "none" : this.controllerAdviceCache.size()));
124+
(this.controllerAdviceCache.isEmpty() ? "none" : this.controllerAdviceCache.size()));
125125
}
126126
}
127127

@@ -167,6 +167,7 @@ private static Map<Class<? extends Throwable>, Method> findExceptionHandlers(Cla
167167
* @return a {@code Mono} with resolved {@code GraphQLError}s as specified in
168168
* {@link DataFetcherExceptionResolver#resolveException(Throwable, DataFetchingEnvironment)}
169169
*/
170+
@Override
170171
public Mono<List<GraphQLError>> resolveException(
171172
Throwable ex, DataFetchingEnvironment environment, @Nullable Object controller) {
172173

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2002-2024 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.support;
17+
18+
import java.util.List;
19+
20+
import graphql.GraphQLError;
21+
import graphql.schema.DataFetchingEnvironment;
22+
import reactor.core.publisher.Mono;
23+
24+
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
25+
import org.springframework.lang.Nullable;
26+
27+
/**
28+
* Extension of {@link DataFetcherExceptionResolver} with overloaded method to
29+
* apply at the point of DataFetcher invocation to allow local exception handling.
30+
*
31+
* @author Rossen Stoyanchev
32+
* @since 1.3
33+
*/
34+
public interface HandlerDataFetcherExceptionResolver extends DataFetcherExceptionResolver {
35+
36+
37+
@Override
38+
default Mono<List<GraphQLError>> resolveException(Throwable exception, DataFetchingEnvironment environment) {
39+
return resolveException(exception, environment, null);
40+
}
41+
42+
/**
43+
* Resolve an exception raised by the given handler.
44+
* @param ex the exception to resolve
45+
* @param environment the environment for the invoked {@code DataFetcher}
46+
* @param handler the handler that raised the exception, if applicable
47+
* @return a {@code Mono} with resolved {@code GraphQLError}s as specified in
48+
* {@link DataFetcherExceptionResolver#resolveException(Throwable, DataFetchingEnvironment)}
49+
*/
50+
Mono<List<GraphQLError>> resolveException(
51+
Throwable ex, DataFetchingEnvironment environment, @Nullable Object handler);
52+
53+
}

0 commit comments

Comments
 (0)