带注解的控制器

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

@Controller
public class GreetingController {

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

}
1 将此方法绑定到一个查询,即Query类型下的一个字段。
2 若在注解中未声明,则通过方法名确定查询。

Spring for GraphQL 使用 RuntimeWiring.Builder 注册上述处理器方法作为名为 "hello" 的查询的 graphql.schema.DataFetcherspring-doc.cadn.net.cn

声明

您可以用@Controller定义标准的Spring bean定义。@Controller注解允许自动检测,与Spring对在类路径上检测@Controller@Component类的一般支持以及为它们自动注册bean定义相一致。它还作为带有注解的类的一个标签,表示其在GraphQL应用程序中的作用是数据获取组件。spring-doc.cadn.net.cn

AnnotatedControllerConfigurer 检测 @Controller 个 Bean,并通过 RuntimeWiring.Builder 将其注解的处理方法注册为 DataFetcher。它是 RuntimeWiringConfigurer 的实现,可以添加到 GraphQlSource.Builder 中。 Boot Starter 会自动将 AnnotatedControllerConfigurer 声明为 Bean,并将所有 RuntimeWiringConfigurer Bean 添加到 GraphQlSource.Builder 中,从而启用对注解 DataFetcher 的支持,请参阅 Boot Starter 文档中的 GraphQL RuntimeWiring 部分。spring-doc.cadn.net.cn

@SchemaMapping

`0` 注解将处理器方法映射到GraphQL模式中的一个字段,并声明它是该字段的 `DataFetcher`。此注解可以指定父类型名称和字段名称:spring-doc.cadn.net.cn

