Browse Source

Configure RSocket server support in GraphQL

This commit adds the RSocket server auto-configuration for GraphQL.

See gh-30453
pull/29812/head
Brian Clozel 4 years ago
parent
commit
eddb2b16ff
  1. 23
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java
  2. 73
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketAutoConfiguration.java
  3. 47
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketController.java
  4. 20
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/package-info.java
  5. 150
      spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketAutoConfigurationTests.java
  6. 34
      spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-graphql.adoc
  7. 52
      spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/graphql/transports/rsocket/RSocketGraphQlClientExample.java
  8. 71
      spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/graphql/transports/rsocket/RSocketGraphQlClientExample.kt

23
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java

@ -41,6 +41,8 @@ public class GraphQlProperties { @@ -41,6 +41,8 @@ public class GraphQlProperties {
private final Websocket websocket = new Websocket();
private final Rsocket rsocket = new Rsocket();
public Graphiql getGraphiql() {
return this.graphiql;
}
@ -61,6 +63,10 @@ public class GraphQlProperties { @@ -61,6 +63,10 @@ public class GraphQlProperties {
return this.websocket;
}
public Rsocket getRsocket() {
return this.rsocket;
}
public static class Schema {
/**
@ -204,4 +210,21 @@ public class GraphQlProperties { @@ -204,4 +210,21 @@ public class GraphQlProperties {
}
public static class Rsocket {
/**
* Mapping of the RSocket message handler.
*/
private String mapping;
public String getMapping() {
return this.mapping;
}
public void setMapping(String mapping) {
this.mapping = mapping;
}
}
}

73
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketAutoConfiguration.java

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
/*
* Copyright 2012-2022 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.boot.autoconfigure.graphql.rsocket;
import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.GraphQL;
import io.rsocket.core.RSocketServer;
import reactor.netty.http.server.HttpServer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.server.GraphQlRSocketHandler;
import org.springframework.graphql.server.RSocketGraphQlInterceptor;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
/**
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
* RSocket.
*
* @author Brian Clozel
* @since 2.7.0
*/
@AutoConfiguration(after = { GraphQlAutoConfiguration.class, RSocketMessagingAutoConfiguration.class })
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class, RSocketServer.class, HttpServer.class })
@ConditionalOnBean({ RSocketMessageHandler.class, AnnotatedControllerConfigurer.class })
@ConditionalOnProperty(prefix = "spring.graphql.rsocket", name = "mapping")
public class GraphQlRSocketAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlRSocketHandler graphQlRSocketHandler(ExecutionGraphQlService graphQlService,
ObjectProvider<RSocketGraphQlInterceptor> interceptorsProvider, ObjectMapper objectMapper) {
List<RSocketGraphQlInterceptor> interceptors = interceptorsProvider.orderedStream()
.collect(Collectors.toList());
return new GraphQlRSocketHandler(graphQlService, interceptors, new Jackson2JsonEncoder(objectMapper));
}
@Bean
@ConditionalOnMissingBean
public GraphQlRSocketController graphQlRSocketController(GraphQlRSocketHandler handler) {
return new GraphQlRSocketController(handler);
}
}

47
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketController.java

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
/*
* Copyright 2012-2022 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.boot.autoconfigure.graphql.rsocket;
import java.util.Map;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.graphql.server.GraphQlRSocketHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
@Controller
class GraphQlRSocketController {
private final GraphQlRSocketHandler handler;
GraphQlRSocketController(GraphQlRSocketHandler handler) {
this.handler = handler;
}
@MessageMapping("${spring.graphql.rsocket.mapping}")
Mono<Map<String, Object>> handle(Map<String, Object> payload) {
return this.handler.handle(payload);
}
@MessageMapping("${spring.graphql.rsocket.mapping}")
Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
return this.handler.handleSubscription(payload);
}
}

20
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/rsocket/package-info.java

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
/*
* Copyright 2020-2022 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.
*/
/**
* Auto-configuration classes for RSocket integration with GraphQL.
*/
package org.springframework.boot.autoconfigure.graphql.rsocket;

150
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/rsocket/GraphQlRSocketAutoConfigurationTests.java

@ -0,0 +1,150 @@ @@ -0,0 +1,150 @@
/*
* Copyright 2012-2022 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.boot.autoconfigure.graphql.rsocket;
import java.net.URI;
import java.time.Duration;
import java.util.function.Consumer;
import graphql.schema.idl.TypeRuntimeWiring;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration;
import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration;
import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.embedded.netty.NettyRouteProvider;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.client.RSocketGraphQlClient;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.GraphQlRSocketHandler;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GraphQlRSocketAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlRSocketAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class)
.withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql");
@Test
void shouldContributeDefaultBeans() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlRSocketHandler.class)
.hasSingleBean(GraphQlRSocketController.class));
}
@Test
void simpleQueryShouldWorkWithTcpServer() {
testWithRSocketTcp(this::assertThatSimpleQueryWorks);
}
@Test
void simpleQueryShouldWorkWithWebSocketServer() {
testWithRSocketWebSocket(this::assertThatSimpleQueryWorks);
}
private void assertThatSimpleQueryWorks(RSocketGraphQlClient client) {
String document = "{ bookById(id: \"book-1\"){ id name pageCount author } }";
String bookName = client.document(document).retrieve("bookById.name").toEntity(String.class)
.block(Duration.ofSeconds(5));
assertThat(bookName).isEqualTo("GraphQL for beginners");
}
private void testWithRSocketTcp(Consumer<RSocketGraphQlClient> consumer) {
ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(
AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class).withPropertyValues(
"spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql");
contextRunner.withInitializer(new RSocketPortInfoApplicationContextInitializer())
.withPropertyValues("spring.rsocket.server.port=0").run((context) -> {
String serverPort = context.getEnvironment().getProperty("local.rsocket.server.port");
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.tcp("localhost", Integer.parseInt(serverPort)).route("graphql").build();
consumer.accept(client);
});
}
private void testWithRSocketWebSocket(Consumer<RSocketGraphQlClient> consumer) {
ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(
AnnotationConfigReactiveWebServerApplicationContext::new).withConfiguration(
AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
ErrorWebFluxAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withUserConfiguration(DataFetchersConfiguration.class, NettyServerConfiguration.class)
.withPropertyValues("spring.main.web-application-type=reactive", "server.port=0",
"spring.graphql.rsocket.mapping=graphql", "spring.rsocket.server.transport=websocket",
"spring.rsocket.server.mapping-path=/rsocket");
contextRunner.run((context) -> {
String serverPort = context.getEnvironment().getProperty("local.server.port");
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.webSocket(URI.create("ws://localhost:" + serverPort + "/rsocket")).route("graphql").build();
consumer.accept(client);
});
}
@Configuration(proxyBeanMethods = false)
static class NettyServerConfiguration {
@Bean
NettyReactiveWebServerFactory serverFactory(NettyRouteProvider routeProvider) {
NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory(0);
serverFactory.addRouteProviders(routeProvider);
return serverFactory;
}
}
@Configuration(proxyBeanMethods = false)
static class DataFetchersConfiguration {
@Bean
RuntimeWiringConfigurer bookDataFetcher() {
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
GraphQlTestDataFetchers.getBookByIdDataFetcher()));
}
}
}

34
spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-graphql.adoc

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
[[web.graphql]]
== Spring GraphQL
If you want to build GraphQL applications, you can take advantage of Spring Boot's auto-configuration for {spring-graphql}[Spring GraphQL].
The Spring GraphQL project is based on https://github.com/graphql-java/graphql-java[GraphQL Java].
== Spring for GraphQL
If you want to build GraphQL applications, you can take advantage of Spring Boot's auto-configuration for {spring-graphql}[Spring for GraphQL].
The Spring for GraphQL project is based on https://github.com/graphql-java/graphql-java[GraphQL Java].
You'll need the `spring-boot-starter-graphql` starter at a minimum.
Because GraphQL is transport-agnostic, you'll also need to have one or more additional starters in your application to expose your GraphQL API over the web:
@ -22,6 +22,9 @@ Because GraphQL is transport-agnostic, you'll also need to have one or more addi @@ -22,6 +22,9 @@ Because GraphQL is transport-agnostic, you'll also need to have one or more addi
| HTTP, WebSocket
| Spring WebFlux
| `spring-boot-starter-rsocket`
| TCP, WebSocket
| Spring WebFlux on Reactor Netty
|===
@ -74,9 +77,11 @@ Spring Data repositories annotated with `@GraphQlRepository` and extending one o @@ -74,9 +77,11 @@ Spring Data repositories annotated with `@GraphQlRepository` and extending one o
are detected by Spring Boot and considered as candidates for `DataFetcher` for matching top-level queries.
[[web.graphql.transports]]
=== Transports
[[web.graphql.web-endpoints]]
=== Web Endpoints
[[web.graphql.transports.http-websocket]]
==== HTTP and WebSocket
The GraphQL HTTP endpoint is at HTTP POST "/graphql" by default. The path can be customized with configprop:spring.graphql.path[].
@ -91,10 +96,6 @@ This is quite useful for retrieving information from an HTTP request header and @@ -91,10 +96,6 @@ This is quite useful for retrieving information from an HTTP request header and
With Spring Boot, you can declare a `WebInterceptor` bean to have it registered with the web transport.
[[web.graphql.cors]]
=== CORS
{spring-framework-docs}/web.html#mvc-cors[Spring MVC] and {spring-framework-docs}/web-reactive.html#webflux-cors[Spring WebFlux] support CORS (Cross-Origin Resource Sharing) requests.
CORS is a critical part of the web config for GraphQL applications that are accessed from browsers using different domains.
@ -111,6 +112,19 @@ Spring Boot supports many configuration properties under the `spring.graphql.cor @@ -111,6 +112,19 @@ Spring Boot supports many configuration properties under the `spring.graphql.cor
----
[[web.graphql.transports.rsocket]]
==== RSocket
RSocket is also supported as a transport, on top of WebSocket or TCP.
Once the <<messaging#messaging.rsocket.server-auto-configuration,RSocket server is configured>>, we can configure our GraphQL handler on a particular route using configprop:spring.graphql.rsocket.mapping[].
For example, configuring that mapping as `"graphql"` means we can use the `RSocketGraphQlClient` as follows.
For RSocket over TCP:
include::code:RSocketGraphQlClientExample[tag=tcp]
For RSocket over WebSocket:
include::code:RSocketGraphQlClientExample[tag=websocket]
[[web.graphql.exception-handling]]
=== Exceptions Handling
@ -126,7 +140,7 @@ Spring Boot will automatically detect `DataFetcherExceptionResolver` beans and r @@ -126,7 +140,7 @@ Spring Boot will automatically detect `DataFetcherExceptionResolver` beans and r
Spring GraphQL offers infrastructure for helping developers when consuming or developing a GraphQL API.
Spring GraphQL ships with a default https://github.com/graphql/graphiql[GraphiQL] page that is exposed at "/graphiql" by default.
Spring GraphQL ships with a default https://github.com/graphql/graphiql[GraphiQL] page that is exposed at `"/graphiql"` by default.
This page is disabled by default and can be turned on with the configprop:spring.graphql.graphiql.enabled[] property.
Many applications exposing such a page will prefer a custom build.
A default implementation is very useful during development, this is why it is exposed automatically with <<using#using.devtools,`spring-boot-devtools`>> during development.

52
spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/graphql/transports/rsocket/RSocketGraphQlClientExample.java

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
/*
* Copyright 2012-2022 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.boot.docs.web.graphql.transports.rsocket;
import java.net.URI;
import java.time.Duration;
import reactor.core.publisher.Mono;
import org.springframework.graphql.client.RSocketGraphQlClient;
public class RSocketGraphQlClientExample {
public void rsocketOverTcp() {
// tag::tcp[]
RSocketGraphQlClient client = RSocketGraphQlClient.builder().tcp("example.spring.io", 8181).route("graphql")
.build();
Mono<Book> book = client.document("{ bookById(id: \"book-1\"){ id name pageCount author } }")
.retrieve("bookById").toEntity(Book.class);
// end::tcp[]
book.block(Duration.ofSeconds(5));
}
public void rsocketOverWebSocket() {
// tag::websocket[]
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.webSocket(URI.create("wss://example.spring.io/rsocket")).route("graphql").build();
Mono<Book> book = client.document("{ bookById(id: \"book-1\"){ id name pageCount author } }")
.retrieve("bookById").toEntity(Book.class);
// end::websocket[]
book.block(Duration.ofSeconds(5));
}
static class Book {
}
}

71
spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/graphql/transports/rsocket/RSocketGraphQlClientExample.kt

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
/*
* Copyright 2012-2022 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.boot.docs.web.graphql.transports.rsocket
import org.springframework.graphql.client.RSocketGraphQlClient
import java.net.URI
import java.time.Duration
class RSocketGraphQlClientExample {
fun rsocketOverTcp() {
// tag::tcp[]
val client = RSocketGraphQlClient.builder()
.tcp("example.spring.io", 8181)
.route("graphql")
.build()
val book = client.document(
"""
{
bookById(id: "book-1"){
id
name
pageCount
author
}
}
""")
.retrieve("bookById").toEntity(Book::class.java)
// end::tcp[]
book.block(Duration.ofSeconds(5))
}
fun rsocketOverWebSocket() {
// tag::websocket[]
val client = RSocketGraphQlClient.builder()
.webSocket(URI.create("wss://example.spring.io/rsocket"))
.route("graphql")
.build()
val book = client.document(
"""
{
bookById(id: "book-1"){
id
name
pageCount
author
}
}
""")
.retrieve("bookById").toEntity(Book::class.java)
// end::websocket[]
book.block(Duration.ofSeconds(5))
}
internal class Book
}
Loading…
Cancel
Save