Edited at
GraphQLDay 6

コードで見る graphql-java 関連ライブラリの関係性

graphql-java を Spring Boot などで使う場合、簡単に使えるようにする場合 graphql-java 以外にも色々なライブラリを読み込む必要があります。

これは graphql-java はプリミティブな API しか持っておらず、web 側との繋ぎとか、subscription の対応をそれ単体ではできないためです。

この辺りの関係性が自分の中でも結構曖昧だったり、ドキュメントの情報しか読んでなかったので、コードリーディングしてわかったことをこの記事にメモします。


前提

Spring Boot + Kotlin で graphql-java-tools を使う場合で考えます。この辺りについては会社のブログの方でも書いたので、こちらもよければご参考にしてください。

以下のライブラリを使います。


構成

大雑把に言うと、graphql-java が GraphQL としてのコア機能を提供し、graphql-java-tools がコア機能を graphql-java のプリミティブな API を呼び出さずに済むように便利にします。

graphql-java-servlet は servlet api を用いて graphql-java をウェブ上で使えるようにし、

graphql-spring-boot-server が graphql-java-servlet を Spring Boot でのやり方に合わせるようにしてくれ DI での開発をやりやすくしてくれます。

正直色々あって混乱します。私もそれまでは Rails 使いだったこともあり、初見では、なんでこのライブラリが必要なのか?これを外したらどうなるのか?など全くわからなかったですし、今も分かっていないことが多いです。

なので間違いがありましたらご指摘いただけたら幸いです。


コードから見た関係性

話がそれますが、コードリーディングする際、各種リポジトリを落としてきて読んだのと、実際にサンプルのアプリを作って動かしてデバッグ実行しての合わせ技で理解していきました。

その際にはこちらのサンプルアプリで動作確認しました。ライブラリのバージョンは古いですが、大枠は変わっていないだろうと考えてアップデートしていません(変わってたら泣きたい)。


依存関係


  • graphql-java


    • 大元のコア機能



  • graphql-java-servlet


    • graphql-java に依存



  • graphql-java-tools


    • graphql-java に依存



  • graphql-spring-boot-starter


    • 同リポジトリの graphql-spring-boot-autoconfigure を経由して、graphql-java, graphql-java-tools, graphql-java-servlet に依存




graphql-java と graphql-java-servlet の関係

graphql-java は単体で HTTP で受けるインターフェースを持っていません。そのため HTTP を通して GraphQL 機能を作るためのライブラリが graphql-java-servlet になります。

servlet と名がつく通り、AbstractGraphQLHttpServlet を通して HttpServlet を継承した、GraphQLHttpServlet がライブラリ中で定義されています。

AbstractGraphQLHttpServlet にて doGet, doPost をオーバーライドしており、HTTP での受け答えができるようにしており、

さらに GraphQLQueryInvoker クラスを通して graphql-java の API を叩き GraphQL の処理を行った結果を返しているようです。


graphql-java と graphql-java-tools の関係

graphql-java で使う GraphQLSchema を生成する部分の手続きを、graphql-java-tools では GraphQL の各型のフィールド用 Resolver 関数が定義されたクラスと、GraphQL の各型を表すデータクラスを定義することで簡便に行うための仕組みを提供している。graphql-java の schema 定義のドキュメントを見ると確かに結構面倒そうで、他の言語で提供されている仕組みと比べるともっと簡易にできる方法があるだろうとは確かに感じる。それもあって graphql-java-tools が便利なところを補ってくれている。

SchemaParserBuilder を使いスキーマを定義したファイル、GraphQLResolver を継承した Resolver クラスを渡し SchemaParser クラス(graphql-java にも同名のクラスがあるが異なるので注意)を生成する。スキーマ定義のパースには graphql-java の Parser クラスを使い(そのためここは graphql-java と共通の処理)、parseDocumentDefinition を取得し内部的に使う。

SchemaParser クラスの makeExecutableSchema を呼び出すことで GraphQLSchema が生成される。makeExecutableSchema で呼び出されている toSchema メソッドは、SchemaObjects のメソッドで実装を見るとわかるように、graphql-java の GraphQLSchema 用のビルダーを使って GraphQLSchema を生成している。toSchema に必要なパラメータは、SchemaObjects のコンストラクタへ渡されている各 query/mutation/subscription 用の GraphQLObjectType であるので、ここへ渡される前に、スキーマや各 Resolver の情報から GraphQLObjectType への変換が済んだ状態になっている。

GraphQLObjectType への変換は SchemaParser クラスの parseSchemaObjects メソッドから呼び出される、createObject メソッドで行われている。createObject メソッドでは、GraphQLObjectType ビルダーを使って GraphQL の型に対応するオブジェクトを定義し GraphQLObjectType を生成する。この時オブジェクトのフィールドについても定義を行い、fieldResolversByType (後述) を使ってフィールド用の DataFetcher を設定する。

