使用 [Spring GraphQL] Schema Mapping Inspection 来检测漏实现的解析器
环境
-
- java: 17
-
- spring-core: 6.0.1
-
- spring-boot: 3.1.3
- spring-graphql: 1.2.2
GraphQL API 的两种实现方法
在实现GraphQL API时,可以大致分为两种方法:基于模式的开发(Schema First)和基于代码的开发(Code First)。
Schema First 是一种先定义架构,然后准备相应实现的方法,
而 Code First 是一种根据函数和解析器的实现,自动生成架构的方法,利用其签名和元信息。
由於這兩種方法的優劣與本文的主題無關,我將轉向其他網站來討論。
-
- https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql/
- https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/
Spring GraphQL 的困难之处
spring-graphql采用了基于Schema First的方法。在开发中,首先需要在*.graphql(s)文件中定义模式,并相应地在java中实现解析器。作为基于Schema First方法的命运的一部分,令人困扰的是“解析器的漏实现”。
例如,假设我们定义了一个返回用户列表的 `Query.users` 字段,并且在启动服务器时对应的解析器没有实现,我们在访问该解析器之前可能不会意识到它未被实现。
尽管静态类型语言的优点是即使不执行代码,编译器也可以告诉我们许多问题,但无法在执行之前意识到问题有些可惜的感觉。
对于这个问题,我之前是通过使用graphql-autotest来自动生成覆盖所有解析器的查询,并将其作为端到端测试的一部分来检测问题。
然而,这种方法只能在访问非空项时产生错误,对于可为空的项,只会从服务器返回null,无法察觉到漏掉的实现,因此无法完全覆盖问题。
架构映射检查
在升级到 SpringBoot 3.x 时,我重新查看了 spring-graphql 的文档,发现在 v1.2 版本中增加了一个名为 Schema Mapping Inspection 的功能(基于 Issue#386)。
在服务器启动时,已经添加了一个名为GraphQlSource.Builder#inspectSchemaMappings的API,它可以将模式和实现之间的差异部分作为报告接收。
根据文档,无法根据签名返回类型,如union,java.lang.Object或List <?>来判断与GraphQL端类型的对应关系的解析程序将无法正确判断。但是,它能够报告任何遗漏,即使我们没有在测试中覆盖到,这是一个很有用的功能。
尝试使用Schema Mapping Inspection
设定
通过在 GraphQlSource.Builder 的 inspectSchemaMappings 方法中设置回调函数,可以使该功能生效。
@Configuration(proxyBeanMethods = false)
@Slf4j
public class Config {
@Bean
public GraphQlSourceBuilderCustomizer customizer() {
return builder -> {
builder.inspectSchemaMappings(report -> {
if (report.unmappedFields().isEmpty() && report.unmappedRegistrations().isEmpty()) {
return;
}
report.unmappedFields().forEach(f -> {
log.error("実装が足りません!! {}.{}", f.getTypeName(), f.getFieldName());
});
report.unmappedRegistrations().forEach((f, d) -> {
final String desc = switch (d) {
case SelfDescribingDataFetcher<?> sdf -> sdf.getDescription();
default -> null;
};
log.error("スキーマ定義が足りません!! {}.{} ({})", f.getTypeName(), f.getFieldName(), desc);
});
// 問題がある場合はサーバを異常終了させる
throw new RuntimeException("マッピングが不完全です!!");
});
};
}
}
试用 API
由于设置已经完成,我会尝试实现一个随意的GraphQL API供试用。
type User {
id: ID!
name: String!
address: String
foo: String # 実験: この field に対応する resolver 実装を意図的に欠落させる
}
type Query {
users: [User!]!
}
public record User(String id, String name, String address) {
}
@Controller
public class UserController {
@QueryMapping
public Flux<User> users() {
return Flux.fromIterable(List.of(
new User("u1", "taro", "tokyo"),
new User("u2", "jiro", "osaka"),
new User("u3", "saburo", null)
));
}
// 実験: Schema 側に存在しない resolver を生やしてみる
@QueryMapping
public Mono<User> userBy(@Argument String id) {
return Mono.just(new User("u1", "taro", "tokyo"));
}
}
执行结果(摘录)
2023-08-27T20:53:08.374+09:00 [ INFO] Loaded 1 resource(s) in the GraphQL schema.
2023-08-27T20:53:08.438+09:00 [ERROR] 実装が足りません!! User.foo
2023-08-27T20:53:08.439+09:00 [ERROR] スキーマ定義が足りません!! Query.userBy (UserController#userBy[1 args])
2023-08-27T20:53:08.440+09:00 [ WARN] Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'graphQlSource' defined in class path resource [org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.class]: Failed to instantiate [org.springframework.graphql.execution.GraphQlSource]: Factory method 'graphQlSource' threw exception with message: マッピングが不完全です!!
检测到缺少实施的解析器和未定义模式的解析器,真是太棒了!
总结
我在引入了spring-graphql v1.2中添加的Schema Mapping Inspection的方法来检测解析器实现中的遗漏问题。
希望能够在未来支持目前无法检测到的一些模式。
目前,我打算将其与使用graphql-autotest创建的端对端测试相互补充,并一起使用。