@Controller
public class BookController {

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

`0` 注解也可以省略这些属性,在这种情况下,字段名称默认为方法名,而类型名称默认为注入到方法中的源/父对象的简单类名。例如,下面的内容默认类型为 "Book" 和字段为 "author":spring-doc.cadn.net.cn

@Controller
public class BookController {

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

`0` 注解可以在类级别声明,以指定该类中所有处理器方法的默认类型名称。spring-doc.cadn.net.cn

@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 类型字段的快捷注释。例如:spring-doc.cadn.net.cn

@Controller
public class BookController {

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

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

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

@SchemaMapping 处理方法具有灵活的签名,并可以从一系列方法参数和返回值中进行选择。spring-doc.cadn.net.cn

方法参数

Schema 映射处理方法可以有以下任意方法参数:spring-doc.cadn.net.cn

方法参数 描述

@Argumentspring-doc.cadn.net.cn

对绑定到更高层次、类型化对象的命名字段参数进行访问。spring-doc.cadn.net.cn

请参阅 @Argumentspring-doc.cadn.net.cn

@Argument Map<String, Object>spring-doc.cadn.net.cn

对于原始参数值的访问。spring-doc.cadn.net.cn

请参阅 @Argumentspring-doc.cadn.net.cn

ArgumentValuespring-doc.cadn.net.cn

对于绑定到更高层次的类型化对象且带有指示输入参数是否省略(未设置)而非设置为null标志的命名字段参数的访问权限。spring-doc.cadn.net.cn

请参阅 ArgumentValuespring-doc.cadn.net.cn

@Argumentsspring-doc.cadn.net.cn

对所有绑定到更高层级类型对象的字段参数的访问。spring-doc.cadn.net.cn

请参阅 @Argumentsspring-doc.cadn.net.cn

@Arguments Map<String, Object>spring-doc.cadn.net.cn

对于参数的原始映射的访问权限。spring-doc.cadn.net.cn

@ProjectedPayload 接口spring-doc.cadn.net.cn

通过项目接口访问字段参数。spring-doc.cadn.net.cn

查看 @ProjectedPayload 接口spring-doc.cadn.net.cn

"源"spring-doc.cadn.net.cn

对于字段的源(即父/容器)实例的访问。spring-doc.cadn.net.cn

源码spring-doc.cadn.net.cn

SubrangeScrollSubrangespring-doc.cadn.net.cn

用于分页参数的访问。spring-doc.cadn.net.cn

请参阅 分页滚动Subrangespring-doc.cadn.net.cn

Sortspring-doc.cadn.net.cn

对于排序详细信息的访问。spring-doc.cadn.net.cn

请参阅 分页Sortspring-doc.cadn.net.cn

DataLoaderspring-doc.cadn.net.cn

对于访问DataLoaderDataLoaderRegistry中的权限。spring-doc.cadn.net.cn

请参阅 DataLoaderspring-doc.cadn.net.cn

@ContextValuespring-doc.cadn.net.cn

从主GraphQLContextDataFetchingEnvironment中访问一个属性。spring-doc.cadn.net.cn

@LocalContextValuespring-doc.cadn.net.cn

DataFetchingEnvironment中访问本地GraphQLContext的属性。spring-doc.cadn.net.cn

GraphQLContextspring-doc.cadn.net.cn

DataFetchingEnvironment访问上下文。spring-doc.cadn.net.cn

java.security.Principalspring-doc.cadn.net.cn

从 Spring Security上下文获取(如果可用的话)。spring-doc.cadn.net.cn

@AuthenticationPrincipalspring-doc.cadn.net.cn

从Spring Security上下文中访问Authentication#getPrincipal()spring-doc.cadn.net.cn

DataFetchingFieldSelectionSetspring-doc.cadn.net.cn

通过DataFetchingEnvironment可以访问查询的选择集。spring-doc.cadn.net.cn

Locale, Optional<Locale>spring-doc.cadn.net.cn

DataFetchingEnvironment访问Localespring-doc.cadn.net.cn

DataFetchingEnvironmentspring-doc.cadn.net.cn

直接访问底层的DataFetchingEnvironmentspring-doc.cadn.net.cn

返回值

Schema mapping handler方法可以返回:spring-doc.cadn.net.cn

返回值 描述

已解析的值 Tspring-doc.cadn.net.cn

任何应用程序类型直接解析。spring-doc.cadn.net.cn

Mono<T>Flux<T>spring-doc.cadn.net.cn

异步值(或多个值)。spring-doc.cadn.net.cn

支持控制器方法以及如 响应式 DataFetcher 中所述适用于任何 DataFetcherspring-doc.cadn.net.cn

Kotlin suspend funFlowspring-doc.cadn.net.cn

它们会自动适应MonoFluxspring-doc.cadn.net.cn

java.util.concurrent.Callable<T>spring-doc.cadn.net.cn

异步生产值。 为了实现这一点,AnnotatedControllerConfigurer 必须配置为具有 Executor。 在 Java 21 及以上版本中,直接返回 T 即可。更多详情,请参阅此表下方的段落。spring-doc.cadn.net.cn

graphql.execution.DataFetcherResult<P>spring-doc.cadn.net.cn

使用 P 可以是上述列出的任意类型(T, Mono<T> 等)。spring-doc.cadn.net.cn

这便是GraphQL Java的“完整”返回值,不仅包含“数据”。 可用于完成结果中的“扩展”或一个本地上下文spring-doc.cadn.net.cn

在 Java 21+ 中,当配置了AnnotatedControllerConfigurerExecutor时,具有阻塞方法签名的控制器方法将异步调用。默认情况下,如果一个控制器方法不返回诸如FluxMonoCompletableFuture等异步类型,并且也不是 Kotlin 挂起函数,则该方法被视为阻塞方法。你可以通过在Predicate上配置AnnotatedControllerConfigurer来帮助确定哪些方法被认为是阻塞的。spring-doc.cadn.net.cn

The Spring Boot starter for Spring for GraphQL automatically configures AnnotatedControllerConfigurer with an Executor for virtual threads when the property spring.threads.virtual.enabled is set.

接口架构映射

当控制器方法被映射到模式接口字段时,默认情况下该映射会被替换为多个映射,一个对应于实现该接口的每个模式对象类型。这使得可以使用一个控制器方法来处理所有子类型。spring-doc.cadn.net.cn

例如,给定:spring-doc.cadn.net.cn

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!
}

你可以编写一个控制器像这样:spring-doc.cadn.net.cn

@Controller
public class ActivityController {

	@QueryMapping
	public List<Activity> activities() {
		// ...
	}

