JEP 320の悪夢 – JDK 11は史上最悪のJDKかもしれない

先に結論から述べると、現時点においてJDK 11はJakarta EE (ex. Java EE) サーバーにとって史上最悪のJDKリリースになる可能性があります。それを裏付けるかのごとく、Jakarta EEサーバーのJDK 11対応予定時期は各ベンダーとも順調に遅れており、最も早くても2019年春以降となる見通しです。一部のベンダーは対応を断念する可能性さえあります。これはJDK 11の非互換性、具体的にはJEP 320の影響に起因します。

既に日本オラクルや、日本Javaユーザグループをはじめとする国内のJavaコミュニティによる周知の通り、JDK 8からJDK 11にかけて2度に渡り後方互換性を損なう変更が実施されています。

  • JDK 9: Javaへのモジュール・システム (JSR 376: Java Platform Module System, a.k.a. Project Jigsaw) の導入とそれに伴う非公開APIの削除、一部公開APIの内部仕様変更
  • JDK 11: JEP 320に基づく、Java SE APIからJava EE関連APIの一部を削除 (※当該APIはJava SE 9にて非推奨化およびJava SE 11での削除を予告済み)

国内のJavaコミュニティではもっぱら前者について影響範囲や対応策が議論されましたが、Jakarta EE (ex. Java EE) サーバーに深刻な影響を及ぼす変更は実は後者です。コミュニティにはJakarta EE (ex. Java EE) サーバーのアーキテクチャを熟知している技術者が極めて少なく、議論の旗振り役となった技術者が後者の影響について過小評価していたことも問題の顕在化を遅らせた一因です。

JEP 320で削除対象とされたAPIは、具体的にはJAX-WSと関連API (JAXBを含む) やCORBA関連APIが中心となっています。これらのうち特にJAXBはJava SEにおいても重宝されていたAPIであっただけに、その削除についてはOpenJDKプロジェクト内部でも反対意見が根強くあったことは想像に難くありません。しかし最終的には「必要であれば外部APIとして追加すれば良い」との結論からJDK 9での非推奨化を経てJDK 11での削除に至りました。この結論が、私には「軽率極まりない判断」と映りました。

実際に、Java SEアプリケーションに限定すれば、JAXBを外部APIとして追加することで問題はほぼ解決します。しかし、Jakarta EE (ex. Java EE) においては一筋縄ではいかないのです。

もしJakarta EE (ex. Java EE) サーバーのアーキテクチャに熟知していないとしても、Java EE関連APIがJava SEから削除されることによる影響は、Jakarta EE (ex. Java EE) サーバーがAPIを使用可能にする仕組みからある程度は推測が可能です。以下、厳密さを少し犠牲にしますができるだけ平易に説明します。

Jakarta EE (ex. Java EE) サーバーでは、Java SE APIおよびJakarta EE (ex. Java EE) APIの双方を使用します。そして、Jakarta EE (ex. Java EE) サーバーがAPIを読み込むタイミング (Javaでは「クラスローディング」と呼んでいます) は大きく (1) Java SE API、(2) Java EE API、(3) Java EEアプリケーション、の3段階あります (実際には各段階がさらに細かく分かれていますが、ここでは割愛します)。そして優先順位についても原則としてこの通りとなります。既存の Java EE サーバーに対する問い合わせの中でも「クラスローディング」に関わるものは比較的多く、Jakarta EE (ex. Java EE) サーバー全体を見ても重要かつデリケートな部分となっています。

以下、Payara Server 5.183 (JDK 11非対応) を具体例として、JEP 320Jakarta EE (ex. Java EE) サーバーに与える悪影響について考えます。

Payara Server 5.183をJDK 9以降で起動を試みると、JAXBが見つからない旨のエラーメッセージを伴って起動することができないはずです。JDK 9 & 10ではJVMオプションで非推奨のJAXBを取り込む手段が用意されています (実際には他の要因も重なりPayara Serverは起動しません) が、JDK 11以降ではJAXB自体が削除されているためそれができません。対応策として最初に思いつくのはJAXBをPayara Server側に追加することですが、ここで問題となってくるのが前述のAPIの呼び出しタイミングと優先順位です。

