Annotated Controllers

Spring for GraphQL 提供了一个基于注解的编程模型,其中 @Controller 组件使用注解来声明具有灵活方法签名的处理程序方法,以获取特定 GraphQL 字段的数据。例如:

@Controller
public class GreetingController {

		@QueryMapping (1)
		public String hello() { (2)
			return "Hello, world!";
		}

}
1 将这个方法绑定到一个查询,即 Query 类型下的一个字段。
2 如果注解中未声明,从方法名称确定查询。

Spring for GraphQL 使用 RuntimeWiring.Builder 将上述处理程序方法注册为名为“hello”的查询的 graphql.schema.DataFetcher

Declaration

你可以将 @Controller bean 定义为标准 Spring bean 定义。@Controller 刻板印象允许自动检测,这与 Spring 对检测类路径上的 @Controller@Component 类以及自动注册它们的 bean 定义的一般支持一致。它还充当注解类的刻板印象,指示其在 GraphQL 应用程序中作为数据获取组件的角色。

AnnotatedControllerConfigurer 检测 @Controller bean,并将其带注释的处理程序方法注册为 DataFetcher`s via `RuntimeWiring.Builder。它是 RuntimeWiringConfigurer 的实现,可以添加到 GraphQlSource.BuilderBoot Starter 自动声明 AnnotatedControllerConfigurer 为 bean,并将所有 RuntimeWiringConfigurer bean 添加到 GraphQlSource.Builder,这样便支持带注释的 DataFetcher,请参阅启动器文档中的 GraphQL RuntimeWiring 部分。

@SchemaMapping

@SchemaMapping 注解将一个处理程序方法映射到 GraphQL schema 中的一个字段,并将其声明为该字段的 DataFetcher。注解可以指定父类型名称和字段名称:

@Controller
public class BookController {

	@SchemaMapping(typeName="Book", field="author")
	public Author getAuthor(Book book) {
		// ...
	}
}

@SchemaMapping 注解也可以省去那些属性,在这种情况下,字段名称默认为方法名,而类型名称默认为注射到方法中的源/父对象的简单类名。例如,下面默认为类型“Book”和字段“author”:

@Controller
public class BookController {

	@SchemaMapping
	public Author author(Book book) {
		// ...
	}
}

@SchemaMapping 注解可以在类级别声明,为类中的所有处理程序方法指定一个默认类型名称。

@Controller
@SchemaMapping(typeName="Book")
public class BookController {

	// @SchemaMapping methods for fields of the "Book" type

}

@QueryMapping@MutationMapping@SubscriptionMapping 是元注解,它们本身带 @SchemaMapping 注解,并且将 typeName 预设为 QueryMutationSubscription。实际上,这些是对 Query、Mutation 和 Subscription 类型下的字段的快捷方式注解。例如:

@Controller
public class BookController {

	@QueryMapping
	public Book bookById(@Argument Long id) {
		// ...
	}

	@MutationMapping
	public Book addBook(@Argument BookInput bookInput) {
		// ...
	}

	@SubscriptionMapping
	public Flux<Book> newPublications() {
		// ...
	}
}

@SchemaMapping 处理程序方法具有灵活的签名,并且可以在一系列方法参数和返回值中进行选择。

Method Signature

Schema 映射处理程序方法可以具有以下任何方法参数:

Method Argument Description

@Argument

可访问绑定到更高级别、类型化对象的命名字段参数。请参阅 @Argument

@Argument Map<String, Object>

可访问原始参数值。请参阅 @Argument

ArgumentValue

可访问绑定到更高级别、类型化对象的命名字段参数,以及一个标志,用于指示输入参数是省略了还是设置为 null。请参阅 ArgumentValue

@Arguments

可访问绑定到更高级别、类型化对象的全部字段参数。请参阅 @Arguments

@Arguments Map<String, Object>

要访问原始参数映射。

@ProjectedPayload Interface

可通过项目接口访问字段参数。请参阅 @ProjectedPayload Interface

"Source"

可访问字段的源(即父级/容器)实例。请参阅 Source

Subrange and ScrollSubrange

可访问分页参数。请参阅 PaginationScrollSubrange

Sort

可访问排序详细信息。请参阅 PaginationSort

DataLoader

可访问 DataLoaderRegistry 中的 DataLoader。请参阅 DataLoader

@ContextValue

对于从 DataFetchingEnvironment 中的 GraphQLContext 访问属性。

@LocalContextValue

对于从 DataFetchingEnvironment 中的 GraphQLContext 访问属性。

GraphQLContext

对于从 DataFetchingEnvironment 访问上下文。

java.security.Principal