	@SchemaMapping
	public User coordinator(Activity activity) {
		// Called for any Activity subtype
	}

}

如果必要,您可以为个别子类型接管映射:spring-doc.cadn.net.cn

@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的值集合。spring-doc.cadn.net.cn

使用 @Argument 注解将一个参数绑定到目标对象,并将其注入处理方法中。如果目标对象不是简单的 Java 类型,则通过主数据构造函数进行绑定,然后递归地使用嵌套的参数值调用嵌套目标对象的构造函数。例如:spring-doc.cadn.net.cn

@Controller
public class BookController {

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

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

alternatively, binding 可以通过首先调用默认构造函数,然后在目标对象上通过 setter 方法应用参数值来完成。spring-doc.cadn.net.cn

如果目标对象没有 setter 方法,您可以配置参数绑定器以回退到通过直接字段访问进行绑定,请参见 参数绑定

默认情况下,如果方法参数名称可用,例如在Java 8+的编译器标志 -parameters 或通过编译器的调试信息,将使用该名称查找参数。如有必要,可以通过注解自定义名称,例如 @Argument("bookInput")spring-doc.cadn.net.cn

注意,@Argument 注解没有 'required' 标记,也不能指定默认值。这些都可以在GraphQL模式级别指定,并且由GraphQL Java强制执行。spring-doc.cadn.net.cn

如果绑定失败,会抛出一个 BindException 并积累绑定问题作为字段错误,在每个错误的 field 是问题发生的位置。spring-doc.cadn.net.cn

您可以使用@Argument和一个Map<String, Object>参数来获取参数的原始值。例如:spring-doc.cadn.net.cn

@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参数值创建并填充目标对象。spring-doc.cadn.net.cn

Argument binding 支持几种自定义选项:spring-doc.cadn.net.cn

  • nameResolver — 自定义GraphQL参数名称与Object属性之间的映射,这在处理如使用"-"这样的命名规范时非常有用。spring-doc.cadn.net.cn

  • fallBackOnDirectFieldAccess — 如果目标对象不使用访问器方法,则回退到直接字段访问。spring-doc.cadn.net.cn

  • conversionService — 用于需要时进行类型转换。spring-doc.cadn.net.cn

要自定义参数绑定,请在AnnotatedControllerConfigurer上使用专用属性来设置GraphQlArgumentBinder.Optionsspring-doc.cadn.net.cn

ArgumentValue

默认情况下,GraphQL 中的输入参数是可空且可选的,这意味着参数可以设置为 null 字面量,或者完全不提供。这种区分对于使用突变进行部分更新非常有用,因为底层数据也可以相应地设置为 null 或保持不变。当使用 @Argument 时,无法做出此类区分,因为在两种情况下您都会得到 null 或空的 Optionalspring-doc.cadn.net.cn

如果要了解是否完全没有提供某个值,您可以声明一个ArgumentValue方法参数,它是一个简单的容器用于存储结果值,并且可以与一个标志一起使用来表明输入参数是否完全省略。您可以在不使用@Argument的情况下使用这种方法参数,这时会根据方法参数名称确定参数名称;或者您可以与@Argument一起使用以指定参数名称。spring-doc.cadn.net.cn

@Controller
public class BookController {

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

ArgumentValue 也支持作为 @Argument 方法参数的对象结构中的一个字段,可以通过构造函数参数或 setter 方法初始化,包括在顶级对象下方的任何层级嵌套的对象中作为字段。spring-doc.cadn.net.cn

这也通过专用的 Jackson 模块在客户端得到支持, 请参阅 ArgumentValue 对客户端的支持 部分。spring-doc.cadn.net.cn

@Arguments

使用@Arguments注解,如果你希望将完整的参数映射绑定到单个目标对象上,而不是使用@Argument注解,后者只绑定特定命名的参数。spring-doc.cadn.net.cn

例如,@Argument BookInput bookInput 使用参数 "bookInput" 的值来初始化 BookInput,而在 @Arguments 中使用完整的参数映射,在这种情况下,顶级参数绑定到 BookInput 属性。spring-doc.cadn.net.cn

您可以使用@Arguments和一个Map<String, Object>参数,以获取所有参数值的原始映射。spring-doc.cadn.net.cn

@ProjectedPayload接口

作为使用完整 @Argument 对象的替代方案, 您也可以通过定义明确的最小接口,使用投影接口来访问 GraphQL 请求参数。 当类路径中存在 Spring Data 时, Spring Data 的接口投影 将提供参数投影功能。spring-doc.cadn.net.cn

要使用此功能,请创建一个带有@ProjectedPayload注解的接口,并将其声明为控制器方法参数。如果该参数带有@Argument注解,那么它将应用于DataFetchingEnvironment.getArguments()映射中的个别参数。如果没有声明@Argument,则投影作用于完整参数映射中的顶级参数。spring-doc.cadn.net.cn

@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中,`0`提供了对字段的源(即父级/容器)实例的访问。要访问这一点,请声明一个预期目标类型的方法参数。spring-doc.cadn.net.cn

@Controller
public class BookController {

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

源方法参数也有助于确定映射的类型名称。 如果 Java 类的简单名称与 GraphQL 类型匹配,则无需在 @Autowired 注解中显式指定类型名称。spring-doc.cadn.net.cn

一个 @BatchMapping 处理方法可以根据源/父书籍对象列表,批量加载查询的所有作者。spring-doc.cadn.net.cn

Subrange

当 Spring 配置中存在 CursorStrategy 类型的 Bean 时, 控制器方法支持一个 Subrange<P> 参数,其中 <P> 是从光标位置转换而来的相对位置。 对于 Spring Data,ScrollSubrange 暴露了 ScrollPosition。 例如:spring-doc.cadn.net.cn

@Controller
public class BookController {

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

}

分页以了解分页和内置机制的概述。spring-doc.cadn.net.cn

Sort

当Spring配置中存在SortStrategy bean时,控制器方法支持Sort作为方法参数。例如:spring-doc.cadn.net.cn

@Controller
public class BookController {

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

}

DataLoader

当您为实体注册批量加载功能时,如在 批量加载 中所述,可以通过声明类型为 DataLoader 的方法参数来访问该实体的 DataLoader ,并使用它来加载实体:spring-doc.cadn.net.cn

@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 方法参数解析器还会尝试使用方法参数名称作为键,但通常情况下这不需要。spring-doc.cadn.net.cn

注意,对于许多加载相关实体的情况,其中@SchemaMapping简单地委托给DataLoader,您可以通过使用下一节中所述的@BatchMapping方法来减少样板代码。spring-doc.cadn.net.cn

验证

当找到一个javax.validation.Validator bean时,AnnotatedControllerConfigurer会启用对注解控制器方法的 Bean Validation 支持。通常,该 bean 的类型为 LocalValidatorFactoryBeanspring-doc.cadn.net.cn

Bean验证让你可以在类型上声明约束:spring-doc.cadn.net.cn

public class BookInput {

