服务器传输

Spring for GraphQL 支持通过 HTTP、WebSocket 和 RSocket 处理 GraphQL 请求。

HTTP

GraphQlHttpHandler 用于处理基于 HTTP 的 GraphQL 请求,并委托给拦截器链进行请求执行。它有两种变体,一种用于 Spring MVC,一种用于 Spring WebFlux。两者都异步处理请求,功能等效,但分别依赖阻塞或非阻塞 I/O 来写入 HTTP 响应。

请求必须使用 HTTP POST,内容类型为 "application/json",GraphQL 请求详情作为 JSON 包含在请求体中,这遵循提议的GraphQL over HTTP规范。JSON 体成功解码后,HTTP 响应状态始终为 200 (OK),GraphQL 请求执行中的任何错误都会出现在 GraphQL 响应的 "errors" 部分中。默认且首选的媒体类型是 "application/graphql-response+json",但也支持 "application/json",如规范所述。

通过声明一个 RouterFunction bean 并使用 Spring MVC 或 WebFlux 中的 RouterFunctions 来创建路由,可以将 GraphQlHttpHandler 暴露为 HTTP 端点。Boot Starter 就做了这件事,详情请参阅Web 端点部分,或者查看其中包含的 GraphQlWebMvcAutoConfigurationGraphQlWebFluxAutoConfiguration 获取实际配置。

默认情况下,GraphQlHttpHandler 会使用 Web 框架中配置的 HttpMessageConverter (Spring MVC) 和 DecoderHttpMessageReader/EncoderHttpMessageWriter (WebFlux) 来序列化和反序列化 JSON 有效载荷。在某些情况下,应用程序为 HTTP 端点配置的 JSON 编解码器可能与 GraphQL 有效载荷不兼容。应用程序可以使用自定义 JSON 编解码器实例化 GraphQlHttpHandler,该编解码器将用于 GraphQL 有效载荷。

此仓库的 1.0.x 分支包含一个 Spring MVC HTTP 示例应用程序。

服务器发送事件

GraphQlSseHandler 与上面列出的 HTTP handler 非常相似,但它使用 Server-Sent Events 协议处理基于 HTTP 的 GraphQL 请求。使用此传输方式时,客户端必须向端点发送 HTTP POST 请求,内容类型为 "application/json",GraphQL 请求详情作为 JSON 包含在请求体中;与普通 HTTP 变体的唯一区别在于客户端必须发送 "text/event-stream" 作为 "Accept" 请求头。响应将作为一条或多条 Server-Sent Event 发送。

这也在提议的GraphQL over HTTP规范中有所定义。Spring for GraphQL 只实现了 "独立连接模式"(Distinct connections mode),因此应用程序必须考虑可扩展性问题,以及采用 HTTP/2 作为底层传输方式是否会有所帮助。

GraphQlSseHandler 的主要用例是替代WebSocket 传输方式,用于接收订阅操作的响应流。其他类型的操作(如查询和变更)在此不受支持,应使用纯 JSON over HTTP 传输变体。

文件上传

作为一种协议,GraphQL 专注于文本数据的交换。这不包括图像等二进制数据,但有一个独立的、非正式的graphql-multipart-request-spec,允许通过 HTTP 上传文件到 GraphQL。

Spring for GraphQL 不直接支持 graphql-multipart-request-spec。虽然该规范确实提供了统一 GraphQL API 的好处,但实际经验导致了一些问题,并且最佳实践建议已经有所发展,有关更详细的讨论,请参阅Apollo Server 文件上传最佳实践

如果您想在应用程序中使用 graphql-multipart-request-spec,可以通过 multipart-spring-graphql 库来实现。

WebSocket

GraphQlWebSocketHandler 处理基于 WebSocket 的 GraphQL 请求,遵循 graphql-ws 库中定义的协议。使用 GraphQL over WebSocket 的主要原因是订阅,它允许发送 GraphQL 响应流,但它也可以用于单个响应的常规查询。该 handler 将每个请求委托给拦截器链以进一步执行请求。

GraphQL Over WebSocket 协议

存在两种此类协议,一种在 subscriptions-transport-ws 库中,另一种在 graphql-ws 库中。前者不再活跃,已被后者取代。请阅读这篇博客文章了解其历史。

GraphQlWebSocketHandler 有两种变体,一种用于 Spring MVC,一种用于 Spring WebFlux。两者都异步处理请求,功能等效。WebFlux handler 还使用非阻塞 I/O 和背压来流式传输消息,这非常有效,因为在 GraphQL Java 中,订阅响应是一个 Reactive Streams Publisher

graphql-ws 项目列出了许多客户端使用的示例

通过声明一个 SimpleUrlHandlerMapping bean 并使用它将 handler 映射到 URL 路径,可以将 GraphQlWebSocketHandler 暴露为 WebSocket 端点。默认情况下,Boot Starter 不会暴露 GraphQL over WebSocket 端点,但您可以添加一个指定端点路径的属性来启用它。请查阅 Boot 参考文档中的Web 端点部分,以及支持的 spring.graphql.websocket 属性列表。您也可以查看 GraphQlWebMvcAutoConfigurationGraphQlWebFluxAutoConfiguration 获取实际的 Boot 自动配置详情。

此仓库的 1.0.x 分支包含一个 WebFlux WebSocket 示例应用程序。

RSocket

