Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit aa5d38a

Browse files
authored
Merge pull request #163 from graphql-java-kickstart/feature/162-exception-handler
Feature/162 exception handler
2 parents a3b5d1d + 7c0f8c5 commit aa5d38a

File tree

20 files changed

+470
-18
lines changed

20 files changed

+470
-18
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,11 @@ Available Spring Boot configuration parameters (either `application.yml` or `app
118118
```yaml
119119
graphql:
120120
servlet:
121-
mapping: /graphql
122-
enabled: true
123-
corsEnabled: true
121+
mapping: /graphql
122+
enabled: true
123+
corsEnabled: true
124+
# if you want to @ExceptionHandler annotation for custom GraphQLErrors
125+
exception-handlers-enabled: true
124126
```
125127
126128
By default a global CORS filter is enabled for `/graphql/**` context.

gradle.properties

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1818
#
1919

20-
version = 5.2.1-SNAPSHOT
20+
version = 5.3-SNAPSHOT
2121
PROJECT_GROUP = com.graphql-java-kickstart
2222
PROJECT_NAME = graphql-spring-boot
2323
PROJECT_DESC = GraphQL Spring Framework Boot
@@ -39,10 +39,11 @@ TARGET_COMPATIBILITY = 1.8
3939
LIB_GRAPHQL_JAVA_VER = 11.0
4040
LIB_JUNIT_VER = 4.12
4141
LIB_SPRING_CORE_VER = 5.0.4.RELEASE
42-
LIB_SPRING_BOOT_VER = 2.0.5.RELEASE
43-
LIB_GRAPHQL_SERVLET_VER = 7.0.0
42+
LIB_SPRING_BOOT_VER = 2.1.0.RELEASE
43+
LIB_GRAPHQL_SERVLET_VER = 7.1.0
4444
LIB_GRAPHQL_JAVA_TOOLS_VER = 5.4.0
4545
LIB_COMMONS_IO_VER = 2.6
46+
kotlin.version=1.3.10
4647

4748
GRADLE_WRAPPER_VER = 4.10.2
4849

graphql-spring-boot-autoconfigure/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
dependencies {
2121
compileOnly "org.springframework.boot:spring-boot-configuration-processor:$LIB_SPRING_BOOT_VER"
22-
22+
2323
compile "org.springframework.boot:spring-boot-autoconfigure:$LIB_SPRING_BOOT_VER"
2424
compile "org.springframework.boot:spring-boot-starter-websocket:$LIB_SPRING_BOOT_VER"
2525
compile "com.graphql-java-kickstart:graphql-java-servlet:$LIB_GRAPHQL_SERVLET_VER"
@@ -31,6 +31,7 @@ dependencies {
3131
testCompile "com.graphql-java:graphql-java:$LIB_GRAPHQL_JAVA_VER"
3232
testCompile "org.springframework.boot:spring-boot-starter-web:$LIB_SPRING_BOOT_VER"
3333
testCompile "org.springframework.boot:spring-boot-starter-test:$LIB_SPRING_BOOT_VER"
34+
testCompile(project(":graphql-spring-boot-test"))
3435
}
3536

3637
compileJava.dependsOn(processResources)

graphql-spring-boot-autoconfigure/src/main/java/com/oembedler/moon/graphql/boot/GraphQLJavaToolsAutoConfiguration.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import com.fasterxml.jackson.databind.ObjectMapper;
55
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
66
import com.fasterxml.jackson.module.kotlin.KotlinModule;
7+
import com.oembedler.moon.graphql.boot.error.GraphQLErrorHandlerFactory;
78
import graphql.schema.GraphQLScalarType;
89
import graphql.schema.GraphQLSchema;
9-
import graphql.schema.idl.SchemaDirectiveWiring;
10+
import graphql.servlet.GraphQLErrorHandler;
1011
import graphql.servlet.GraphQLSchemaProvider;
1112
import org.springframework.beans.factory.annotation.Autowired;
1213
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@@ -22,8 +23,6 @@
2223
import java.io.IOException;
2324
import java.util.List;
2425

25-
import static com.coxautodev.graphql.tools.SchemaParserOptions.newOptions;
26-
2726
/**
2827
* @author Andrew Potter
2928
*/

graphql-spring-boot-autoconfigure/src/main/java/com/oembedler/moon/graphql/boot/GraphQLServletProperties.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class GraphQLServletProperties {
3434

3535
private boolean asyncModeEnabled = false;
3636

37+
private boolean exceptionHandlersEnabled = false;
38+
3739
public String getMapping() {
3840
return mapping != null ? mapping : "/graphql";
3941
}
@@ -85,4 +87,12 @@ public boolean isAsyncModeEnabled() {
8587
public void setAsyncModeEnabled(boolean asyncModeEnabled) {
8688
this.asyncModeEnabled = asyncModeEnabled;
8789
}
90+
91+
public boolean isExceptionHandlersEnabled() {
92+
return exceptionHandlersEnabled;
93+
}
94+
95+
public void setExceptionHandlersEnabled(boolean exceptionHandlersEnabled) {
96+
this.exceptionHandlersEnabled = exceptionHandlersEnabled;
97+
}
8898
}

graphql-spring-boot-autoconfigure/src/main/java/com/oembedler/moon/graphql/boot/GraphQLWebAutoConfiguration.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import com.fasterxml.jackson.databind.InjectableValues;
2323
import com.fasterxml.jackson.databind.ObjectMapper;
24+
import com.oembedler.moon.graphql.boot.error.GraphQLErrorHandlerFactory;
2425
import graphql.execution.AsyncExecutionStrategy;
2526
import graphql.execution.ExecutionStrategy;
2627
import graphql.execution.SubscriptionExecutionStrategy;
@@ -29,12 +30,16 @@
2930
import graphql.execution.preparsed.PreparsedDocumentProvider;
3031
import graphql.schema.GraphQLSchema;
3132
import graphql.servlet.*;
33+
import org.springframework.beans.BeansException;
3234
import org.springframework.beans.factory.annotation.Autowired;
3335
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
3436
import org.springframework.boot.autoconfigure.condition.*;
3537
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
3638
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3739
import org.springframework.boot.web.servlet.ServletRegistrationBean;
40+
import org.springframework.context.ApplicationContext;
41+
import org.springframework.context.ApplicationContextAware;
42+
import org.springframework.context.ConfigurableApplicationContext;
3843
import org.springframework.context.annotation.Bean;
3944
import org.springframework.context.annotation.Configuration;
4045
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@@ -60,7 +65,7 @@
6065
@ConditionalOnProperty(value = "graphql.servlet.enabled", havingValue = "true", matchIfMissing = true)
6166
@AutoConfigureAfter({GraphQLJavaToolsAutoConfiguration.class, JacksonAutoConfiguration.class})
6267
@EnableConfigurationProperties({GraphQLServletProperties.class})
63-
public class GraphQLWebAutoConfiguration {
68+
public class GraphQLWebAutoConfiguration implements ApplicationContextAware {
6469

6570
public static final String QUERY_EXECUTION_STRATEGY = "queryExecutionStrategy";
6671
public static final String MUTATION_EXECUTION_STRATEGY = "mutationExecutionStrategy";
@@ -96,6 +101,15 @@ public class GraphQLWebAutoConfiguration {
96101
@Autowired(required = false)
97102
private MultipartConfigElement multipartConfigElement;
98103

104+
@Override
105+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
106+
if (!applicationContext.containsBean(GraphQLErrorHandler.class.getSimpleName())) {
107+
ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
108+
errorHandler = new GraphQLErrorHandlerFactory().create(context, graphQLServletProperties.isExceptionHandlersEnabled());
109+
context.getBeanFactory().registerSingleton(errorHandler.getClass().getCanonicalName(), errorHandler);
110+
}
111+
}
112+
99113
@Bean
100114
@ConditionalOnClass(CorsFilter.class)
101115
@ConditionalOnProperty(value = "graphql.servlet.corsEnabled", havingValue = "true", matchIfMissing = true)
@@ -236,5 +250,4 @@ public ServletRegistrationBean<AbstractGraphQLHttpServlet> graphQLServletRegistr
236250
private MultipartConfigElement multipartConfigElement() {
237251
return Optional.ofNullable(multipartConfigElement).orElse(new MultipartConfigElement(""));
238252
}
239-
240253
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.oembedler.moon.graphql.boot.error;
2+
3+
import graphql.GraphQLError;
4+
5+
import java.lang.reflect.Method;
6+
import java.util.Optional;
7+
8+
interface GraphQLErrorFactory {
9+
10+
Optional<Class<? extends Throwable>> mostConcrete(Throwable t);
11+
12+
GraphQLError create(Throwable t);
13+
14+
static GraphQLErrorFactory withReflection(Object object, Method method) {
15+
return new ReflectiveGraphQLErrorFactory(object, method);
16+
}
17+
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.oembedler.moon.graphql.boot.error;
2+
3+
import graphql.ErrorType;
4+
import graphql.ExceptionWhileDataFetching;
5+
import graphql.GraphQLError;
6+
import graphql.SerializationError;
7+
import graphql.servlet.DefaultGraphQLErrorHandler;
8+
import graphql.servlet.GenericGraphQLError;
9+
10+
import java.util.HashMap;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.Optional;
14+
import java.util.stream.Collectors;
15+
16+
class GraphQLErrorFromExceptionHandler extends DefaultGraphQLErrorHandler {
17+
18+
private List<GraphQLErrorFactory> factories;
19+
20+
GraphQLErrorFromExceptionHandler(List<GraphQLErrorFactory> factories) {
21+
this.factories = factories;
22+
}
23+
24+
protected List<GraphQLError> filterGraphQLErrors(List<GraphQLError> errors) {
25+
return errors.stream().map(this::transform).collect(Collectors.toList());
26+
}
27+
28+
private GraphQLError transform(GraphQLError error) {
29+
if (error.getErrorType() == ErrorType.DataFetchingException) {
30+
return extractException(error).map(this::transform).orElse(defaultError(error.getMessage()));
31+
}
32+
33+
return defaultError(error.getMessage());
34+
}
35+
36+
private Optional<Throwable> extractException(GraphQLError error) {
37+
if (error instanceof ExceptionWhileDataFetching) {
38+
return Optional.of(((ExceptionWhileDataFetching) error).getException());
39+
} else if (error instanceof SerializationError) {
40+
return Optional.of(((SerializationError) error).getException());
41+
}
42+
return Optional.empty();
43+
}
44+
45+
private GraphQLError transform(Throwable throwable) {
46+
Map<Class<? extends Throwable>, GraphQLErrorFactory> applicables = new HashMap<>();
47+
factories.forEach(factory -> factory.mostConcrete(throwable).ifPresent(t -> applicables.put(t, factory)));
48+
return applicables.keySet().stream()
49+
.min(new ThrowableComparator())
50+
.map(applicables::get)
51+
.map(factory -> factory.create(throwable))
52+
.orElse(this.defaultError(throwable));
53+
}
54+
55+
private GraphQLError defaultError(Throwable throwable) {
56+
return new ThrowableGraphQLError(throwable);
57+
}
58+
59+
private GraphQLError defaultError(String message) {
60+
return new GenericGraphQLError(message);
61+
}
62+
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.oembedler.moon.graphql.boot.error;
2+
3+
import graphql.servlet.DefaultGraphQLErrorHandler;
4+
import graphql.servlet.GraphQLErrorHandler;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.beans.factory.config.BeanDefinition;
7+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
8+
import org.springframework.context.ApplicationContext;
9+
import org.springframework.context.ConfigurableApplicationContext;
10+
import org.springframework.web.bind.annotation.ExceptionHandler;
11+
12+
import java.util.Arrays;
13+
import java.util.Collections;
14+
import java.util.List;
15+
import java.util.Objects;
16+
import java.util.stream.Collectors;
17+
18+
@Slf4j
19+
public class GraphQLErrorHandlerFactory {
20+
21+
public GraphQLErrorHandler create(ConfigurableApplicationContext applicationContext, boolean exceptionHandlersEnabled) {
22+
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
23+
List<GraphQLErrorFactory> factories = Arrays.stream(beanFactory.getBeanDefinitionNames())
24+
.map(beanFactory::getBeanDefinition)
25+
.map(BeanDefinition::getBeanClassName)
26+
.filter(Objects::nonNull)
27+
.map(name -> scanForExceptionHandlers(applicationContext, beanFactory, name))
28+
.flatMap(List::stream)
29+
.collect(Collectors.toList());
30+
31+
if (!factories.isEmpty() || exceptionHandlersEnabled) {
32+
return new GraphQLErrorFromExceptionHandler(factories);
33+
}
34+
35+
return new DefaultGraphQLErrorHandler();
36+
}
37+
38+
private List<GraphQLErrorFactory> scanForExceptionHandlers(ApplicationContext context, ConfigurableListableBeanFactory beanFactory, String className) {
39+
try {
40+
Class<?> objClz = beanFactory.getBeanClassLoader().loadClass(className);
41+
// todo: need to handle proxies?
42+
return Arrays.stream(objClz.getDeclaredMethods())
43+
.filter(method -> method.isAnnotationPresent(ExceptionHandler.class))
44+
.map(method -> GraphQLErrorFactory.withReflection(context.getBean(className), method))
45+
.collect(Collectors.toList());
46+
} catch (ClassNotFoundException e) {
47+
log.error("Cannot load class " + className + ". " + e.getMessage());
48+
return Collections.emptyList();
49+
}
50+
}
51+
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.oembedler.moon.graphql.boot.error;
2+
3+
import graphql.GraphQLError;
4+
import graphql.servlet.GenericGraphQLError;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.web.bind.annotation.ExceptionHandler;
7+
8+
import java.lang.reflect.InvocationTargetException;
9+
import java.lang.reflect.Method;
10+
import java.util.Optional;
11+
12+
@Slf4j
13+
class ReflectiveGraphQLErrorFactory implements GraphQLErrorFactory {
14+
15+
private Object object;
16+
private Method method;
17+
private Throwables throwables;
18+
19+
ReflectiveGraphQLErrorFactory(Object object, Method method) {
20+
this.object = object;
21+
this.method = method;
22+
23+
throwables = new Throwables(method.getAnnotation(ExceptionHandler.class).value());
24+
}
25+
26+
@Override
27+
public Optional<Class<? extends Throwable>> mostConcrete(Throwable t) {
28+
return throwables.mostConcrete(t);
29+
}
30+
31+
@Override
32+
public GraphQLError create(Throwable t) {
33+
try {
34+
method.setAccessible(true);
35+
return (GraphQLError) method.invoke(object, t);
36+
} catch (IllegalAccessException | InvocationTargetException e) {
37+
log.error("Cannot create GraphQLError from throwable {}", t.getClass().getSimpleName(), e);
38+
return new GenericGraphQLError(t.getMessage());
39+
}
40+
}
41+
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.oembedler.moon.graphql.boot.error;
2+
3+
import java.util.Comparator;
4+
5+
class ThrowableComparator implements Comparator<Class<? extends Throwable>> {
6+
7+
@Override
8+
public int compare(Class<? extends Throwable> t1, Class<? extends Throwable> t2) {
9+
if (t1 == t2) {
10+
return 0;
11+
}
12+
return t1.isAssignableFrom(t2) ? 1 : -1;
13+
}
14+
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.oembedler.moon.graphql.boot.error;
2+
3+
import graphql.servlet.GenericGraphQLError;
4+
5+
import java.util.Objects;
6+
7+
public class ThrowableGraphQLError extends GenericGraphQLError {
8+
9+
private final Throwable throwable;
10+
11+
public ThrowableGraphQLError(Throwable throwable) {
12+
this(throwable, throwable.getMessage());
13+
}
14+
15+
public ThrowableGraphQLError(Throwable throwable, String message) {
16+
super(message);
17+
18+
this.throwable = throwable;
19+
}
20+
21+
public String getType() {
22+
return throwable.getClass().getSimpleName();
23+
}
24+
25+
@Override
26+
public final boolean equals(Object o) {
27+
if (this == o) return true;
28+
if (!(o instanceof ThrowableGraphQLError)) return false;
29+
ThrowableGraphQLError that = (ThrowableGraphQLError) o;
30+
return Objects.equals(throwable, that.throwable) && Objects.equals(getMessage(), that.getMessage());
31+
}
32+
33+
@Override
34+
public final int hashCode() {
35+
return Objects.hash(throwable, getMessage());
36+
}
37+
38+
}

0 commit comments

Comments
 (0)