	@NotNull
	private String title;

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

然后可以使用@ModelAttribute注解控制器方法参数,在方法调用前对其进行验证:spring-doc.cadn.net.cn

@Controller
public class BookController {

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

如果在验证过程中发生错误,会抛出一个 ConstraintViolationException。 你可以通过使用 异常链(Exceptions) 来决定如何将该错误呈现给客户端,将其转换为GraphQL响应中包含的错误信息。spring-doc.cadn.net.cn

除了使用@Valid,你还可以使用Spring的@Validated来指定验证组。

Bean 验证适用于 @Argument@Arguments@ProjectedPayload 方法参数,但也更广泛地适用于任何方法参数。spring-doc.cadn.net.cn

验证与Kotlin协程

Hibernate Validator 与 Kotlin 协程方法不兼容,并在反查其方法参数时失败。请参阅 spring-projects/spring-graphql#344 (评论) 以获取相关问题的链接和建议的工作around。spring-doc.cadn.net.cn

HTTP 头部

要从控制器方法访问HTTP标头,请推荐使用一个WebGraphQlInterceptor 来复制感兴趣的HTTP标头到GraphQLContext中,在那里它们可以被任何DataFetcher 以及注解控制器方法作为@GraphQlContext参数。spring-doc.cadn.net.cn

存在一个内置的HttpRequestHeaderInterceptor用于帮助。 请参见Web 拦截器spring-doc.cadn.net.cn

本地上下文

The main GraphQlContext 是全局的,适用于整个查询,并可用于存储和检索用于可观测性、安全性和其他方面的跨切面上下文数据。 有时您希望将附加信息传递给子字段的数据获取器以避免污染主要上下文。 在这种情况下,您应该考虑使用局部 GraphQLContext,因为它是局限于一部分数据获取操作的。spring-doc.cadn.net.cn

控制器方法可以通过返回一个持有解析数据和新上下文的DataFetcherResult<T>来贡献本地上下文:spring-doc.cadn.net.cn

@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;
	}

@BatchMapping

批量加载通过使用org.dataloader.DataLoader来延迟单个实体实例的加载,从而可以一起加载。例如:spring-doc.cadn.net.cn

@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());
	}

}

对于上面展示的直接加载关联实体的情况,`0` 方法除了委托给 `1` 方法之外不做其他事情。这是一些可以使用 `@BatchMapping` 方法避免的样板代码。例如:spring-doc.cadn.net.cn

@Controller
public class BookController {

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

上述内容在BatchLoaderRegistry中变成了一个批量加载功能,其中键是Book实例,加载的值为它们的作者。此外,DataFetcher还会透明地绑定到类型Bookauthor字段上,该字段仅仅委托给DataLoader来处理作者信息,给定其源/父级实例Bookspring-doc.cadn.net.cn

要作为唯一键使用,Book 必须实现 hashcodeequalsspring-doc.cadn.net.cn

默认情况下,字段名称会自动使用方法名,而类型名称则默认为输入List元素类型的简单类名。两者都可以通过注解属性进行自定义。类型名称也可以从类级别的@SchemaMapping继承。spring-doc.cadn.net.cn

@BatchMapping 是一种“快捷方式”,适用于使用 BatchLoaderRegistry 会添加过多繁琐代码而没有实际益处的简单情况。如果你的应用场景需要更多关于父级 @SchemaMapping 调用的信息(例如 @Argument 或上下文数据),比如用于过滤要加载的对象,这时必须使用 BatchLoaderRegistry。 请参阅 批量加载Recipes 部分。spring-doc.cadn.net.cn

方法参数

批处理映射方法支持以下参数:spring-doc.cadn.net.cn

方法参数 描述

List<K>spring-doc.cadn.net.cn

源/父对象。spring-doc.cadn.net.cn

java.security.Principalspring-doc.cadn.net.cn

来自 Spring Security 上下文(如果可用的话)。spring-doc.cadn.net.cn

@ContextValuespring-doc.cadn.net.cn

GraphQLContextBatchLoaderEnvironment访问一个值,这与来自DataFetchingEnvironment的上下文相同。spring-doc.cadn.net.cn

GraphQLContextspring-doc.cadn.net.cn

BatchLoaderEnvironment访问上下文, 这与DataFetchingEnvironment的上下文相同。spring-doc.cadn.net.cn

BatchLoaderEnvironmentspring-doc.cadn.net.cn

GraphQL Java为一个 org.dataloader.BatchLoaderWithContext 提供的环境。spring-doc.cadn.net.cn

context 属性的 BatchLoaderEnvironment 返回相同的 GraphQLContext 实例,该实例也可以通过 DataFetchingEnvironment 访问 @SchemaMapping 方法。spring-doc.cadn.net.cn

keyContexts 属性的 BatchLoaderEnvironment 返回的是 localContext 当调用 DataFetchingEnvironmentDataLoader 时为每个键获取到的。spring-doc.cadn.net.cn

返回值

批量映射方法可以返回:spring-doc.cadn.net.cn

返回类型 描述

Mono<Map<K,V>>spring-doc.cadn.net.cn

以父对象作为键,批量加载的对象作为值的映射。spring-doc.cadn.net.cn

Flux<V>spring-doc.cadn.net.cn

必须按与传递给方法的源/父对象相同顺序加载的一系列批处理对象。spring-doc.cadn.net.cn

Map<K,V>, Collection<V>spring-doc.cadn.net.cn

不使用远程调用的命令式变体。spring-doc.cadn.net.cn

Callable<Map<K,V>>, Callable<Collection<V>>spring-doc.cadn.net.cn

异步调用的命令式变体。为了使这一点生效, AnnotatedControllerConfigurer 必须配置一个 Executorspring-doc.cadn.net.cn

Kotlin 协程与 Map<K,V>,Kotlin Flow<K,V>spring-doc.cadn.net.cn

已适应为Mono<Map<K,V>Flux<V>spring-doc.cadn.net.cn

在 Java 21+ 中,当配置了AnnotatedControllerConfigurerExecutor时,具有阻塞方法签名的控制器方法将异步调用。默认情况下,如果一个控制器方法不返回诸如FluxMonoCompletableFuture等异步类型,并且也不是 Kotlin 挂起函数,则该方法被视为阻塞方法。你可以通过在Predicate上配置AnnotatedControllerConfigurer来帮助确定哪些方法被认为是阻塞的。spring-doc.cadn.net.cn

The Spring Boot starter for Spring for GraphQL automatically configures AnnotatedControllerConfigurer with an Executor for virtual threads when the property spring.threads.virtual.enabled is set.

接口批量映射

当批次映射方法被映射到模式接口字段时,就像在 接口Schema映射 中的情况一样, 这种映射会被替换为多个映射,每个模式对象类型都实现该接口就对应一个映射。spring-doc.cadn.net.cn

这意味着,给定以下内容:spring-doc.cadn.net.cn

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!
}

你可以编写一个控制器像这样:spring-doc.cadn.net.cn

@Controller
public class BookController {