从 Spring Security 上下文中获取,如果可用。

@AuthenticationPrincipal

对于从 Spring Security 上下文中访问 Authentication#getPrincipal()

DataFetchingFieldSelectionSet

对于通过 DataFetchingEnvironment 访问查询的选择集。

Locale, Optional<Locale>

对于从 DataFetchingEnvironment 访问 Locale

DataFetchingEnvironment

对于直接访问底层 DataFetchingEnvironment

Schema 映射处理程序方法可以返回:

  • 任何类型的解析值。

  • Mono`和 `Flux`用于异步值。支持控制器方法,以及任何 `DataFetcher,如 Reactive DataFetcher中所述。

  • `java.util.concurrent.Callable`让值异步生成。要做到这一点,`AnnotatedControllerConfigurer`必须使用 `Executor`进行配置。

@Argument

在 GraphQL Java 中,DataFetchingEnvironment 提供对特定于字段的参数值的映射的访问。这些值可以是简单的标量值(例如 String、Long)、用于更复杂的输入的 Map 或值的 List

使用 @Argument 注解使一个参数绑定到目标对象并注入到处理程序方法中。绑定是通过将参数值映射到预期的使用方法参数类型的基本数据构造函数或使用默认构造函数创建对象然后将参数值映射到其属性来执行的。这会递归重复,使用所有嵌套参数值,并相应地创建嵌套目标对象。例如:

@Controller
public class BookController {

	@QueryMapping
	public Book bookById(@Argument Long id) {
		// ...
	}

	@MutationMapping
	public Book addBook(@Argument BookInput bookInput) {
		// ...
	}
}

如果目标对象没有 setter,并且您无法更改它,则可以使用 AnnotatedControllerConfigurer 中的 aproperty 来允许使用直接字段访问权限进行回退绑定。

如果没有给出 method 参数名(Java 8 以上版本中需要使用 -parameters 编译器标记,或者要求编译器的调试信息),那么将使用它来查找参数。如果需要,您可以通过注释来自定义名称,例如:@Argument("bookInput").

@Argument 注释没有 “required” 标志,也没有指定默认值。可在 GraphQL Schema 级别指定这两者,并由 GraphQL Java 强制执行。

如果绑定失败,则会抛出一个 BindException,它会将绑定问题积累为 field 错误,其中每个错误的 field 都是出现问题后的参数路径。

使用一个 Map<String, Object> 参数,并配以 @Argument,就能获取参数的原始值。例如:

@Controller
public class BookController {

	@MutationMapping
	public Book addBook(@Argument Map<String, Object> bookInput) {
		// ...
	}
}

在 1.2 之前,如果注释未指定名称,@Argument Map<String, Object> 将返回完整参数映射。在 1.2 之后,使用 Map<String, Object>@Argument 将始终返回原始参数值,匹配注释中指定的名称或参数名称。如需访问完整参数映射,请改为使用 xref:controllers.adoc#controllers.schema-mapping.arguments[@Arguments

ArgumentValue

默认情况下,GraphQL 中的输入参数是可空的和可选的,这意味着可以在 null 文字中设置参数,或者根本不提供参数。这种区分对部分更新很有用,其中底层数据也可以相应地设置成 null 或根本不更改。当使用 xref:controllers.adoc#controllers.schema-mapping.argument[@Argument 时,无法做出这种区分,因为在两种情况下你都会得到 null 或一个空 Optional

如果您想了解一个值是否根本没有提供,则可以声明一个 ArgumentValue 方法参数,它是一个结果值的一个简单容器,并带有标志,以表示输入参数是否全部都已省略。您可以使用它来代替 @Argument,在这种情况下,参数名将根据方法参数名来决定,或者与 @Argument 一起使用来指定参数名。

例如:

@Controller
public class BookController {

	@MutationMapping
	public void addBook(ArgumentValue<BookInput> bookInput) {
		if (!bookInput.isOmitted()) {
			BookInput value = bookInput.value();
			// ...
		}
	}
}

ArgumentValue 也被支持为一个 @Argument 方法参数的对象结构中的一个字段,可以经由一个构造器参数或一个 setter 来初始化,包括作为位于顶层对象下任何级别的对象的字段。

@Arguments

如果您想将完整的参数映射捆绑到一个单独的目标对象上,而不是捆绑一个特定的带名字的参数,那么请使用 @Arguments 注释。

例如,@Argument BookInput bookInput 使用参数 "bookInput" 的值来初始化 BookInput,而 @Arguments 使用完整的参数映射,在这种情况下,顶层参数将被捆绑到 BookInput 属性。

使用一个 Map<String, Object> 参数,并配以 @Arguments,就能获取所有参数值的原始映射。

