Java 9のその先へ~JavaOne Conference 2017レポート

第3回Java 9のモジュール機能で何が変わるのか[JavaOne2017]

2017年9月にリリースされたJava 9にはさまざまな新機能が追加されていますが、中でもとりわけ影響度が大きいのがモジュール機能です。この機能の導入にともなって、JDK 9には旧バージョンとの互換性を伴わないいくつかの修正が加わっているからです。すなわち、既存のアプリケーションやライブラリがJava 9ではそのまま動かない可能性がある、ということです。

10月1日から5日の5日間に渡って開催されたJavaOne 2017でも、このモジュール機能は大きなトピックのひとつとして挙げられ、多くのセッションが開かれました。本レポートでは、それらのセッションで紹介された内容も踏まえながら、Java 9のモジュール機能についていま一度おさらいします。

モジュール機能の追加に至る紆余曲折

Javaへのモジュール機能の追加に関する議論がスタートしたのは10年以上も前のことになります。Javaアプリケーションの多様化やJava仕様そのものの巨大化によって、従来のパッケージの仕組みだけではクラスライブラリの適切な構造化や管理が難しくなったというのがその発端です。この問題は「Jar地獄」などと呼ばれ、Java 9のリリースに至るまで何度となく語られてきました。

さて、Javaのモジュール機能としては最初にJava SE 7に向けて2つのJSR(JSR 277とJSR 294)が提案されましたが、これらは実現には至りませんでした。その後、2009年にJava SEのスペックリードであるMark Reinhold氏が、モジュール機能の仕様化と実装を行うプロジェクトとしてProject Jigsawを立ち上げます。正式なJSRはJSR 376: Java Platform Module Systemです。

これが最終的にJava 9につながるわけですが、実際にはその道は非常に険しいものでした。結果としてJigsawは、Java SE 7および8のリリースには間に合わず、実に8年を経てようやくJava 9に含まれることになったわけです。これだけ時間がかかった理由はさまざまですが、最も大きな要因としては、Jigsawの影響範囲が極めて広く、既存のAPIやライブラリに大きく関係する変更だったからということが挙げられます。

Jigsawの仕様はこの8年の間に二転三転しています。Java 9リリースの直前にも、JCPでの最終投票がこのJigsawが原因となって一旦否決されており、細かな仕様の調整が行われました。したがって、Java 9のモジュール機能を使う場合には、必ず正式リリース以降の情報を参照することをお勧めします。

8年に渡る長い道のり 左がMark Reinhold氏
8年に渡る長い道のり 左がMark Reinhold氏

モジュール機能によってできること

Project Jigsawによって何ができるようになるかということは、OpenJDKのProject Jigsawのページに詳しく掲載されています。Jigsawは複数のJEP(JDK Enhancement Proposal)から構成されていますが、簡単にまとめると次のようなことができるようになるということです。

  • モジュール間の依存関係の明確化
  • モジュールの公開範囲の設定
  • バージョン設定
  • 標準ライブラリのモジュール化
  • 必要なモジュールのみを含むランタイムの作成

大雑把に言えば、複数のクラスをひとつのモジュールとしてまとめて、その公開範囲の設定や、バージョンの設定、モジュール間の依存関係の設定などを行うことができるということになります。そのうえで、標準ライブラリ自体もこの仕組みを使って複数のモジュールに分割し、依存関係や公開範囲が明確なるように作り直そうというわけです。つまり、標準ライブラリの構成がこれまでとはガラリと変わります。

たとえば、Java SEのコア部分のAPIは下図のような26個のモジュールに分割されました。矢印はモジュール間の依存関係を表しています。

Java SEのコアAPIのモジュール構成
Java SEのコアAPIのモジュール構成

JDK 9には上記の他に、Java SEのコアAPIに含まれないJDKのAPIを提供するモジュールや、Java FX関連のAPIを提供するモジュールなどが含まれています。これにともなって、JDKのAPIドキュメントもモジュール単位での閲覧ができるように修正されています。どのパッケージがどのモジュールに含まれているかなどはAPIドキュメントを参照してください。

最も基本的な事項のおさらい

それでは、まずは最も基本的な内容をおさらいしましょう。JavaOne 2017ではモジュール機能の基礎を扱うセッションが多数行われましたが、そのうち中からOracleのAlex Buckley氏によるセッション「Modular Development with JDK 9」で解説された内容を紹介します。

