使用 [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创建的端对端测试相互补充,并一起使用。