Skip to content

Commit

Permalink
Configure WebInterceptors in handlers
Browse files Browse the repository at this point in the history
This commit gathers `WebInterceptor` beans from the context (in an
ordered fashion) and configures them on the relevant GraphQL handlers.

Closes spring-iogh-7
  • Loading branch information
bclozel committed Feb 2, 2021
1 parent 2990df6 commit 5696b8c
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 15 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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' }
}
Expand Down Expand Up @@ -55,6 +55,7 @@ repositories {
<!-- ... -->
</dependencies>

<!-- Don't forget to add spring milestone or snapshot repositories -->
<repositories>
<repository>
<id>spring-milestones</id>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -28,6 +30,7 @@
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;
Expand All @@ -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()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@

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;
Expand All @@ -32,6 +34,7 @@
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;
Expand All @@ -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
Expand All @@ -81,15 +85,16 @@ static class WebSocketConfiguration {
@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))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No JSON converter"));

return new GraphQLWebSocketHandler(
graphQLBuilder.build(), Collections.emptyList(),
graphQLBuilder.build(), interceptors.orderedStream().collect(Collectors.toList()),
converter, properties.getConnectionInitTimeoutDuration()
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -19,6 +19,7 @@
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;
Expand All @@ -30,6 +31,8 @@
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;

Expand Down Expand Up @@ -75,6 +78,26 @@ void queryIsInvalidJson() {
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)
Expand All @@ -92,7 +115,7 @@ private void testWithWebClient(Consumer<WebTestClient> consumer) {
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")
Expand All @@ -111,4 +134,18 @@ public RuntimeWiringCustomizer bookDataFetcher() {
}
}

@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")));
}
};
}
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,6 +28,8 @@
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;
Expand Down Expand Up @@ -60,11 +79,28 @@ void invalidJson() {
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")
Expand Down Expand Up @@ -94,4 +130,18 @@ public RuntimeWiringCustomizer bookDataFetcher() {
}
}

@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")));
}
};
}
}

}

0 comments on commit 5696b8c

Please sign in to comment.