	@QueryMapping
	public List<Activity> activities() {
		// ...
	}

	@BatchMapping
	Map<Activity, User> coordinator(List<Activity> activities) {
		// Called for all Activity subtypes
	}
}

如果必要,您可以为个别子类型接管映射:spring-doc.cadn.net.cn

@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方法来处理数据获取时的异常,并且可以通过灵活的方法签名进行自定义。当这些异常处理器方法声明在控制器中时,它们仅适用于同一控制器中的异常。spring-doc.cadn.net.cn

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中声明时,异常处理方法会应用于所有控制器:spring-doc.cadn.net.cn

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来构建错误,因为它已经准备好使用当前的DataFetchingEnvironmentspring-doc.cadn.net.cn

通过@GraphQlExceptionHandler方法进行的异常处理会自动应用于控制器调用。要处理其他基于graphql.schema.DataFetcher实现的异常,而非基于控制器方法,则需要从DataFetcherExceptionResolver中获得一个AnnotatedControllerConfigurer对象,并将其注册在GraphQlSource.Builder中作为DataFetcherExceptionResolverspring-doc.cadn.net.cn

方法签名

异常处理方法支持灵活的方法签名,方法参数来自DataFetchingEnvironment,并匹配@SchemaMapping 方法spring-doc.cadn.net.cn

支持的返回类型如下:spring-doc.cadn.net.cn

返回类型 描述

graphql.GraphQLErrorspring-doc.cadn.net.cn

将异常解析为单个字段错误。spring-doc.cadn.net.cn

Collection<GraphQLError>spring-doc.cadn.net.cn

解决异常为多个字段错误。spring-doc.cadn.net.cn

voidspring-doc.cadn.net.cn

解决异常而不返回错误响应。spring-doc.cadn.net.cn

Objectspring-doc.cadn.net.cn

将异常解析为一个错误、多个错误或不解析任何错误。
返回值必须是GraphQLErrorCollection<GraphQLError>nullspring-doc.cadn.net.cn

Mono<T>spring-doc.cadn.net.cn

异步解析中,<T> 是一种支持的同步返回类型。spring-doc.cadn.net.cn

命名空间

在模式级别,查询和突变操作直接定义在 QueryMutation 类型下。 丰富的GraphQL API 可以在这些类型下定义数十个操作,这使得探索API和分离关注点变得困难。 你可以选择 在GraphQL模式中定义命名空间。 尽管这种方法有一些注意事项,你仍然可以使用Spring for GraphQL 注解控制器来实现这一模式。spring-doc.cadn.net.cn

使用命名空间后,您的GraphQL模式可以将查询操作嵌套在顶级类型之下,而不是直接将其列在Query下。 在这里,我们将定义MusicQueriesUserQueries类型,并使其在Query下可用:spring-doc.cadn.net.cn

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查询:spring-doc.cadn.net.cn

{
  music {
    album(id: 42) {
      id
      title
    }
  }
}

并获取以下响应:spring-doc.cadn.net.cn

{
  "data": {
    "music": {
      "album": {
        "id": "42",
        "title": "Spring for GraphQL"
      }
    }
  }
}

这可以通过一个@Controller实现以下模式:spring-doc.cadn.net.cn

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 定义一个名为"音乐"的命名空间@QueryMapping
3 "music"查询返回一个"空"记录,但也可能返回一个空映射
4 查询现在作为字段声明在 "MusicQueries" 类型下

不再需要在控制器中显式声明包装类型("MusicQueries", "UserQueries"), 你可以选择使用一个GraphQlSourceBuilderCustomizer通过运行时配置来对其进行配置,这可以通过 Spring Boot 来实现:spring-doc.cadn.net.cn

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)
		);
	}

}
1 列出 "Query" 类型的所有包装类型
2 手动为每个它们声明数据获取器,并返回一个空的 Map