带注释的控制器
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
。
声明
您可以将 @Controller
bean 定义为标准 Spring bean 定义。@Controller
构造型允许自动检测,与 Spring 对检测类路径上的 @Controller
和 @Component
类以及自动注册它们的 bean 定义的一般支持保持一致。它还充当注释类的一个构造型,表明它在 GraphQL 应用程序中作为数据获取组件的作用。
AnnotatedControllerConfigurer
检测 @Controller
bean,并通过 RuntimeWiring.Builder
将其注释的处理程序方法注册为 DataFetcher
。它是 RuntimeWiringConfigurer
的一个实现,可以添加到 GraphQlSource.Builder
。 Boot Starter 自动将 AnnotatedControllerConfigurer
声明为一个 bean,并将所有 RuntimeWiringConfigurer
bean 添加到 GraphQlSource.Builder
,从而支持带注释的 DataFetcher
,请参阅 Boot starter 文档中的 GraphQL RuntimeWiring 部分。
@SchemaMapping
@SchemaMapping
注释将处理程序方法映射到 GraphQL 模式中的一个字段,并声明它为此字段的 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 分别预设为 Query
、Mutation
或 Subscription
。实际上,这些是分别用于 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
处理程序方法具有灵活的签名,并且可以选择一系列方法参数和返回值。
方法参数
模式映射处理程序方法可以具有以下任何方法参数
方法参数 | 说明 |
---|---|
|
用于访问绑定到更高级别类型化对象的命名字段参数。 请参阅 |
|
用于访问原始参数值。 请参阅 |
|
用于访问绑定到更高级别类型化对象的命名字段参数,以及一个标志,以指示输入参数是已省略还是设置为 请参阅 |
|
用于访问绑定到更高级别类型化对象的所有字段参数。 请参阅 |
|
用于访问参数的原始映射。 |
|
用于通过项目接口访问字段参数。 请参阅 |
“Source” |
用于访问字段的源(即父/容器)实例。 请参阅 Source。 |
|
用于访问分页参数。 |
|
有关排序详细信息的访问权限。 |
|
有关访问 请参阅 |
|
有关访问 |
|
有关访问 |
|
有关访问 |
|
如果可用,则从 Spring Security 上下文中获取。 |
|
有关访问 Spring Security 上下文中的 |
|
有关通过 |
|
有关访问 |
|
有关直接访问底层 |
返回值
模式映射处理程序方法可以返回
-
任何类型的已解析值。
-
用于异步值(s) 的
Mono
和Flux
。支持控制器方法和任何DataFetcher
,如 反应式DataFetcher
中所述。 -
Kotlin 协程和
Flow
适用于Mono
和Flux
。 -
java.util.concurrent.Callable
以异步方式生成值(s)。为此,必须使用Executor
配置AnnotatedControllerConfigurer
。
在 Java 21+ 上,当使用 Executor
配置 AnnotatedControllerConfigurer
时,会异步调用具有阻塞方法签名的控制器方法。默认情况下,如果控制器方法不返回异步类型(如 Flux
、Mono
、CompletableFuture
),并且也不是 Kotlin 挂起函数,则该方法被视为阻塞。您可以在 AnnotatedControllerConfigurer
上配置阻塞控制器方法 Predicate
,以帮助确定哪些方法被视为阻塞。
当属性 spring.threads.virtual.enabled 设置时,用于 Spring for GraphQL 的 Spring Boot starter 会使用 Executor 自动为虚拟线程配置 AnnotatedControllerConfigurer 。
|
接口模式映射
当控制器方法映射到模式接口字段时,默认情况下,该映射将替换为多个映射,每个映射对应一个实现该接口的模式对象类型。这允许对所有子类型使用一个控制器方法。
例如,给定
type Query {
activities: [Activity!]!
}
interface Activity {
id: ID!
coordinator: User!
}
type FooActivity implements Activity {
id: ID!
coordinator: User!
}
type BarActivity implements Activity {
id: ID!
coordinator: User!
}
type User {
name: String!
}
您可以编写如下控制器
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@SchemaMapping
public User coordinator(Activity activity) {
// Called for any Activity subtype
}
}
如有必要,您可以接管各个子类型的映射
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@SchemaMapping
public User coordinator(Activity activity) {
// Called for any Activity subtype except FooActivity
}
@SchemaMapping
public User coordinator(FooActivity activity) {
// ...
}
}
@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 上的属性来允许通过直接字段访问进行绑定。
|
默认情况下,如果方法参数名称可用(需要 Java 8+ 的 -parameters
编译器标志或来自编译器的调试信息),则使用它来查找参数。如有需要,您可以通过注释自定义名称,例如 @Argument("bookInput")
。
@Argument 注释没有“required”标志,也没有指定默认值选项。这两者都可以在 GraphQL 模式级别指定,并且由 GraphQL Java 强制执行。
|
如果绑定失败,则会引发 BindException
,其中绑定问题累积为字段错误,其中每个错误的 field
是发生问题的参数路径。
您可以将 @Argument
与 Map<String, Object>
参数一起使用,以获取参数的原始值。例如
@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 始终返回原始参数值,匹配注释中指定的名称或参数名称。要访问完整的参数映射,请改用 @Arguments 。
|
ArgumentValue
默认情况下,GraphQL 中的输入参数是可空且可选的,这意味着参数可以设置为 null
文字,或者根本不提供。此区别对于具有突变的部分更新很有用,其中基础数据也可以相应地设置为 null
或根本不更改。使用 @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
相反。
例如,@Argument BookInput bookInput
使用参数“bookInput”的值来初始化 BookInput
,而 @Arguments
使用完整参数映射,在这种情况下,顶层参数绑定到 BookInput
属性。
你可以将 @Arguments
与 Map<String, Object>
参数一起使用,以获取所有参数值原始映射。
@ProjectedPayload
接口
作为使用带有 @Argument
的完整对象的替代方法,你还可以使用投影接口通过一个定义明确的最小接口来访问 GraphQL 请求参数。当 Spring Data 在类路径中时,Spring Data 的 接口投影 提供参数投影。
要使用此功能,请创建一个使用 @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();
}
源
在 GraphQL Java 中,DataFetchingEnvironment
提供对字段的源(即父级/容器)实例的访问。要访问它,只需声明一个预期目标类型的函数参数。
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
源函数参数还有助于确定映射的类型名称。如果 Java 类的简单名称与 GraphQL 类型匹配,则无需在 @SchemaMapping
注解中明确指定类型名称。
|
@BatchMapping
当 Spring 配置中存在 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);
// ...
}
}
有关分页和内置机制的概述,请参阅 分页。
Sort
当 Spring 配置中存在 SortStrategy bean 时,控制器函数支持 Sort
作为函数参数。例如
@Controller
public class BookController {
@QueryMapping
public Window<Book> books(Optional<Sort> optionalSort) {
Sort sort = optionalSort.orElse(Sort.by(..));
}
}
DataLoader
当您为实体注册批量加载函数(如 批量加载 中所述)时,您可以通过声明类型为 DataLoader
的函数参数来访问该实体的 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 函数(如下一节所述)来减少样板代码。
验证
当找到 javax.validation.Validator
bean 时,AnnotatedControllerConfigurer
启用对带注释的控制器函数的 Bean 验证 的支持。通常,该 bean 的类型为 LocalValidatorFactoryBean
。
Bean 验证允许您声明对类型的约束
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
。您可以使用 异常 链来决定如何将其呈现给客户端,方法是将其转换为要包含在 GraphQL 响应中的错误。
除了 @Valid ,您还可以使用 Spring 的 @Validated ,它允许指定验证组。
|
Bean 验证对于 @Argument
、@Arguments
和 @ProjectedPayload 方法参数非常有用,但更普遍地适用于任何方法参数。
验证和 Kotlin 协程
Hibernate Validator 与 Kotlin 协程方法不兼容,并且在内省其方法参数时会失败。请参阅 spring-projects/spring-graphql#344 (评论) 以获取相关问题的链接和建议的解决方法。 |
@BatchMapping
批量加载 通过使用 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
字段,该字段仅委托给作者的 DataLoader
,给定其源/父 Book
实例。
要作为唯一键使用, |
默认情况下,字段名称默认为方法名称,而类型名称默认为输入 List
元素类型的简单类名。两者都可以通过注释属性进行自定义。类型名称也可以从类级别的 @SchemaMapping
继承。
方法参数
批量映射方法支持以下参数
方法参数 | 说明 |
---|---|
|
源/父对象。 |
|
如果可用,则从 Spring Security 上下文中获取。 |
|
用于访问 |
|
用于访问 |
|
GraphQL Java 中可用于 |
返回值
批处理映射方法可以返回
返回类型 | 说明 |
---|---|
|
一个以父对象为键,以批处理加载的对象为值的映射。 |
|
一个批处理加载的对象序列,该序列必须与传递给该方法的源/父对象保持相同的顺序。 |
|
命令式变体,例如无需进行远程调用。 |
|
要异步调用的命令式变体。要实现此操作,必须使用 |
带有 |
已调整为 |
在 Java 21+ 上,当使用 Executor
配置 AnnotatedControllerConfigurer
时,会异步调用具有阻塞方法签名的控制器方法。默认情况下,如果控制器方法不返回异步类型(如 Flux
、Mono
、CompletableFuture
),并且也不是 Kotlin 挂起函数,则该方法被视为阻塞。您可以在 AnnotatedControllerConfigurer
上配置阻塞控制器方法 Predicate
,以帮助确定哪些方法被视为阻塞。
当属性 spring.threads.virtual.enabled 设置时,用于 Spring for GraphQL 的 Spring Boot starter 会使用 Executor 自动为虚拟线程配置 AnnotatedControllerConfigurer 。
|
接口批处理映射
与 接口模式映射 的情况一样,当批处理映射方法映射到模式接口字段时,该映射将替换为多个映射,每个映射对应于实现该接口的每个模式对象类型。
这意味着,给定以下内容
type Query {
activities: [Activity!]!
}
interface Activity {
id: ID!
coordinator: User!
}
type FooActivity implements Activity {
id: ID!
coordinator: User!
}
type BarActivity implements Activity {
id: ID!
coordinator: User!
}
type User {
name: String!
}
您可以编写如下控制器
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@BatchMapping
Map<Activity, User> coordinator(List<Activity> activities) {
// Called for all Activity subtypes
}
}
如有必要,您可以接管各个子类型的映射
@Controller
public class BookController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@BatchMapping
Map<Activity, User> coordinator(List<Activity> activities) {
// Called for all Activity subtypes
}
@BatchMapping(field = "coordinator")
Map<Activity, User> fooCoordinator(List<FooActivity> activities) {
// ...
}
}
@GraphQlExceptionHandler
使用 @GraphQlExceptionHandler
方法来处理具有灵活 方法签名 的数据提取异常。在控制器中声明时,异常处理程序方法适用于来自同一控制器的异常
@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
,并将其注册到 GraphQlSource.Builder
中,作为 DataFetcherExceptionResolver。
方法签名
异常处理程序方法支持灵活的方法签名,其中方法参数从 DataFetchingEnvironment
解析,并与 @SchemaMapping 方法 的参数匹配。
支持的返回类型如下所列
返回类型 | 说明 |
---|---|
|
将异常解析为单个字段错误。 |
|
将异常解析为多个字段错误。 |
|
解析异常,不返回错误。 |
|
将异常解析为单个错误、多个错误或无错误。返回值必须为 |
|
用于异步解析,其中 |
命名空间
在架构级别,查询和突变操作直接在 Query
和 Mutation
类型下定义。丰富的 GraphQL API 可以在这些类型下定义几十个操作,这使得探索 API 和分离关注点变得更加困难。你可以选择在 GraphQL 架构中定义命名空间。虽然这种方法有一些需要注意的地方,但你可以使用 Spring for GraphQL 注解控制器实现此模式。
使用命名空间,你的 GraphQL 架构可以将查询操作嵌套在顶级类型下,而不是直接在 Query
下列出它们。在此,我们将定义 MusicQueries
和 UserQueries
类型,并使它们在 Query
下可用
type Query {
music: MusicQueries
users: UserQueries
}
type MusicQueries {
album(id: ID!): Album
searchForArtist(name: String!): [Artist]
}
type Album {
id: ID!
title: String!
}
type Artist {
id: ID!
name: String!
}
type UserQueries {
user(login: String): User
}
type User {
id: ID!
login: String!
}
GraphQL 客户端会像这样使用 album
查询
{
music {
album(id: 42) {
id
title
}
}
}
并获得以下响应
{
"data": {
"music": {
"album": {
"id": "42",
"title": "Spring for GraphQL"
}
}
}
}
这可以在 @Controller
中使用以下模式实现
import java.util.List;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
@Controller
@SchemaMapping(typeName = "MusicQueries") (1)
public class MusicController {
@QueryMapping (2)
public MusicQueries music() {
return new MusicQueries();
}
(3)
public record MusicQueries() {
}
@SchemaMapping (4)
public Album album(@Argument String id) {
return new Album(id, "Spring GraphQL");
}
@SchemaMapping
public List<Artist> searchForArtist(@Argument String name) {
return List.of(new Artist("100", "the Spring team"));
}
}
1 | 使用 @SchemaMapping 和 typeName 属性对控制器进行注释,以避免在方法上重复它 |
2 | 为“music”命名空间定义 @QueryMapping |
3 | “music”查询返回“空”记录,但也可以返回空映射 |
4 | 查询现在声明为“MusicQueries”类型下的字段 |
无需在控制器中显式声明包装类型(“MusicQueries”、“UserQueries”),你可以选择使用 Spring Boot 的 GraphQlSourceBuilderCustomizer
使用运行时布线对其进行配置
import java.util.Collections;
import java.util.List;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class NamespaceConfiguration {
@Bean
public GraphQlSourceBuilderCustomizer customizer() {
List<String> queryWrappers = List.of("music", "users"); (1)
return (sourceBuilder) -> sourceBuilder.configureRuntimeWiring((wiringBuilder) ->
queryWrappers.forEach((field) -> wiringBuilder.type("Query",
(builder) -> builder.dataFetcher(field, (env) -> Collections.emptyMap()))) (2)
);
}
}
1 | 列出“Query”类型的全部包装类型 |
2 | 为每个数据提取器手动声明数据提取器,返回一个空映射 |