Configure WebInterceptors in handlers

This commit gathers `WebInterceptor` beans from the context (in an
ordered fashion) and configures them on the relevant GraphQL handlers.

Closes gh-7
This commit is contained in:
Brian Clozel
2021-02-02 13:53:53 +01:00
parent 2990df6c43
commit 5696b8cead
5 changed files with 118 additions and 15 deletions

View File

@@ -1,6 +1,6 @@
# Spring GraphQL
Experimental project to create [GraphQL](https://graphql.org/) support for Spring applications.
Experimental project for [GraphQL](https://graphql.org/) support in Spring applications with [GraphQL Java](https://github.com/graphql-java/graphql-java).
[![Build status](https://ci.spring.io/api/v1/teams/spring-graphql/pipelines/spring-graphql/jobs/build/badge)](https://ci.spring.io/teams/spring-graphql/pipelines/spring-graphql)
@@ -27,7 +27,7 @@ dependencies {
repositories {
mavenCentral()
// don't forget to add spring milestone and snapshot repositories
// don't forget to add spring milestone or snapshot repositories
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
@@ -55,6 +55,7 @@ repositories {
<!-- ... -->
</dependencies>
<!-- Don't forget to add spring milestone or snapshot repositories -->
<repositories>
<repository>
<id>spring-milestones</id>
@@ -85,7 +86,7 @@ type Person {
}
```
And then you should configure the data fetching using a `RuntimeWiringCustomizer` and custom components like
Then you should configure the data fetching process using a `RuntimeWiringCustomizer` and custom components like
Spring Data repositories, `WebClient` instances for Web APIs, a `@Service` bean, etc.
```java
@@ -127,6 +128,12 @@ management.metrics.graphql.autotime.enabled=true
You can contribute `RuntimeWiringCustomizer` beans to the context in order to configure the runtime wiring of your GraphQL application.
### Extension points
You can contribute [`WebInterceptor` beans](https://github.com/spring-projects-experimental/spring-graphql/blob/master/spring-graphql-web/src/main/java/org/springframework/graphql/WebInterceptor.java)
to the application context, so as to customize the `ExecutionInput` or the `ExecutionResult` of the query.
A custom `WebInterceptor` can, for example, change the HTTP request/response headers.
### Metrics
If the `spring-boot-starter-actuator` dependency is on the classpath, metrics will be collected for GraphQL queries.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2020 the original author or authors.
* Copyright 2020-2021 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.
@@ -16,9 +16,11 @@
package org.springframework.graphql.boot;
import java.util.Collections;
import java.util.stream.Collectors;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -28,6 +30,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.graphql.WebInterceptor;
import org.springframework.graphql.webflux.GraphQLHttpHandler;
import org.springframework.graphql.webflux.GraphQLWebSocketHandler;
import org.springframework.http.MediaType;
@@ -50,17 +53,18 @@ public class WebFluxGraphQLAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQLHttpHandler graphQLHandler(GraphQL.Builder graphQLBuilder) {
return new GraphQLHttpHandler(graphQLBuilder.build(), Collections.emptyList());
public GraphQLHttpHandler graphQLHandler(GraphQL.Builder graphQLBuilder, ObjectProvider<WebInterceptor> interceptors) {
return new GraphQLHttpHandler(graphQLBuilder.build(), interceptors.orderedStream().collect(Collectors.toList()));
}
@Bean
@ConditionalOnMissingBean
public GraphQLWebSocketHandler graphQLWebSocketHandler(
GraphQL.Builder graphQLBuilder, GraphQLProperties properties, ServerCodecConfigurer configurer) {
GraphQL.Builder graphQLBuilder, GraphQLProperties properties, ServerCodecConfigurer configurer,
ObjectProvider<WebInterceptor> interceptors) {
return new GraphQLWebSocketHandler(
graphQLBuilder.build(), Collections.emptyList(),
graphQLBuilder.build(), interceptors.orderedStream().collect(Collectors.toList()),
configurer, properties.getConnectionInitTimeoutDuration()
);
}

View File

@@ -17,11 +17,13 @@ package org.springframework.graphql.boot;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import javax.websocket.server.ServerContainer;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -32,6 +34,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.graphql.WebInterceptor;
import org.springframework.graphql.webmvc.GraphQLHttpHandler;
import org.springframework.graphql.webmvc.GraphQLWebSocketHandler;
import org.springframework.http.MediaType;
@@ -57,8 +60,9 @@ public class WebMvcGraphQLAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQLHttpHandler graphQLHandler(GraphQL.Builder graphQLBuilder) {
return new GraphQLHttpHandler(graphQLBuilder.build(), Collections.emptyList());
public GraphQLHttpHandler graphQLHandler(GraphQL.Builder graphQLBuilder,
ObjectProvider<WebInterceptor> interceptors) {
return new GraphQLHttpHandler(graphQLBuilder.build(), interceptors.orderedStream().collect(Collectors.toList()));
}
@Bean
@@ -81,7 +85,8 @@ public class WebMvcGraphQLAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQLWebSocketHandler graphQLWebSocketHandler(
GraphQL.Builder graphQLBuilder, GraphQLProperties properties, HttpMessageConverters converters) {
GraphQL.Builder graphQLBuilder, GraphQLProperties properties, HttpMessageConverters converters,
ObjectProvider<WebInterceptor> interceptors) {
HttpMessageConverter<?> converter = converters.getConverters().stream()
.filter(candidate -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON))
@@ -89,7 +94,7 @@ public class WebMvcGraphQLAutoConfiguration {
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
return new GraphQLWebSocketHandler(
graphQLBuilder.build(), Collections.emptyList(),
graphQLBuilder.build(), interceptors.orderedStream().collect(Collectors.toList()),
converter, properties.getConnectionInitTimeoutDuration()
);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -19,6 +19,7 @@ import java.util.Collections;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
@@ -30,6 +31,8 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContex
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.WebInterceptor;
import org.springframework.graphql.WebOutput;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
@@ -75,6 +78,26 @@ class WebFluxApplicationContextTests {
testWithWebClient(client -> client.post().uri("").bodyValue(":)").exchange().expectStatus().isBadRequest());
}
@Test
void interceptedQuery() {
testWithWebClient(client -> {
String query = "{" +
" bookById(id: \\\"book-1\\\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
client.post().uri("")
.bodyValue("{ \"query\": \"" + query + "\"}")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals("X-Custom-Header", "42");
});
}
private void testWithWebClient(Consumer<WebTestClient> consumer) {
testWithApplicationContext(context -> {
WebTestClient client = WebTestClient.bindToApplicationContext(context)
@@ -92,7 +115,7 @@ class WebFluxApplicationContextTests {
private void testWithApplicationContext(ContextConsumer<ApplicationContext> consumer) {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AUTO_CONFIGURATIONS)
.withUserConfiguration(DataFetchersConfiguration.class)
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues(
"spring.main.web-application-type=reactive",
"spring.graphql.schema-location:classpath:books/schema.graphqls")
@@ -111,4 +134,18 @@ class WebFluxApplicationContextTests {
}
}
@Configuration(proxyBeanMethods = false)
static class CustomWebInterceptor {
@Bean
public WebInterceptor customWebInterceptor() {
return new WebInterceptor() {
@Override
public Mono<WebOutput> postHandle(WebOutput webOutput) {
return Mono.just(webOutput.transform(output -> output.header("X-Custom-Header", "42")));
}
};
}
}
}

View File

@@ -1,7 +1,24 @@
/*
* Copyright 2020-2021 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
@@ -11,6 +28,8 @@ import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguratio
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.WebInterceptor;
import org.springframework.graphql.WebOutput;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
@@ -60,11 +79,28 @@ class WebMvcApplicationContextTests {
testWith(mockMvc -> mockMvc.perform(post("/graphql").content(":)")).andExpect(status().isBadRequest()));
}
@Test
void interceptedQuery() {
testWith(mockMvc -> {
String query = "{" +
" bookById(id: \\\"book-1\\\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
MvcResult asyncResult = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn();
mockMvc.perform(asyncDispatch(asyncResult))
.andExpect(status().isOk())
.andExpect(header().string("X-Custom-Header", "42"));
});
}
private void testWith(MockMvcConsumer mockMvcConsumer) {
new WebApplicationContextRunner()
.withConfiguration(AUTO_CONFIGURATIONS)
.withUserConfiguration(DataFetchersConfiguration.class)
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues(
"spring.main.web-application-type=servlet",
"spring.graphql.schema-location:classpath:books/schema.graphqls")
@@ -94,4 +130,18 @@ class WebMvcApplicationContextTests {
}
}
@Configuration(proxyBeanMethods = false)
static class CustomWebInterceptor {
@Bean
public WebInterceptor customWebInterceptor() {
return new WebInterceptor() {
@Override
public Mono<WebOutput> postHandle(WebOutput webOutput) {
return Mono.just(webOutput.transform(output -> output.header("X-Custom-Header", "42")));
}
};
}
}
}