@ProjectedPayload Interface

作为使用具有 @Argument 的完整对象的备选方案,您还可以使用投影接口通过明确定义的最小接口访问 GraphQL 请求参数。当 Spring Data 位于类路径中时,参数投影由 Spring Data’s Interface projections 提供。

要使用此方法,请创建一个带有 @ProjectedPayload 注释的接口,并将其声明为一个控制器方法参数。如果参数带 @Argument 注释了,那么它将适用于 DataFetchingEnvironment.getArguments() 映射中的单个参数。当在没有 @Argument 的情况下声明该映射时,该投影将在完整的参数映射中的顶层参数上起作用。

例如:

@Controller
public class BookController {

	@QueryMapping
	public Book bookById(BookIdProjection bookId) {
		// ...
	}

	@MutationMapping
	public Book addBook(@Argument BookInputProjection bookInput) {
		// ...
	}
}

@ProjectedPayload
interface BookIdProjection {

	Long getId();
}

@ProjectedPayload
interface BookInputProjection {

	String getName();

	@Value("#{target.author + ' ' + target.name}")
	String getAuthorAndName();
}

Source

在 GraphQL Java 中,DataFetchingEnvironment 提供对字段的来源(即父级/容器)实例的访问权限。要访问此方法,只需声明一个带有目标类型的预期方法参数。

@Controller
public class BookController {

	@SchemaMapping
	public Author author(Book book) {
		// ...
	}
}

源方法参数也有助于确定映射的类型名称。如果 Java 类的简单名称与 GraphQL 类型匹配,那么不需要在 @SchemaMapping 注释中显式指定类型名称。

@BatchMapping 处理程序方法可以批量加载给定源/父级书籍对象列表的查询的所有作者。

Subrange

当 Spring 配置中有一个 xref:request-execution.adoc#execution.pagination.cursor.strategy[CursorStrategy bean 时,控制器方法支持 Subrange<P> 参数,其中 <P> 是从游标转换后的相对位置。对于 Spring Data,ScrollSubrange 引出 ScrollPosition。例如:

@Controller
public class BookController {

	@QueryMapping
	public Window<Book> books(ScrollSubrange subrange) {
		ScrollPosition position = subrange.position().orElse(ScrollPosition.offset());
		int count = subrange.count().orElse(20);
		// ...
	}

}

有关分页和内置机制的概述,请参见 Pagination

Sort

当 Spring 配置中存在 SortStrategy bean 时,控制器方法支持将 Sort 作为方法参数。例如:

@Controller
public class BookController {

	@QueryMapping
	public Window<Book> books(Optional<Sort> optionalSort) {
		Sort sort = optionalSort.orElse(Sort.by(..));
	}

}

DataLoader

当为实体注册批量加载函数(如 Batch Loading 中所述)时,可以通过声明 DataLoader 类型的 method 参数来访问该实体的 DataLoader,并使用它来加载该实体:

@Controller
public class BookController {

	public BookController(BatchLoaderRegistry registry) {
		registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
			// return Map<Long, Author>
		});
	}

	@SchemaMapping
	public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
		return loader.load(book.getAuthorId());
	}

}

默认情况下,BatchLoaderRegistry 使用值的类型(例如:Author 的类名)的完整类名作为注册的密钥,因此,只需用泛型类型声明 DataLoader 方法参数就能提供足够的信息在 DataLoaderRegistry 中找到它。作为备选方案,DataLoader 方法参数解析器还会尝试使用方法参数名作为密钥,但一般来说,这是没有必要的。

请注意,对于许多与加载相关实体的情况,其中 @SchemaMapping 仅仅委派给 DataLoader,你可以通过使用下一节中所述的 @BatchMapping 方法来减少样板代码。

Validation

当找到 javax.validation.Validator bean 时, AnnotatedControllerConfigurer 允许在带注释的控制器方法上使用 Bean Validation。通常,bean 的类型为 LocalValidatorFactoryBean

Bean Validation 让你可以声明类型约束:

public class BookInput {

	@NotNull
	private String title;

	@NotNull
	@Size(max=13)
	private String isbn;
}

然后,你可以在方法调用之前使用 @Valid 注释控制器方法参数,对其进行验证:

@Controller
public class BookController {

	@MutationMapping
	public Book addBook(@Argument @Valid BookInput bookInput) {
		// ...
	}
}

如果在验证过程中发生错误,则会引发 ConstraintViolationException。你可以使用 Exceptions 链来决定如何将其显示给客户端,方法是将其转换为包含在 GraphQL 响应中的错误。

@Valid 之外,您还可以使用 Spring 的 @Validated,它允许指定验证组。