モジュールは、アプリケーションのルートディレクトリに配置するmodule-info.javaというファイルで定義します。モジュール定義に使用するおもなキーワードは以下の3つです。

  • module - モジュールの宣言をする
  • exports - 公開するパッケージを指定する
  • requires - 依存するモジュールを指定する

次の図のコードでは、hello.worldというモジュールを宣言しています。hello.worldモジュールはjava.baseモジュールに依存し、外部のすべてのモジュールに対してcom.example.helloパッケージを公開します。このサンプルではjava.baseモジュールを明示的にrequires指定していますが、実際にはjava.baseは全てのモジュールが暗黙的に依存するモジュールなので、requires宣言は省略することが可能です。

モジュール定義におけるexportsとrequires
モジュール定義におけるexportsとrequires

コンパイルは下図の右下のように、javacコマンドにmodule-info.javaを含める形で行います。この例では、com.example.helloパッケージのSayHello.javaクラスをコンパイルしています。もし依存するモジュールがある場合には、-pでそのモジュールのディレクトリのパスを指定します。

モジュール付きのコンパイル
モジュール付きのコンパイル

実行時には、javaコマンドに-pで必要なモジュールがあるディレクトリのパスを、-mでメインとなるモジュール名またはクラス名を指定します。この例では、モジュールのパスとしてmodsを、メインのモジュールとしてhello.worldが指定されています。hello.worldのexportsされたパッケージにはmain()メソッドが1つのみあるので、それが起動時に呼び出されるメソッドになります。

モジュールの実行
モジュールの実行

なお、モジュール化されたアプリケーションを実行する場合には、依存関係に関する次のようなルールがあります。

  • 依存するモジュールが足りない場合は実行できない
  • モジュールは循環依存になっていていはいけない(AとBがお互いに依存し合うなど)
  • 1つのパッケージが複数のモジュールにまたがってはいけない

既存のアプリケーションをモジュール対応にするには

Java 8以前に作られた既存のアプリケーションやライブラリをモジュールに対応させるには、まずそのアプリケーションやライブラリがどんなモジュールに依存しており、どの機能を外部に公開するのかを明確にすることから始まります。

クラスファイルやライブラリ、モジュールの依存関係を調べるには、jdepsコマンドを使うと便利です。次の例は、myapp.jarとmylib.jarについて依存関係を調べたものです。

jdepsコマンドでjarファイルの依存関係を調べる
jdepsコマンドでjarファイルの依存関係を調べる

mylib.jarはjava.baseモジュールに依存しており、myapp.jarはjackson-core-2.6.2.jar、jackson-databind-2.6.2.jar、mylib.jar、java.baseモジュール、java.sqlモジュールにそれぞれ依存していることがわかります。jackson-core-2.6.2.jarとjackson-databind-2.6.2.jarはサードパーティ製のライブラリです。もしこれらのライブラリがすでにモジュールに対応している場合(モジュール定義情報がある場合)には、それぞれのmodule-info.javaはたとえば次のように書けばいいということになります。

mylibのモジュール定義の例
mylibのモジュール定義の例
myappのモジュール定義の例
myappのモジュール定義の例

しかし、もし依存しているjarファイルの中にモジュール定義情報がないものが含まれていた場合はどうでしょうか。モジュール定義情報が無いjarファイルは、内部的に"automatic module"と呼ばれるモジュールとして使えるようになっています。automatic moduleは次のような特徴を持っています。

  • モジュール名はMANIFEST.MFのAutomatic-Module-Nameの設定によって決まる。MANIFEST.MFに記載が無い場合はjarファイル名が使われる
  • すべてのパッケージが外部にexportsされている状態になる
  • すべての他のモジュールをrequiresしている状態になる

このautomatic moduleの仕組みによって、モジュール対応が済んでいないライブラリも、元のjarファイルに手を加えることなくモジュール化されたアプリケーションで使うことができるようになっています。

標準ライブラリがモジュール化された影響

Java 9への対応でもう1つ注意しなくてはならないことが、前述のようにJDKの標準ライブラリそのものが小さなモジュールに分割され、公開範囲や依存関係が明確に指定されているということです。中でもとくに気をつけなくてはいけない点として、次の2つの項目が挙げられています。

注意点1:Java EE関連APIのモジュールが非推奨になった

