この記事では読者であるあなたを、Eclipse Vert.xツールキットを使ったモダンでレスポンシブ、かつレジリエントな、メッセージ駆動の継続的インテグレーション(CI)システムによる設計と開発の世界にお誘いします。記事中では、Project JigsawをプロトタイプとするJava Platform Module System(JPMS)を活用して、明確に定義されたインターフェース経由で通信する疎結合モジュールを複数使用したアプリケーションの構築を行います。
JPMSは、大規模な既存コードベースへの対処に加えて、新たなアプリケーション開発においても、Javaアーキテクトと開発者が自信を持ってモジュールを使用できることを意図しています。しかしながら、既存のJavaライブラリをモジュールシステムで使用するのは、必ずしも容易ではありません。そこで今回は、Javaモジュールシステムを使った開発で遭遇するさまざまな課題と、システムを稼働させる上での回避策について説明したいと思います。
まず最初に、この新たなCIシステムのMVP(Minimum Viable Product)を定義することにしましょう。ここではDockerネイティブなシステムとしての構築を目指します。このプロダクトでは、以下のコア機能をREST APIとして提供することが求められています。
- リポジトリに対するCRUDのサポート。リポジトリとは、バージョン管理下のプロジェクトを表します。接続の詳細をGitリポジトリあるいはPerforceデポに対して指定します。
- “パイプライン・アズ・コード”思想のサポート。パイプラインは、アーティファクトとしてのコードを作成するワークフローを定義します。パイプラインの定義にはJavaScriptを使用します。スクリプトファイルはコードベースとともに、ソースコードリポジトリに格納することが可能です。
- パイプラインの起動と停止を行なうAPIの提供。指定されたパイプラインのインスタンスがビルドを構成します。
MVPの定義が完了すれば、システムの構築に取り掛かることができます。最初のステップはプロジェクトスケルトンの作成です。この目的には、IntelliJのマルチモジュールgradleプロジェクトテンプレートが便利です。今回はJDK 9を使用するので、最新かつ最高バージョンのgradle(執筆時点では4.4.1)を使用できるというメリットがあります。モジュラーjarを作成するには、試験提供されているjigsawプラグインを追加して、ソース互換性としてJava 9を指定する必要があります。プロジェクトのメインである“build.gradle”ファイルは、次のような内容になっているはずです。
ほとんどのシステムと同じように、このシステムでもエンティティオブジェクトをホストする共通コアライブラリ、ユーティリティクラス、共有定数やクエリパーザを持つことにします。このコアライブラリをJava 9モジュールとして定義しましょう。
先に述べたようにJava 9モジュールは、インターフェースやクラス、リソースを集めた自己記述型のコレクションです。開発者がモジュールの公開部分と他のモジュールへの依存関係を定義するために、JPMSの新たな構成体として“module-info.java”が追加されました。このファイルを使用してモジュールの名称を定義し、他のモジュールへの依存関係、およびこのアプリケーション内で他のモジュールによって使用されるパッケージを指定します。
以下のコードは、module-infoファイルを使ってコアモジュールを記述する方法を示しています。
定義部分を詳しく見ていきましょう。すべての“module-info.java”ファイルは、キーワード“module”に続くモジュール名から始まります。モジュール名の定義は、パッケージ名の命名規則としては一般的な、逆ドメイン名記述法で行います。
それに続くコードブロック内には2つの新しいキーワード、“exports”と“requires”があります。“exports”キーワードは、このモジュールが公開するパブリックパッケージ、つまりこのモジュールの公開APIの宣言に使用されます。“requires”キーワードはモジュールの依存性の宣言に使用します。
この時点で、すでに公開されている多種多様なサードパーティ製の非モジュールライブラリに依存するJava 9モジュールはどうやって作成するのか、という疑問が生じることと思います。この目的のために導入されたのが、自動モジュールといういう概念です。
非モジュールjarは、そのjarファイルの名称をベースとした名前のモジュールjarに、その名のとおり自動的に変換されます。自動付与される名称は、“jarファイル名の拡張子を削除し、ドットをハイフンに置き換えた上で、バージョン番号があれば削除する”、というアルゴリズムで導出されます。例えば非モジュールである“vert-core-3.5.0.jar”ライブラリは、“vertx.core”という名前の依存対象として指定されます。ただし、後述のNettyのネイティブトランスポートライブラリに依存する場合のように、この方法で推論された自動モジュールが常に機能するとは限りません。
データベースコールやユーザ認証、エンジンの実行、CIシステム内のさまざまなシステムプラグインとの通信などの処理を行なうモジュールも、コアモジュールと同じように定義する必要があります。
モジュールJavaアプリケーションの基本概念をいくつか紹介しましたので、ここで一歩戻ってvert.xの説明をしましょう。vert.xは、呼び出しスレッドをブロックしない非ブロックAPIを提供するツールキットです。この非ブロック性のおかげで、vert.xアプリケーションは、少数のスレッドプールを使って多数の並列処理を行なうことができます。これを実現しているのがマルチリアクタ(multi-reactor)パターンです。
JavaScriptに精通した開発者であれば、登録されたハンドラに到達したイベントを配信する、単一のスレッドイベントループを思い浮かべることができるでしょう。マルチリアクタパターンはこれに準じたアプローチですが、単一スレッドが本質的に持つ制限を克服するために、vert.xでは、サーバで利用可能なコア数に基づいた複数のイベントループを使用します。
vert.xはまた、“アクタ(actor)”が受信したメッセージに応答することで、メッセージを使った相互通信を行うという、疎結合のアクタモデルに基づいた独善的(opinionated)並列処理モデルを備えています。vert.xのアクタは“バーティクル(verticle)”と呼ばれて、通常はイベントバスを介して送信されるJSONメッセージを使って他のバーティクルと通信します。vert.xがデプロイするインスタンス数をバーティクル毎に指定することもできます。
イベントバスは、HazelcastやZookeeperなどのさまざまなクラスタマネージャをプラグアンドプレイで使用して、クラスタを構成できるように設計されています。各バーティクル、あるいはvert.xインスタンス上で動作するバーティクルの組み合わせは、マイクロサービスであると見なすことができます。マルチリアクタモデルとアクタ的なバーティクル、そしてイベントバスの分散的性質の組み合わせによってvert.xアプリケーションは、レスポンスとレジリエンスに優れ、かつエラスティックであるという、 リアクティブ宣言(reactive manifesto)に完全に沿ったものになります。
そのことを念頭において、今回のCIシステムの全体的な流れを見てみましょう。
上に示すように、多数のバーティクルがvert.xイベントバスを介して相互通信を行います。この図において、プラグインもバーティクルである点に注意してください。CIシステムへのエントリポイントは、サーババーティクル(SERVCER VERTICLE)を経由します。 サーババーティクルはパブリックなバーティクルで、REST APIを公開します。APIエンドポイントはCLIおよびGUIクライアントによって使用され、リポジトリへの接続の詳細を指示するとともに、パイプラインの生成とその実行を行います。
以下のコードは、vert.xでのAPIとルートの定義方法に関する考え方を示す部分を抜粋したものです。
この例では、REST APIの定義にvert.xのWebライブラリを使用して、すべてのルートを“/api/v1/”ベースのパスの下にマウントします。vert.xはその他にも、リアクティブなアプリケーションを短時間で開発するために、数多くのライブラリや機能を提供しています。
例えば、Web APIコントラクトライブラリを使ってOpen API 3仕様準拠のアプリケーションAPIを設計すれば、リクエストの検証とセキュリティの検証をライブラリに自動処理させることが可能になります。vert.xのOAuthライブラリを使用すれば、GoogleやFacebook、あるいは独自のカスタムプロバイダなどのOAuthプロバイダを使って、アプリケーションとAPIを保護することができます。
先程の図に戻ると、エンジンバーティクル(ENGINE VERTICLE)は、パイプラインインスタンスの実行や構築を調整する役割を持っています。パイプラインを起動するために、サーババーティクルの提供するREST APIがクライアントによって呼び出されると、サーババーティクルがエンジンバーティクルにメッセージを送ります。パイプライン実行を開始するこのメッセージを受信することにより、エンジンが新しいフローオブジェクトをインスタンス化するのです。
フローオブジェクトは、パイプラインインスタンスの進行を追跡するために使用される、シンプルなステートマシンです。任意の時点において、フローオブジェクトは、セットアップ・実行・ティアダウンのいずれかの状態を持つことができます。着信したメッセージに基づいて、新たな状態に移行することも可能です。それぞれ状態においてフローオブジェクトはイベントを発行し、メッセージとしてイベントバス上に送信します。
登録されたプラグインがこれらのメッセージを処理して、処理結果を非同期に、イベントバスを介して返信します。パイプラインを起動するメッセージハンドラを登録し、フローオブジェクトを生成して、プラグインからの着信メッセージを処理する方法を示すコードの抜粋を以下に示します。
エンジンバーティクルは、プラグインとバーティクルの配置やデプロイも行います。今回のシステムでは、Java 6で導入され、Java 9で変更されたJavaのサービスローダのメカニズムを使って、サーバ起動時にプラグインの配置とデプロイを行うことにします。サービスのロードについて理解するには、サービスとサービスプロバイダについて説明しなければなりません。
サービスは、よく知られているインターフェースないしクラス(通常は抽象クラス)です。サービスの具象クラスがサービスプロバイダです。ServiceLoaderクラスは、特定のサービスを実装したサービスプロバイダをロードするための機構です。モジュールが特定のサービスを使用していることを宣言するには、このServiceLoaderクラスを使用します。ServiceLoaderを使うことで、ランタイム環境にデプロイされたサービスプロバイダを特定してロードすることが可能になります。
例えば、サーバモジュールはPluginインターフェースを使うことを宣言し、ワークスペース(WORKSPACE)モジュールは、対応する“module-info.java”ファイルに記述されたPluginコントラクトを実装した2つのサービスを提供することを宣言します。
これによって、サーバモジュールが起動すると、ServiceLoaderが呼び出されて2つのプラグインを受信し、それらがバーティクルとしてデプロイされます。
プラグインの仕事はたくさんあります。まず最初に、パイプラインフローの対象イベントを処理するメッセージハンドラを登録します。例えばワークスペースプラグインは、gitからのコード同期を担当します。スクリプトパーザプラグイン(SCRIPT-PERSER PLUGIN)は、ワークスペースをスキャンして – JavaScriptで記述された – パイプラインスクリプトを見つけ出し、パイプラインを実行する方法について記述したコードを実行する役割を持っています。処理結果はいくつかのshellコマンドで、それらがスクリプトランナ(SCRIPT RUNNER)プラグインによってDockerコンテナ内で実行されます。vert.xはJavaの組込みJavaScriptエンジンであるNashhornを利用しているので、JavaScriptを実行できます。vert.xがJavaScriptやKotlin、Groovyなどの言語をサポートする多言語ツールキットであることに注目してください。
以下はパイプラインスクリプトの一部です。
スクリプトランナプラグインは、受信したメッセージに基づいてDockerイメージをダウンロードし、コンテナを生成して、その中でshellコマンドを実行します。ですから当然、Dockerエンジンと対話する必要があります。
DockerのREST APIは、TCPソケット上の従来のHTTPではなく、Unixドメインソケット上のHTTPで公開されるのが一般的です。vert.xの活躍する場がここにあります。Dockerエンジンとの通信でブロッキングコードを使用している既製のjarを使う代わりに、vert.xの非同期クライアントを使った通信が可能になります。
vert.xは、Nettyが提供するネイティブなトランスポートライブラリが存在することを確認すると、そちらのトランスポートを使用します。“build.grade”ファイルと“module-info.jar”ファイルの両方に“netty-transport-native-kqueue”のようなネイティブトランスポートへの依存性を加えることで、この機能が有効になります。
vert.xがunixドメインソケット上でのhttpwをサポートしていない点には注意してください。この問題は近々、うまくいけば次期リリースで対処される予定です。当面はvert.xコアライブラリに簡単なコード修正を施して再構築することで、この問題を回避できます。Dcokerエンジンと通信するプラグインは次のようなものになります。
“netty-transport-native-kqueue-4.1.15.Final-osx-x86_64.jar”のような非モジュールjarを依存関係として加えると、自動的にモジュール名が生成されるのですが、jarファイルにJavaの予約語である“native”が含まれているため。コンパイル時にエラーとなります。
Nettyの開発者は次期リリースでこの問題に対処するとしていますが、jarのマニフェストファイルに“Automatic-Module-Name”エントリを加えれば、この問題を回避することができます。それにはまず、jarをフォルダに展開して、その中にcdします。次に“MANIFEST.MF”ファイルを編集して、エントリ“Automatic-Module-Name: io.netty.transport.kqueue”を追加します。それが終われば、次のコマンドを実行してjarファイルを生成します。
次のコマンドを実行することによって、マニフェストファイルで指定されている自動生成されたモジュール名がjavaによって認識されていることを確認できます。
自動生成されたモジュール名がJavaの予約語と干渉するその他の非モジュールjarについても、同じようなコマンドを使って修正する必要があります。
これまでの作業で、CIシステムの構築して実行する準備ができました。アプリケーションの実行には次のコマンドを使用します。
JPMSをサポートするため、“javac”や“java”といった既存のコマンドツールにも新しいオプションが追加されています。これらのオプションは、以前のクラスパスに代えて、モジュールパスとモジュールjarを使用するように、javaのコンパイラやランタイムに指示するものです。注目すべきオプションとしては、
“-p” と “--module-path” は、指定されたフォルダからjavaモジュールを検索するように、javaに指示するために使用します。
“-m” と “--module” は、アプリケーションを起動するモジュールとmainクラスを指定するためのものです。
今回の記事では、vert.xツールキットを使ってモジュール化されたマイクロサービスベースのアプリケーションを設計し、それに基づいてJPMSとJDK 9を使用したDockerネイティブなCIシステムを構築しました。具体的なコードをGitHubで参照して、vert.xとモジュールがコンパクトで独立性の高いモジュールJavaアプリケーションの構築にいかに適しているか、詳しく確認して頂ければと思います。
著者について
Uday Tatiraju氏はOracleのプリンシパルエンジニアとして、Eコマースプラットフォーム、検索エンジン、バックエンドシステム、Webおよびモジュールプログラミングの分野で10年近い経験を持っています。