Bean 验证适用于 @Argument`、`@Arguments`` 和 @ProjectedPayload 方法参数,但更普遍地适用于任何方法参数。

Example 1. Validation and Kotlin Coroutines

Hibernate Validator 与 Kotlin 协程方法不兼容,在内省其方法参数时会失败。请参阅 spring-projects/spring-graphql#344 (comment) 以获取相关问题的链接和建议的解决方法。

@BatchMapping

Batch Loading 通过使用 org.dataloader.DataLoader 来延迟加载各个实体实例,以便可以将它们一起加载,从而解决了 N+1 选择问题。例如:

@Controller
public class BookController {

	public BookController(BatchLoaderRegistry registry) {
		registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
			// return Map<Long, Author>
		});
	}

	@SchemaMapping
	public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
		return loader.load(book.getAuthorId());
	}

}

对于上面所示的加载关联实体的简单案例,@SchemaMapping 方法所做的不仅仅是委派到 DataLoader。这是一个可以通过 @BatchMapping 方法避免的样板。例如:

@Controller
public class BookController {

	@BatchMapping
	public Mono<Map<Book, Author>> author(List<Book> books) {
		// ...
	}
}

上面的代码在 BatchLoaderRegistry 中成为一个批处理加载函数,其中键为 Book 实例,加载的值为其作者。此外,还将`DataFetcher` 透明地绑定到 Book 类型的 author 字段,它只是给定其源/父 Book 实例,委派给作者的`DataLoader`。

为了用作唯一键,Book 必须实现 hashcode 和`equals`。

默认情况下,字段名称默认为方法名称,而类型名称默认为输入 List 元素类型的简单类名称。两者都可以通过注释属性自定义。类型名称也可以从类级别的`@SchemaMapping` 继承。

Method Signature

批处理映射方法支持以下参数:

Method Argument Description

List<K>

The source/parent objects.

java.security.Principal

从 Spring Security 环境中获取(如果存在)。

@ContextValue

要访问 BatchLoaderEnvironmentGraphQLContext 的值,这是与 DataFetchingEnvironment 中的值相同的环境。

GraphQLContext

要访问 BatchLoaderEnvironment 中的环境,这是与 DataFetchingEnvironment 中的值相同的环境。

BatchLoaderEnvironment

在 GraphQL Java 中可用的 org.dataloader.BatchLoaderWithContext 环境。

批处理映射方法可以返回:

Return Type Description

Mono<Map<K,V>>

一个映射,其中父对象为键,批量加载的对象为值。

Flux<V>

按相同顺序加载对象的序列,与传入该方法的源/父对象保持一致。

Map<K,V>, Collection<V>

命令变型,例如,无需进行远程调用。

Callable<Map<K,V>>, Callable<Collection<V>>

要异步调用的必要变体。要实现此功能,AnnotatedControllerConfigurer 必须配置为 Executor

@GraphQlExceptionHandler

使用 @GraphQlExceptionHandler 方法来使用灵活的 method signature 处理数据获取异常。当在控制器中声明时,异常处理程序方法适用于来自同一控制器的异常:

@Controller
public class BookController {

	@QueryMapping
	public Book bookById(@Argument Long id) {
		// ...
	}

	@GraphQlExceptionHandler
	public GraphQLError handle(BindException ex) {
		return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build();
	}

}

@ControllerAdvice 中声明时,异常处理程序方法应用于所有控制器:

@ControllerAdvice
public class GlobalExceptionHandler {

	@GraphQlExceptionHandler
	public GraphQLError handle(BindException ex) {
		return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build();
	}

}

通过 @GraphQlExceptionHandler 方法进行异常处理会自动应用于控制器调用。要处理来自其他 graphql.schema.DataFetcher 实现的异常(不基于控制器方法),请从 AnnotatedControllerConfigurer 获取 DataFetcherExceptionResolver,并将其作为 DataFetcherExceptionResolver 注册到 GraphQlSource.Builder 中。

Method Signature

异常处理方法支持灵活的方法签名,其中方法参数从 DataFetchingEnvironment, 中解析,并与 @SchemaMapping methods 的方法参数匹配。

支持的返回类型如下列出:

Return Type Description

graphql.GraphQLError

将异常解析为单个字段错误。

Collection<GraphQLError>

将异常解析为多个字段错误。

void

解析异常,没有响应错误。

Object

将异常解析为单个错误、多个错误或无错误。 返回值必须是 GraphQLErrorCollection&lt;GraphQLError&gt;null

Mono<T>

用于 &lt;T&gt; 是其中一个受支持的同步返回类型的情况下进行异步解析。