带注解的控制器

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 beans 定义为标准的 Spring bean 定义。@Controller 构造型(stereotype)允许自动检测,与 Spring 对类路径上检测 @Controller@Component 类并为其自动注册 bean 定义的通用支持保持一致。它也充当带注解类的构造型,表示其在 GraphQL 应用中作为数据获取组件的作用。

AnnotatedControllerConfigurer 检测 @Controller beans 并通过 RuntimeWiring.Builder 将其带注解的处理方法注册为 DataFetcher。它是 RuntimeWiringConfigurer 的实现,可以添加到 GraphQlSource.BuilderBoot Starter 会自动将 AnnotatedControllerConfigurer 声明为一个 bean,并将所有 RuntimeWiringConfigurer beans 添加到 GraphQlSource.Builder,从而启用对带注解的 DataFetcher 的支持,请参阅 Boot starter 文档中的 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 处理方法具有灵活的签名,并且可以选择多种方法参数和返回值。

方法参数

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

方法参数 描述

@Argument

用于访问绑定到更高级、类型化对象的命名字段参数。

参见 @Argument

@Argument Map<String, Object>

用于访问原始参数值。

参见 @Argument

ArgumentValue

用于访问绑定到更高级、类型化对象的命名字段参数,并带有指示输入参数是被省略还是被设置为 null 的标志。

参见 ArgumentValue

@Arguments

用于访问绑定到更高级、类型化对象的所有字段参数。

参见 @Arguments

@Arguments Map<String, Object>

用于访问原始参数映射。

@ProjectedPayload 接口

通过投影接口访问字段参数。

参见 @ProjectedPayload 接口

"源"

用于访问字段的源(即父/容器)实例。

参见

SubrangeScrollSubrange

用于访问分页参数。

参见 分页滚动Subrange

Sort

用于访问排序详情。

参见 分页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 映射处理方法可以返回:

  • 任何类型的解析值。

  • MonoFlux 用于异步值。支持控制器方法和任何 DataFetcher,如 响应式 DataFetcher 中所述。

  • Kotlin coroutine 和 Flow 被适配为 MonoFlux

  • java.util.concurrent.Callable 用于异步生成值。为此,AnnotatedControllerConfigurer 必须配置一个 Executor

在 Java 21+ 上,当 AnnotatedControllerConfigurer 配置了 Executor 后,具有阻塞方法签名的控制器方法将异步调用。默认情况下,如果控制器方法不返回 FluxMonoCompletableFuture 等异步类型,并且也不是 Kotlin suspending 函数,则被视为阻塞方法。你可以在 AnnotatedControllerConfigurer 上配置一个阻塞控制器方法 Predicate,以帮助确定哪些方法被视为阻塞方法。

Spring for GraphQL 的 Spring Boot starter 在设置属性 spring.threads.virtual.enabled 时,会自动为 AnnotatedControllerConfigurer 配置一个用于虚拟线程的 Executor

接口 Schema 映射

当控制器方法映射到 schema 接口字段时,默认情况下该映射会被替换为多个映射,每个实现该接口的 schema 对象类型对应一个映射。这允许对所有子类型使用同一个控制器方法。

例如,给定:

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 schema 级别指定,并由 GraphQL Java enforced。

如果绑定失败,将抛出 BindException,其中包含绑定问题作为字段错误,每个错误的 field 是问题发生的参数路径。

你可以将 @ArgumentMap<String, Object> 参数一起使用,以获取参数的原始值。例如:

@Controller
public class BookController {

	@MutationMapping
	public Book addBook(@Argument Map<String, Object> bookInput) {
		// ...
	}
}
在 1.2 版本之前,如果注解未指定名称,@Argument Map<String, Object> 返回完整的参数映射。在 1.2 版本之后,@ArgumentMap<String, Object> 总是返回原始参数值,与注解中指定的名称或参数名称匹配。要访问完整的参数映射,请改用 @Arguments

ArgumentValue

默认情况下,GraphQL 中的输入参数是可为空且可选的,这意味着参数可以设置为 null 字面量,或者根本不提供。这种区别对于使用 mutation 进行部分更新非常有用,因为基础数据可能因此被设置为 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 的属性。

你可以将 @ArgumentsMap<String, Object> 参数一起使用,以获取所有参数值的原始映射。

@ProjectedPayload 接口

作为使用带有 @Argument 的完整对象的替代方案,你也可以使用投影接口通过明确定义的最小接口访问 GraphQL 请求参数。当 Spring Data 在类路径上时,参数投影由 Spring Data 的接口投影提供。