GraphQlRSocketHandler 处理基于 RSocket 的 GraphQL 请求。查询和变更操作期望并被作为 RSocket request-response 交互处理,而订阅操作被作为 request-stream 处理。

GraphQlRSocketHandler 可以作为映射到 GraphQL 请求路由的 @Controller 的委托对象使用。例如

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
public class GraphQlRSocketController {

	private final GraphQlRSocketHandler handler;

	GraphQlRSocketController(GraphQlRSocketHandler handler) {
		this.handler = handler;
	}

	@MessageMapping("graphql")
	public Mono<Map<String, Object>> handle(Map<String, Object> payload) {
		return this.handler.handle(payload);
	}

	@MessageMapping("graphql")
	public Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
		return this.handler.handleSubscription(payload);
	}
}

拦截

服务器传输方式允许在调用 GraphQL Java 引擎处理请求之前和之后拦截请求。

WebGraphQlInterceptor

HTTPWebSocket 传输方式调用一个包含 0 个或多个 WebGraphQlInterceptor 的链,然后是调用 GraphQL Java 引擎的 ExecutionGraphQlService。拦截器允许应用程序拦截传入请求,以便

  • 检查 HTTP 请求详情

  • 自定义 graphql.ExecutionInput

  • 添加 HTTP 响应头

  • 自定义 graphql.ExecutionResult

  • 等等

例如,拦截器可以将 HTTP 请求头传递给 DataFetcher

import java.util.Collections;

import reactor.core.publisher.Mono;

import org.springframework.graphql.data.method.annotation.ContextValue;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.stereotype.Controller;

class RequestHeaderInterceptor implements WebGraphQlInterceptor { (1)

	@Override
	public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
		String value = request.getHeaders().getFirst("myHeader");
		request.configureExecutionInput((executionInput, builder) ->
				builder.graphQLContext(Collections.singletonMap("myHeader", value)).build());
		return chain.next(request);
	}
}

@Controller
class MyContextValueController { (2)

	@QueryMapping
	Person person(@ContextValue String myHeader) {
		...
	}
}
1 拦截器将 HTTP 请求头值添加到 GraphQLContext 中
2 数据控制器方法访问该值

反过来,拦截器可以访问控制器添加到 GraphQLContext 中的值

import graphql.GraphQLContext;
import reactor.core.publisher.Mono;

import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Controller;

// Subsequent access from a WebGraphQlInterceptor

class ResponseHeaderInterceptor implements WebGraphQlInterceptor {

	@Override
	public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) { (2)
		return chain.next(request).doOnNext((response) -> {
			String value = response.getExecutionInput().getGraphQLContext().get("cookieName");
			ResponseCookie cookie = ResponseCookie.from("cookieName", value).build();
			response.getResponseHeaders().add(HttpHeaders.SET_COOKIE, cookie.toString());
		});
	}
}

@Controller
class MyCookieController {

	@QueryMapping
	Person person(GraphQLContext context) { (1)
		context.put("cookieName", "123");
		...
	}
}
1 控制器将值添加到 GraphQLContext
2 拦截器使用该值添加 HTTP 响应头

WebGraphQlHandler 可以修改 ExecutionResult,例如,检查和修改在执行开始之前引发且无法通过 DataFetcherExceptionResolver 处理的请求验证错误

import java.util.List;

import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import reactor.core.publisher.Mono;

import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;

class RequestErrorInterceptor implements WebGraphQlInterceptor {

	@Override
	public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
		return chain.next(request).map((response) -> {
			if (response.isValid()) {
				return response; (1)
			}

			List<GraphQLError> errors = response.getErrors().stream() (2)
					.map((error) -> {
						GraphqlErrorBuilder<?> builder = GraphqlErrorBuilder.newError();
						// ...
						return builder.build();
					})
					.toList();

			return response.transform((builder) -> builder.errors(errors).build()); (3)
		});
	}
}
1 如果 ExecutionResult 包含非空值的 "data" 键,则返回相同结果
2 检查并转换 GraphQL 错误
3 使用修改后的错误更新 ExecutionResult

使用 WebGraphQlHandler 配置 WebGraphQlInterceptor 链。这得到Boot Starter的支持,请参阅Web 端点

WebSocketGraphQlInterceptor

WebSocketGraphQlInterceptor 扩展了 WebGraphQlInterceptor,增加了处理 WebSocket 连接开始和结束以及客户端取消订阅的回调。它也会拦截 WebSocket 连接上的每个 GraphQL 请求。

使用 WebGraphQlHandler 配置 WebGraphQlInterceptor 链。这得到Boot Starter的支持,请参阅Web 端点。一个拦截器链中最多只能有一个 WebSocketGraphQlInterceptor

有两个内置的 WebSocket 拦截器,称为 AuthenticationWebSocketInterceptor,一个用于 WebMVC 传输,一个用于 WebFlux 传输。这些拦截器有助于从 GraphQL over WebSocket 消息的 "connection_init" 有效载荷中提取认证详情,进行认证,然后将 SecurityContext 传播到 WebSocket 连接上的后续请求。

spring-graphql-examples 中有一个 websocket-authentication 示例。

RSocketQlInterceptor

类似于WebGraphQlInterceptorRSocketQlInterceptor 允许在 GraphQL Java 引擎执行之前和之后拦截 GraphQL over RSocket 请求。您可以使用它来自定义 graphql.ExecutionInputgraphql.ExecutionResult