Payara Server 5.183はOpenJDK 8でビルドを行っているため、Payara Server 5.183はJAXBがJDK側に含まれている前提でAPIの読み込みを試みます。ところがOpenJDK 11にはJAXBが含まれないため、当然ながらこの読み込みは失敗します。Payara Server 5にJAXBを追加した上でOpenJDK 11でビルドすれば、JDK 11環境においてこの問題は解決しますが、その一方でJDK 8で動作させることが不可能になってしまいます。Payara Server 5のビルドをJDK 8向けとJDK 11向けで別途用意するという方法も考えられるでしょうが、メンテナンスコストが高騰するため現実的な選択肢ではありません (Payara Serverの各バージョンはリリースから10年間のサポートを提供するためです)。

より現実的な解はPayara Server 5側にJAXBを追加した上で、これをOpenJDK 8でビルドすることです。しかし単純にJAXBを追加しただけではJDK 8上でPayara Server 5を実行した際に意図しない動作を引き起こす場合があります。

Payara Server 5をJDK 8上で起動した場合には、まず最初にJDK 8に含まれるJAXBがJava SE APIとして読み込まれます。続いてPayara Serverに追加したJAXBがさらにJava EE APIとして読み込まれるため、都合2つのJAXB実装が同一のPayara Server上に共存する状態が発生します。その結果、ある状況ではJDK側のJAXB実装が使用され、また別の状況ではPayara Server側のJAXB実装が使用されるという事態が起こり得ます。そして、もし2つのJAXB実装にわずかでも差分があればJAXB API呼び出しで確実にコンフリクトします。

補足: Javaのクラスの同一性は、以下の3点すべてを充足していることにより保証されます。つまり以下のいずれか1つでも充足していない場合、それは実行時に別クラスとして見なされます。

  • クラス定義 (パッケージ名を含む) が完全に同一であること
  • コンパイル後のクラス・ファイルが同一バージョンであること
  • 同一のクラスローダーによってロードされていること

これはApache CommonsGoogle Guavaなどの有名なライブラリを使用するアプリケーションで頻発している問題と類似しています (JPMSはこの問題を回避する手段も提供しています) が、より基盤部分で発生し得る (すなわちサーバーのダウンを引き起こす可能性がある) という点でより深刻な問題となります。

他のAPIについても同様の問題を抱えており、さらにJVMオプションの非互換性が加わるため、Jakarta EE (ex. Java EE) サーバーの視点に立つと、JDK 8とJDK 11ソースレベルおよびバイナリレベルの双方で互換性を持たないランタイムとして映ります。JPMSについては後方互換性のためのオプション (automatic moduleなど) を用いることで対処は可能です。しかしJEP 320に関してはクラスローディングの段階で互換性が破壊されるため、問題の解決は非常に困難となります。

Payara ServerのJDK 11対応作業は、あるベースラインにおいてすべてのソースファイルをOpenJDK 11でビルド可能となるよう修正した後、オリジナル (OpenJDK 8でのビルドを想定したもの) との差分を抽出して、当該箇所をJDK 8とJDK 11の双方で問題なくビルド・実行できるよう何らかのギミックを仕掛けるものと予想されます。かなり大がかりで、かつ地味な作業になりますが、おそらく現状考えうる最善策だと思われます。

加えて、Jakarta EE (ex. Java EE) サーバーにとってJPMSはあまり有益なものではありません。Jakarta EE (ex. Java EE) サーバーはJava SEを満遍なく使用するためJDKのフットプリント削減は見込めません。また、サーバー実装は既にOSGiなどによるモジュール化が行われているため、新たにJPMSでモジュール化する必然性は薄いと言えます。