要利用这一点,创建一个带有 @ProjectedPayload 注解的接口,并将其声明为控制器方法的参数。如果参数带有 @Argument 注解,则它应用于 DataFetchingEnvironment.getArguments() map 中的单个参数。如果未声明 @Argument,则投影作用于完整参数 map 中的顶级参数。

例如:

@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 处理方法可以批量加载查询的所有作者。

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 验证 支持。通常,该 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 (comment)

@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 实例。

作为唯一键使用时,Book 必须实现 hashcodeequals

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

方法参数

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

方法参数 描述

List<K>

源/父对象。

java.security.Principal

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

@ContextValue

用于从 BatchLoaderEnvironmentGraphQLContext 访问值,该上下文与 DataFetchingEnvironment 中的上下文相同。

GraphQLContext

用于从 BatchLoaderEnvironment 访问上下文,该上下文与 DataFetchingEnvironment 中的上下文相同。

BatchLoaderEnvironment

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

BatchLoaderEnvironmentcontext 属性返回与 @SchemaMapping 方法通过 DataFetchingEnvironment 可用的 GraphQLContext 实例相同的实例。

BatchLoaderEnvironmentkeyContexts 属性返回在为每个键调用 DataLoader 时从 DataFetchingEnvironment 获取的 localContext。

返回值

批量映射方法可以返回:

返回类型 描述

Mono<Map<K,V>>

一个以父对象为键,批量加载对象为值的 Map。

Flux<V>

批量加载对象的序列,必须与传递给方法的源/父对象的顺序相同。

Map<K,V>, Collection<V>

命令式变体,例如不需要进行远程调用。

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

要异步调用的命令式变体。为此,AnnotatedControllerConfigurer 必须配置一个 Executor

带有 Map<K,V> 的 Kotlin Coroutine,Kotlin Flow<K,V>

适配为 Mono<Map<K,V>Flux<V>

在 Java 21+ 上,当 AnnotatedControllerConfigurer 配置了 Executor 后,具有阻塞方法签名的控制器方法将异步调用。默认情况下,如果控制器方法不返回 FluxMonoCompletableFuture 等异步类型,并且也不是 Kotlin suspending 函数,则被视为阻塞方法。你可以在 AnnotatedControllerConfigurer 上配置一个阻塞控制器方法 Predicate,以帮助确定哪些方法被视为阻塞方法。

Spring for GraphQL 的 Spring Boot starter 在设置属性 spring.threads.virtual.enabled 时,会自动为 AnnotatedControllerConfigurer 配置一个用于虚拟线程的 Executor

接口批量映射

接口 Schema 映射 的情况一样,当批量映射方法映射到 schema 接口字段时,该映射会被替换为多个映射,每个实现该接口的 schema 对象类型对应一个映射。

这意味着,给定以下内容:

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 方法的参数匹配。

支持的返回类型如下:

返回类型 描述

graphql.GraphQLError

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

Collection<GraphQLError>

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

void

解析异常但不包含响应错误。

Object

将异常解析为单个错误、多个错误或没有错误。返回值必须是 GraphQLErrorCollection<GraphQLError>null

Mono<T>

用于异步解析,其中 <T> 是受支持的同步返回类型之一。

命名空间

在 schema 级别,查询和 mutation 操作直接在 QueryMutation 类型下定义。丰富的 GraphQL API 可以在这些类型下定义数十个操作,这使得探索 API 和分离关注点变得更加困难。你可以选择在 GraphQL schema 中定义命名空间。虽然这种方法有一些需要注意的地方,但你可以使用 Spring for GraphQL 带注解的控制器实现这种模式。

通过命名空间,你的 GraphQL schema 可以例如将查询操作嵌套在顶级类型下,而不是直接列在 Query 下。这里,我们将定义 MusicQueriesUserQueries 类型并将它们在 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 使用 @SchemaMappingtypeName 属性注解控制器,以避免在方法上重复:
2 为 "music" 命名空间定义一个 @QueryMapping
3 “music” 查询返回一个“空”记录,但也可以返回一个空 map:
4 查询现在被声明为 "MusicQueries" 类型下的字段。

你可以选择使用 Spring Boot 通过 GraphQlSourceBuilderCustomizer 在运行时配置中配置它们,而不是在控制器中显式声明包装类型("MusicQueries"、"UserQueries"):

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 手动为每个包装类型声明数据获取器,并返回一个空 Map: