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:
13
README.md
13
README.md
@@ -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).
|
||||
|
||||
[](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.
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user