従来のJDKにはJava EEに由来するサーバサイド向けのAPIがいくつか含まれていましたが、それらが非推奨となり、将来的にはJava SE自体からも削除されます。現在はまだJava SEの一部ではありますが、デフォルトでは読み込まれないモジュールになりました。対象のモジュールは次のようなものです。

  • java.activation
  • java.corba
  • java.transaction
  • java.xml.bind
  • java.xml.ws
  • java.xml.ws.annotation

注意点2:JDKの内部クラスが利用できなくなる

JDKには本来は一般的に利用されることを想定していない、内部の処理のみで使うためのクラスが含まれています。しかし従来のパッケージベースのアクセス権管理だけでは、言語仕様として利用不可とすることはできなかっため、現実にはそのような内部クラスを利用しているアプリケーションやライブラリが多数存在します。

これらの内部クラスは、Java 9ではモジュール機能によって外部公開されないように設定されました。したがって、もし利用しているアプリケーションやライブラリは、Java 9に移行するにあたって代替のクラスを使うように修正する必要があります。

最もわかりやすい変更は、これまでJDKの標準APIのパッケージがまとめられていた「rt.jar」というファイルが無くなりました。したがってこのファイルを直接クラスパスに指定して実行しているようなアプリケーションは起動できません。また、⁠sun.security」のようなパッケージもアクセスできなくなっているため、代替のものに置き換える必要があります。

モジュールでSPIを使用する

Java 9では、SPI(Service Provider Interface)の仕組みにもモジュールを適用することができます。SPIは、APIとしてはインタフェースのみが定義され、それに対して第三者が実装を提供できるようにするための仕組みです。ネットワーク関連APIやJDBCなど、標準APIだけでは実装方法が確定しないような場面で幅広く活用されています。

このSPIにモジュールを適用する場合、module-info.javaにはどのように依存関係を記述すればいいのでしょうか。ここでは、同じくAlex Buckley氏によるセッション「Modules and Services」で解説された内容を紹介します。

Alex Buckley氏
Alex Buckley氏

例として挙げられたのは次図のようなケースです。

サービスの関係
サービスの関係

サービスインタフェースとしてPrinterServiceLookupがあり、printlibモジュールのFastPrintクラスがその実装を提供します。一方でjava.desktopモジュールのPrinterクラスは、PrinterServiceLookupを利用する側のクラスです。

このとき、提供側となるprintlibモジュールでは、モジュール定義の中でprovides節を使ってPrintServiceLookupの実装を提供することを宣言します。provides節ではwithを使って実装クラスを指定します。一方利用側のとなるjava.desktopモジュールでは、uses節でPrinterServiceLookupを利用することを宣言します。このほかに、モジュール間の依存関係や公開範囲(requires/exports)の設定も必要です。

usesとprovidesを使ってサービスの利用側/提供側の宣言をする
usesとprovidesを使ってサービスの利用側/提供側の宣言をする

このように宣言することで、利用側はSPIで提供されるサービスローダの仕組みを経由してPrintServiceLookupの実装であるFastPrintクラスを利用できるようになるとのことです。次の図は、サービスローダ経由で実装クラスを取得するコードの例です。

サービス利用側のコード例
サービス利用側のコード例

従来のSPIでは、/META-INF/services/以下にインタフェースの実装クラスの情報を記述しておく必要がありましたが、モジュール定義情報がある場合はこれが不要になります。ただし、互換性のために残しておくこともできるとのことです。

Jigsaw breaks "some" things.

このように、Java 9への移行はモジュール化への対応とセットで考える必要があります。そのためにはモジュール機能に関する知識を得るのと同時に、これまでと何が異なるっているのかもよく把握しておかなければなりません。

Java 9への移行について、Mark Reinhold氏はキーノートで次のようにまとめています。

Jigsawは既存のアプリケーションの多くのものを壊すことがありませんが、多少の影響は与えます。もしあなたのコードがJava SE 8の標準APIのみを使っている場合には、ほぼ修正することなくJDK 9でも動作するでしょう。しかし、もしJDKの内部クラスに依存しているような場合には、修正が必要になるかもしれません

"Jigsaw breaks some things."
Jigsaw breaks some things.

Java 9のモジュールが実際の現場に浸透するまでにはまだしばらく時間がかかると思われます。とはいえ、何度も書いているようにこれは既存アプリケーションが動かなくなる可能性もある大きな変更です。できるだけ早く対応を進める必要があるでしょう。

JavaOne 2017
https://www.oracle.com/javaone/

おすすめ記事

記事・ニュース一覧