GraphQLサーバーをJavaで実装してみる


こんにちは。個人的に、JavaでGraphQLサーバーを実装する機会があったので、今回はその知見について書きたいと思います。
この記事はGraphQL Advent Calendar 2020の18日目の記事です。

はじめに

この記事では、GraphQLの概要や実装の中で使用しているSpring Bootおよびその周辺ライブラリについての説明は記述していません。それらをある程度知っている方が対象になると思いますが、そこまで難しいことは書いていないのでおそらく雰囲気で理解できると思います。

ライブラリを探す

まずは、GraphQLサーバーを実装する上で必要なライブラリを探します。

の2つが見つかりました。どちらもSpring Bootと組み合わせて使用できるようです。
GraphQLのResolverの実装例を見ると、GraphQL Java Kickstartの方がよりシンプルに書けそうだったので、今回はこちらを使用することにします。

GraphQL Java KickstartのREADMEを見ると、

This project wraps the Java implementation of GraphQL provided by GraphQL Java.

GraphQL Java の方をラップしている。と書いています。なるほど。

実装する機能を決める

次にGraphQLを使って実装する機能を決めます。今回は基本となるQuery、Mutation、そしてSubscriptionを使った機能を実装することにしました。機能のユースケースはざっくり以下の通り。

  • Query: 本の一覧を取得する。
  • Query: 本のIDを指定して、特定の本を取得する。
  • Mutation: 新しい本を登録する。
  • Subscription: 新たに登録された本を通知する。

プロジェクトを構成する

それでは、アプリケーションを作成していきます。プロジェクトはひな型はSpring Initializerで適当に作成します。必要なdependenciesは後から追加するので、ここではLombokのみ追加しています。

アプリケーションを作成する

build.gradle

はじめに、build.gradleを修正します。

com.graphql-java-kickstart:graphql-spring-boot-starterをdependencyに追加します。
また、com.graphql-java-kickstart:graphiql-spring-boot-starterも追加して、GraphiQL上でAPIのテストを行います。
その他、io.projectreactor:reactor-corespring-actuatormicrometer-registry-prometheusのdependencyも追加しました。前者はSubscription実装のため、後者はMetrics取得のためです。

