5. 注解控制器
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。
5.1. 声明
您可以用@Controller定义标准的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 部分。
5.2. @SchemaMapping
`0` 注解将处理器方法映射到GraphQL模式中的一个字段,并声明它是该字段的 `DataFetcher`。此注解可以指定父类型名称和字段名称:
@Controller
public class BookController {
@SchemaMapping(typeName="Book", field="author")
public Author getAuthor(Book book) {
// ...
}
}
`0` 注解也可以省略这些属性,在这种情况下,字段名称默认为方法名,而类型名称默认为注入到方法中的源/父对象的简单类名。例如,下面的内容默认类型为 "Book" 和字段为 "author":
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
`0` 注解可以在类级别声明,以指定该类中所有处理器方法的默认类型名称。
@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 处理方法具有灵活的签名,并可以从一系列方法参数和返回值中进行选择。
5.2.1. 方法签名
Schema 映射处理方法可以有以下任意方法参数:
| 方法参数 | 描述 |
|---|---|
|
用于访问绑定到更高层级、已类型化对象的命名字段参数。
参见 |
|
对于获取原始参数映射的访问权限,在此情况下, |
|
用于访问绑定到更高层级、已类型化对象的命名字段参数,
以及一个标志位,用于指示输入参数是省略了还是被设置为 |
|
用于访问绑定到更高级别、类型化对象的所有字段参数。
请参阅 |
|
对于参数的原始映射的访问权限。 |
|
用于通过项目接口访问字段参数。
请参阅 |
"源" |
对于字段的源(即父级/容器)实例的访问,请参阅 源。 |
|
如需访问 |
|
从主 |
|
从 |
|
从 |
|
从 Spring Security上下文获取(如果可用的话)。 |
|
从Spring Security上下文中访问 |
|
通过 |
|
从 |
|
直接访问底层的 |
Schema mapping handler方法可以返回:
-
任何类型解析后的值。
-
Mono和Flux用于异步值。支持控制器方法以及任何DataFetcher,如 响应式DataFetcher中所述。 -
java.util.concurrent.Callable用于产生异步值。 要实现这一点,AnnotatedControllerConfigurer必须配置一个Executor。
5.2.2. @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) {
// ...
}
}
默认情况下,如果方法参数名称可用(需要使用 Java 8+ 的 -parameters 编译器标志或编译器提供的调试信息),则会使用该名称来查找参数。如需自定义名称,可以通过注解进行设置,例如 @Argument("bookInput")。
The @Argument annotation does not have a |
如果绑定失败,会抛出一个 BindException 并积累绑定问题作为字段错误,在每个错误的 field 是问题发生的位置。
您可以使用@Argument和一个Map<String, Object>参数来获取所有参数值的原始映射。@Argument上的name属性必须未设置。
5.2.3. 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 方法初始化,包括在顶级对象下方的任何层级嵌套的对象中作为字段。
5.2.4. @Arguments
使用@Arguments注解,如果你希望将完整的参数映射绑定到单个目标对象上,而不是使用@Argument注解,后者只绑定特定命名的参数。
例如,@Argument BookInput bookInput 使用参数 "bookInput" 的值来初始化 BookInput,而在 @Arguments 中使用完整的参数映射,在这种情况下,顶级参数绑定到 BookInput 属性。
您可以使用@Arguments和一个Map<String, Object>参数,以获取所有参数值的原始映射。
5.2.5. @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();
}
5.2.6. 源代码
在GraphQL Java中,`0`提供了对字段的源(即父级/容器)实例的访问。要访问这一点,请声明一个预期目标类型的方法参数。
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
源方法参数也有助于确定映射的类型名称。
如果 Java 类的简单名称与 GraphQL 类型匹配,则无需在 @Autowired 注解中显式指定类型名称。
|
一个 |
5.2.7. 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方法来减少样板代码。
5.2.8. 验证
当找到一个javax.validation.Validator bean时,AnnotatedControllerConfigurer会启用对注解控制器方法的
Bean Validation
支持。通常,该 bean 的类型为 LocalValidatorFactoryBean。
Bean验证让你可以在类型上声明约束:
public class BookInput {
@NotNull
private String title;
@NotNull
@Size(max=13)
private String isbn;
}
然后可以使用@ModelAttribute注解控制器方法参数,在方法调用前对其进行验证:
@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 (评论) 以获取相关问题的链接和建议的工作around。 |
5.3. @BatchMapping
批量加载通过使用一个org.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());
}
}
对于上面展示的直接加载关联实体的情况,`0` 方法除了委托给 `1` 方法之外不做其他事情。这是一些可以使用 `@BatchMapping` 方法避免的样板代码。例如:
@Controller
public class BookController {
@BatchMapping
public Mono<Map<Book, Author>> author(List<Book> books) {
// ...
}
}
上述内容在BatchLoaderRegistry中变成了一个批量加载功能,其中键是Book实例,加载的值为它们的作者。此外,DataFetcher还会透明地绑定到类型Book的author字段上,该字段仅仅委托给DataLoader来处理作者信息,给定其源/父级实例Book。
|
要作为唯一键使用, |
默认情况下,字段名称会自动使用方法名,而类型名称则默认为输入List元素类型的简单类名。两者都可以通过注解属性进行自定义。类型名称也可以从类级别的@SchemaMapping继承。
5.3.1. 方法签名
批处理映射方法支持以下参数:
| 方法参数 | 描述 |
|---|---|
|
源/父对象。 |
|
来自 Spring Security 上下文(如果可用的话)。 |
|
从 |
|
从 |
|
GraphQL Java为一个
|
批量映射方法可以返回:
| 返回类型 | 描述 |
|---|---|
|
以父对象作为键,批量加载的对象作为值的映射。 |
|
必须按与传递给方法的源/父对象相同顺序加载的一系列批处理对象。 |
|
不使用远程调用的命令式变体。 |
|
异步调用的命令式变体。为了使这一点生效,
|