注解控制器
Spring for GraphQL 提供了一个基于注解的编程模型,其中 @Controller 组件使用注解来声明具有灵活方法签名的处理器方法,以获取特定 GraphQL 字段的数据。例如:
@Controller
public class GreetingController {
@QueryMapping 1
public String hello() { 2
return "Hello, world!";
}
}
将此方法绑定到查询,即 Query 类型下的一个字段。
如果注解上未声明,则根据方法名确定查询。
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 处理器方法具有灵活的签名,可以从一系列方法参数和返回值中进行选择。
方法参数
模式映射处理程序方法可以包含以下任意方法参数:
| 方法参数 | 描述 |
|---|---|
@Argument | 用于访问绑定到更高级别类型化对象的命名字段参数。 参见 @Argument。 |
@Argument Map<String, Object> | 用于访问原始参数值。 参见 @Argument。 |
ArgumentValue | 用于访问绑定到更高级别类型化对象的命名字段参数,同时包含一个标志,用于指示输入参数是被省略还是设置为 null。参见 ArgumentValue。 |
@Arguments | 用于访问绑定到更高级别类型化对象的所有字段参数。 参见 @Arguments。 |
@Arguments Map<String, Object> | 用于访问参数的原始映射。 |
@ProjectedPayload Interface | 通过投影接口访问字段参数。 参见 @ProjectedPayload Interface。 |
| "Source" | 用于访问字段的源(即父级/容器)实例。 参见 Source。 |
Subrange 和 ScrollSubrange | 用于访问分页参数。 参见 Pagination、Scroll、Subrange。 |
Sort | 用于访问排序详细信息。 参见 Pagination、Sort。 |
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 mapping handler 方法可以返回:
| 返回值 | 描述 |
|---|---|
已解析的值 T | 直接解析的任何应用程序类型。 |
Mono<T> 和 Flux<T> | 用于异步值。 支持控制器方法和任何 DataFetcher,如 Reactive DataFetcher 中所述。 |
Kotlin suspend fun 和 Flow | 它们会自动适配为 Mono 和 Flux。 |
java.util.concurrent.Callable<T> | 用于异步生成值。要使此功能生效,AnnotatedControllerConfigurer 必须配置一个 Executor。在 Java 21+ 中,直接返回 T 就足够了。更多详情请阅读此表格后的段落。 |
graphql.execution.DataFetcherResult<P> | 其中 P 可以是上面列出的任何类型(T、Mono<T> 等)。这是 GraphQL Java 的“完整”返回值,不仅包含“数据”。对于使用“扩展”或“本地上下文”完成结果很有用。 |
在 Java 21+ 版本中,当 AnnotatedControllerConfigurer 配置了 Executor 时,具有阻塞方法签名的控制器方法会被异步调用。默认情况下,如果一个控制器方法不返回异步类型(例如 Flux、Mono、CompletableFuture),并且也不是 Kotlin 挂起函数,则会被视为阻塞方法。你可以在 AnnotatedControllerConfigurer 上配置一个阻塞控制器方法的 Predicate,以帮助确定哪些方法应被视为阻塞方法。
当属性 spring.threads.virtual.enabled 被设置时,Spring for GraphQL 的 Spring Boot starter 会自动为 AnnotatedControllerConfigurer 配置一个用于虚拟线程的 Executor。
接口模式映射
当一个控制器方法映射到模式接口字段时,默认情况下,该映射会被替换为多个映射,每个映射对应实现该接口的每个模式对象类型。这允许使用一个控制器方法来处理所有子类型。
例如,给定:
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 ActivityController {
@QueryMapping
public List<Activity> activities() {
// ...
}
@SchemaMapping
public User coordinator(Activity activity) {
// Called for any Activity subtype
}
}
如有必要,您可以接管个别子类型的映射:
@Controller
public class ActivityController {
@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 注解可以将参数绑定到目标对象并注入到处理器方法中。如果目标对象不是简单的 Java 类型,则通过主数据构造函数执行绑定,并通过使用嵌套的参数值来调用嵌套目标对象的构造函数,从而递归地重复此过程。例如:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
}
或者,也可以通过先调用默认构造函数,然后在目标对象上通过 setter 方法应用参数值来完成绑定。
如果目标对象没有 setter 方法,你可以配置参数绑定器以回退到通过直接字段访问进行绑定,详见参数绑定。
默认情况下,如果方法参数名称可用(例如在 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 版本之后,@Argument Map<String, Object> 总是返回原始参数值,该值基于参数名称或注解中的名称。如需访问完整的参数映射,请改用 @Arguments。
参数绑定
@Argument 绑定的支持由 GraphQlArgumentBinder 提供。该类负责从 GraphQL 参数值创建并填充目标对象。
参数绑定支持多种自定义选项:
-
nameResolver— 自定义 GraphQL 参数名到对象属性的映射,可用于处理命名约定(例如使用连字符"-"的情况)。 -
fallBackOnDirectFieldAccess— 当目标对象未使用访问器方法时,回退到直接字段访问。 -
conversionService— 在需要类型转换时使用。
要自定义参数绑定,请使用 AnnotatedControllerConfigurer 上的专用属性来设置 GraphQlArgumentBinder.Options。
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 方法进行初始化,包括作为嵌套在顶层对象下任意层级对象中的字段。
在客户端也通过专门的 Jackson 模块支持此功能,详见客户端的 ArgumentValue 支持章节。
@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) {
// ...
}
}
source 方法参数同样有助于确定映射的类型名称。如果 Java 类的简单名称与 GraphQL 类型匹配,则无需在 @SchemaMapping 注解中显式指定类型名称。
一个 @BatchMapping 处理器方法可以根据给定的源/父书籍对象列表,批量加载查询中的所有作者。
Subrange
当 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 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。你可以利用异常处理链来决定如何向客户端呈现该异常,将其转换为包含在 GraphQL 响应中的错误。
除了 @Valid,你也可以使用 Spring 的 @Validated,它允许指定验证组。
Bean validation 对于 @Argument、@Arguments 和 @ProjectedPayload 方法参数非常有用,但更普遍地适用于任何方法参数。
验证与 Kotlin 协程
Hibernate Validator 与 Kotlin 协程方法不兼容,在检查其方法参数时会失败。请参阅 spring-projects/spring-graphql#344 (comment) 了解相关问题的链接和推荐的解决方案。
HTTP 头部
要从控制器方法访问HTTP头部,我们建议使用 WebGraphQlInterceptor 将感兴趣的HTTP头部复制到 GraphQLContext 中,这样任何 DataFetcher 以及带有 @GraphQlContext 注解的控制器方法都可以访问这些头部信息。
内置的 HttpRequestHeaderInterceptor 可用于此目的。详见 Web 拦截器。
本地上下文
主要的 GraphQlContext 在整个查询过程中是全局的,可用于存储和检索横切关注点的上下文数据,例如可观测性、安全性等。有时,您可能希望向子字段的数据获取器传递额外信息,同时避免污染主上下文。对于此类用例,您应考虑使用局部 GraphQLContext,因为它仅包含在数据获取操作的一个子集中。
控制器方法可以通过返回一个包含已解析数据和新上下文的 DataFetcherResult<T> 来提供本地上下文:
@Controller
public class LocalContextBookController {
@QueryMapping
public DataFetcherResult<Book> bookById(@Argument Long id) {
// Our controller method must return a DataFetcherResult
DataFetcherResult.Builder<Book> resultBuilder = DataFetcherResult.newResult();
BookAndAuthor bookAndAuthor = this.fetchBookAndAuthorById(id);
// Create a new local context and store the author value
GraphQLContext localContext = GraphQLContext.getDefault()
.put("author", bookAndAuthor.author);
return resultBuilder
.data(bookAndAuthor.book)
.localContext(localContext)
.build();
}
@SchemaMapping
public List<Book> related(Book book, @LocalContextValue Author author) {
List<Book> relatedBooks = new ArrayList<>();
relatedBooks.addAll(fetchBooksByAuthor(author));
relatedBooks.addAll(fetchSimilarBooks(book));
return relatedBooks;
}
如果你想查看更详细的示例和关于使用此功能的讨论,请参阅 graphql-java 文档中的“通过前瞻构建高效数据获取器”部分。
@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 字段上,该字段仅根据其源/父 Book 实例委托给作者的 DataLoader。
要作为唯一键使用,Book 必须实现 hashcode 和 equals 方法。
默认情况下,字段名默认为方法名,而类型名默认为输入 List 元素类型的简单类名。两者都可以通过注解属性进行自定义。类型名也可以从类级别的 @SchemaMapping 继承。
@BatchMapping 在简单场景下是一个有效的“快捷方式”,因为使用 BatchLoaderRegistry 会引入过多的样板代码,而实际收益不大。但如果你的用例需要更多关于父级 @SchemaMapping 调用的信息(例如 @Argument 或上下文数据),比如用于筛选要加载的实体,那么就必须使用 BatchLoaderRegistry。请参考 批量加载方案 部分。
方法参数
批量映射方法支持以下参数:
| 方法参数 | 描述 |
|---|---|
List<K> | 源/父对象。 |
java.security.Principal | 如果可用,从 Spring Security 上下文中获取。 |
@ContextValue | 用于访问来自 BatchLoaderEnvironment 的 GraphQLContext 中的值,该上下文与 DataFetchingEnvironment 中的上下文相同。 |
GraphQLContext | 用于访问来自 BatchLoaderEnvironment 的上下文,该上下文与 DataFetchingEnvironment 中的上下文相同。 |
BatchLoaderEnvironment | GraphQL Java 中 org.dataloader.BatchLoaderWithContext 可用的环境。BatchLoaderEnvironment 的 context 属性返回与通过 DataFetchingEnvironment 对 @SchemaMapping 方法可用的相同的 GraphQLContext 实例。BatchLoaderEnvironment 的 keyContexts 属性返回为每个键调用 DataLoader 时,从 DataFetchingEnvironment 获取的本地上下文。 |
返回值
批处理映射方法可以返回:
| 返回类型 | 描述 |
|---|---|
Mono<Map<K,V>> | 一个以父对象为键、批量加载对象为值的映射。 |
Flux<V> | 批量加载对象的序列,其顺序必须与传入方法的源/父对象顺序相同。 |
Map<K,V>、Collection<V> | 命令式变体,例如无需进行远程调用。 |
Callable<Map<K,V>>、Callable<Collection<V>> | 需要异步调用的命令式变体。要使此功能生效,AnnotatedControllerConfigurer 必须配置一个 Executor。 |
Kotlin 协程 Map<K,V>、Kotlin Flow<K,V> | 适配为 Mono<Map<K,V> 和 Flux<V>。 |
在 Java 21+ 版本中,当 AnnotatedControllerConfigurer 配置了 Executor 时,具有阻塞方法签名的控制器方法会被异步调用。默认情况下,如果一个控制器方法不返回异步类型(例如 Flux、Mono、CompletableFuture),并且也不是 Kotlin 挂起函数,则被视为阻塞方法。你可以在 AnnotatedControllerConfigurer 上配置一个阻塞控制器方法的 Predicate,以帮助确定哪些方法被视为阻塞方法。
当属性 spring.threads.virtual.enabled 被设置时,Spring for GraphQL 的 Spring Boot starter 会自动为 AnnotatedControllerConfigurer 配置一个用于虚拟线程的 Executor。
接口 批量映射
与接口模式映射的情况类似,当批量映射方法被映射到模式接口字段时,该映射会被替换为多个映射,每个映射对应实现该接口的每个模式对象类型。
这意味着,给定以下内容:
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 方法,通过灵活的方法签名来处理数据获取过程中的异常。当在控制器中声明时,异常处理方法适用于同一控制器中的异常:
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.GraphQlExceptionHandler;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindException;
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
return ...
}
@GraphQlExceptionHandler
public GraphQLError handle(GraphqlErrorBuilder<?> errorBuilder, BindException ex) {
return errorBuilder
.errorType(ErrorType.BAD_REQUEST)
.message(ex.getMessage())
.build();
}
}
当在 @ControllerAdvice 中声明时,异常处理方法将应用于所有控制器:
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import org.springframework.graphql.data.method.annotation.GraphQlExceptionHandler;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ControllerAdvice
public class GlobalExceptionHandler {
@GraphQlExceptionHandler
public GraphQLError handle(GraphqlErrorBuilder<?> errorBuilder, BindException ex) {
return errorBuilder
.errorType(ErrorType.BAD_REQUEST)
.message(ex.getMessage())
.build();
}
}
如上例所示,你应该通过注入 GraphQlErrorBuilder 到方法签名中来构建错误,因为它已通过当前的 DataFetchingEnvironment 进行了配置。
通过 @GraphQlExceptionHandler 方法实现的异常处理会自动应用于控制器调用。若要处理其他不基于控制器方法的 graphql.schema.DataFetcher 实现中的异常,可从 AnnotatedControllerConfigurer 获取 DataFetcherExceptionResolver,并将其作为 DataFetcherExceptionResolver 注册到 GraphQlSource.Builder 中。
方法签名
异常处理器方法支持灵活的方法签名,其方法参数从 DataFetchingEnvironment 中解析,并与 @SchemaMapping 方法 的参数相匹配。
支持的返回类型如下:
| 返回类型 | 描述 |
|---|---|
graphql.GraphQLError | 将异常解析为单个字段错误。 |
Collection<GraphQLError> | 将异常解析为多个字段错误。 |
void | 解析异常但不返回响应错误。 |
Object | 将异常解析为单个错误、多个错误或无错误。返回值必须是 GraphQLError、Collection<GraphQLError> 或 null。 |
Mono<T> | 用于异步解析,其中 <T> 是支持的同步返回类型之一。 |
命名空间
在模式层面,查询和变更操作直接定义在 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"));
}
}
使用
@SchemaMapping注解和typeName属性标注控制器,以避免在方法上重复声明为 "music" 命名空间定义
@QueryMapping"music" 查询返回一个 "空" 记录,但也可以返回一个空映射
查询现在被声明为 "MusicQueries" 类型下的字段
除了在控制器中显式声明包装类型("MusicQueries"、"UserQueries"),您还可以选择通过 Spring Boot 的 GraphQlSourceBuilderCustomizer 配合运行时配置来设置它们:
import java.util.Collections;
import java.util.List;
import org.springframework.boot.graphql.autoconfigure.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
);
}
}
列出 "Query" 类型的所有包装器类型
为每个包装器类型手动声明数据获取器,返回一个空的 Map