build.gradle
plugins {
    id 'org.springframework.boot' version '2.4.1'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.graphql-java-kickstart:graphql-spring-boot-starter:8.0.0'
    runtimeOnly 'com.graphql-java-kickstart:graphiql-spring-boot-starter:8.0.0'
    // To embed GraphiQL tool
    implementation 'com.graphql-java-kickstart:graphiql-spring-boot-starter:8.0.0'
    // For subscripion
    implementation 'io.projectreactor:reactor-core:3.4.1'
    // For metrics
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-registry-prometheus:1.6.0'

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

application.yml

次に、application.ymlを作成します。

  • 1. To enable graphiql以下の設定で、GraphiQLのURLエイリアスとGraphQLのアクエスポイントURLを指定します。
  • 2. To enable graphql metricsは、GraphQLに関するMetricsを取得するための設定です。
  • 3. To enable to get metrics..は、spring-actuatorの設定です。/actuator/prometheusにアクセスすると、各MetricsがPrometheusのフォーマットで取得できます。
application.yml
# 1. To enable graphiql
graphiql:
  mapping: /graphiql
  endpoint:
    graphql: /graphql
# 2. To enable graphql metrics
graphql:
  servlet:
    actuator-metrics: true
# 3. To enable to get metrics of spring-actuator from /actuator/prometheus
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus

schema.graphqls

schema.graphqlsを作成して、GraphQLのスキーマを定義します。
schema.graphqlssrc/main/resources/graphsql以下に登録します。

schema.graphqls
type Query {
    bookById(id: ID): Book
    books: [Book]!
}

type Book {
    id: ID
    name: String
    pageCount: Int
}

type Mutation {
    registerBook (
        id: ID
        name: String
        pageCount: Int
    ): Book
}

type Subscription {
    subscribeBooks: Book!
}

Java Model

GraphQL Javaでは、スキーマに定義したJavaのクラスが必要です。まず、Book TypeのJava Modelを作成します。

Book.java
@AllArgsConstructor
@Data
public class Book {
    private String id;
    private String name;
    private int pageCount;
}

Resolver

次に、Query、Mutation、そしてSubscriptionを実装したResolverを作成します。
それぞれ以下の実装クラスの作成が必要で、スキーマに定義したクエリー名と実装したメソッド名は同一にします。
また、実装クラスはSpringのBeanとして登録します。

  • Query : GraphQLQueryResolverを実装したクラスを作成し、スキーマに定義したクエリー名と引数を持つメソッドを追加します。
  • Mutation : GraphQLMutationResolverを実装したクラスを作成し、スキーマに定義したMutation名と引数を持つメソッドを追加します。
  • Subscription : GraphQLSubscriptionResolverを実装したクラスを作成し、スキーマに定義したSubscription名と引数を持つメソッドを実装します。Return値はreactive-streamsPublisherである必要があります。

DataProviderIBookProcessorは独自に作成したクラスです。以下の処理を行います。

  • DataProvider : データ提供クラス。すべてのBookのList、または指定されたbookIdBookを返す。
  • IBookProcessor : 本の登録をイベントとして登録(emit)し、イベントのPublisherを発行(publish)する。
BookResolver.java
@Slf4j
@AllArgsConstructor
@Component
public class BookResolver implements GraphQLQueryResolver,
                                     GraphQLMutationResolver,
                                     GraphQLSubscriptionResolver {
    private final DataProvider dataProvider;
    private final IBookProcessor bookProcessor;

    /**
     * Query: Get all books.
     */
    public List<Book> books() {
        return dataProvider.books();
    }

    /**
     * Query: Retrieve a book by id.
     */
    public Book bookById(String bookId) {
        return dataProvider.bookById(bookId);
    }

    /**
     * Mutation: Register a book.
     */
    public Book registerBook(String id, String name, int pageCount) {
        final Book book = new Book(id, name, pageCount);
        dataProvider.books().add(book);

        // Emit an event for subscription.
        bookProcessor.emit(book);
        return book;
    }

    /**
     * Subscription: Publish an event that a book is registered.
     * Need to return Publisher on reactive-streams.
     */
    public Publisher<Book> subscribeBooks() {
        return bookProcessor.publish();
    }

    /**
     * Error handler. can handle an throwable that occurs in resolver execution.
     */
    @ExceptionHandler(Throwable.class)
    GraphQLError handle(Throwable e) {
        log.error("Failed to execute resolver.", e);
        return new ThrowableGraphQLError(e, "Failed to execute resolver.");
    }
}

わかりやすく図にしてみました。

アプリケーションを実行する

作成したアプリケーションを実行します。SpringBootのアプリケーションを起動し、GraphiQLのエンドポイント(http://localhost:8080/graphiql)にブラウザでアクセスし、Queryを実行します。

  • Query: 本の一覧を取得する。

  • Query: 本のIDを指定して、特定の本を取得する。

  • Mutation: 新しい本を登録する。

  • Subscription: 新たに登録された本を通知する。

    ちょっとわかりづらいですが、上のイメージは以下の操作を行った後のイメージです。

    1. 上記Subscriptionを実行する。
    2. 別ブラウザを開いて、Mutationを実行する。
    3. Subscriptionの結果に、登録された本の情報が表示される。

Metricsも正しく収集されていました。グラフ化には以下のサイトにあるMetricatというツールを使っています。

MetricatのPrometheus exporter URLにspring-actuatorのprometheusエンドポイント(http://localhost:8080/actuator/prometheus)を設定し、その後、GraphiQLでクエリーを実行すると以下のようなグラフが表示されます。

まとめ

以上、JavaでGraphQLサーバーを実装する方法について記述しました。実装したコードはあくまでサンプルコードレベルですが、Resolverなど簡単に開発でき、使い勝手は悪くない印象でした。

実務で使用する予定はまだないですが、実務で使うことを想像して気になる点をいくつかあげてみました。

  • Resolverのasyncで実装したい。
    CompletionStage(実装クラス:CompletableFuture)がreturnできるようです。Async Resolvers #1
  • 分散Tracingの導入
    → 一応、ここTracing and Metricsに記述がありますが、Apollo style tracing ? 謎なので後で調べる。
  • MetricにResolverのレイテンシを追加したい。
    Micrometerを使って、実装する必要がある感じ?Resolverの中にハードコードしたくないので、Spring AOPなどで実装したい。後で調べる。
  • Spring WebFlux対応
    → なにかあるので後で調べる。graphql-kickstart-spring-webflux

使用したソースコードは、以下のGitHub上に登録しています。参考にしていただければ幸いです。