fieldResolversByTypeObjectTypeDefinition がキーで、MutableMap<FieldDefinition, FieldResolver> をバリューとしてもつ Map で、SchemaParser クラスのコンストラクタで渡される ScannedSchemaObjects から取得している。ScannedSchemaObjectsSchemaParserBuilder から渡されるクラスで、SchemaClassScannerscanForClasses メソッドで生成する。

各 Resolver の動きに注目すると、SchemaClassScanner にてまず ResolverInfo を継承した各クラスに Resolver の情報を持たせる。

その上で scanResolverInfoForPotentialMatches にて、オブジェクトのフィールドにマッチする FieldResolverResolverInfo を渡して生成し、fieldResolversByType へオブジェクトの型( ObjectTypeDefinition )をキーにして、FieldDefinition とともにセットする。FieldResolver を継承した各クラスは DataFetcher を生成するための createDataFetcher メソッドを持っているため、これで各 Resolver をフィールドにマッピングすることができる。FieldResolverMethodFieldResolver, PropertyFieldResolver, PropertyMapResolver と定義されており、適切なクラスがマッピングされる。

ちょっと理解がふわふわしているところもあるが、大まかな動きはこんな感じであろうと思う。


graphql-spring-boot-starter と各ライブラリの関係

上記 graphql-java, graphql-java-servlet, graphql-java-tools を組み合わせたものを、Spring Boot っぽく DI の仕組みで使えるようにしたライブラリです。

graphql-spring-boot-starter の実体はほぼ同一リポジトリにある graphql-spring-boot-autoconfigure なので、このコードをもとに説明します。

graphql-spring-boot-autoconfigure には、Spring Framework の @Configuration アノテーションがついたクラスが存在していて、XxxAutoConfiguration という postfix がついています。WebSocket 向けの設定などもありますが、この記事では GraphQLJavaToolsAutoConfigurationGraphQLWebAutoConfiguration について見ていきます。

GraphQLJavaToolsAutoConfiguration はその名の通り graphql-java-tools 向けの設定で、graphql-java の GraphQLSchemaBean として作るための一連の設定となっています。GraphQLSchema を作るための SchemaParser に渡す Resolver は List<GraphQLResolver<?>> resolvers として Constructor Injection で受け取るようにしており、使う側としては GraphQLResolver の各種 IF を定義したクラスを作るだけで、アプリ起動後に GraphQL の機能が使えるようになります。graphql-java-tools もこれがあるおかげで、その真価を発揮できているように感じます。

GraphQLWebAutoConfiguration は graphql-java-servlet 用の一連の設定となっており、ServletRegistrationBean<AbstractGraphQLHttpServlet>@Bean として定義することで、GraphQLHttpServlet(デフォルトでは)を Servlet として登録しています。ServletRegistrationBean についてはこちら。GraphQLHttpServlet の設定として渡される GraphQLSchemaGraphQLJavaToolsAutoConfiguration で定義されたものが使用されるようになっており、これによって GraphQL のスキーマ定義から実際に HTTP 上で動作する GraphQL の機能が使えるようになります。

ServletRegistrationBean 以外の @Bean には @ConditionalOnMissingBean@Autowired(required = false) が付いているため、デフォルト挙動は決めつつ、必要なところは上書きするってことができるようになっています。

GraphQLJavaToolsAutoConfiguration もそうですが上手く Spring の機能を上手く使っているなと感じます。なんか Rails っぽいですよね?(筆者は Rubyist で Rails が比較的得意です)。


まとめ

今までドキュメントでだけ理解していた箇所が、コードリーディングで関係を明らかにしたことによって中身の理解がかなり深まりました。

他の言語だと graphql-java-tools 相当のライブラリにいきなり辿り着けるので、比較的理解もしやすいのですが、graphql-java はもうちょっと間に挟まってしまう感じがあって自由度は高いもののちょっとハードルがあるかなと感じました。

もともと Ruby, Elixir が好きなのもあるとは思うものの、graphql-ruby とか Elixir の Absinthe とかはかなりイメージがつかみやすいです(graphql-ruby と Absinthe はコードファーストなライブラリというのもありますが)。

ただこれからの流れとして Java などで GraphQL をやるなら graphql-java をそのまま使うのではなく graphql-java-tools を使うか、同じように schema 定義を楽にしてくれるライブラリとセットで使うのが普通になっていくのではないかと思います。

それと今回調査をするにあたって graphql-java 関連の情報があまりなく、graphql-java ほとんど使われていないんじゃ疑惑が拡大しています。

graphql-java 使っている方たちがいましたらアウトプットに期待しています。


参考


GraphQL Java 関連


Spring 周りについて


関連記事、サンプル実装、他言語での情報とか