※本記事は、Simon RobertsとMikalai Zaikinによる”Quiz Yourself: Migrating to the Java Platform Module System (Advanced)“を翻訳したものです。


アプリケーションをJavaプラットフォーム・モジュール・システムに移行するときのコマンドライン

著者:Simon Roberts、Mikalai Zaikin 
2020年4月14日 | 本記事をPDFでダウンロード

その他の設問はこちらから

過去にこのクイズの設問に挑戦したことがある方なら、どれ1つとして簡単な問題はないことをご存じでしょう。クイズの設問は、認定試験の中でも難しいものに合わせています。「中級者向け」「上級者向け」という指定は、設問の難易度ではなく、対応する試験による分類です。しかし、ほとんどすべての場合において、「上級者向け」の方が難しくなります。設問は認定試験対策として作成しており、認定試験と同じルールの適用を意図しています。文章は文字どおりに解釈してください。回答者を引っかけようとする設問ではなく、率直に言語の詳細な知識を試すものだと考えてください。

Java SE 9より前のバージョンで開発したアプリケーションをJava SE 11に移行したいと思います。移行にあたっては、トップダウンとボトムアップの移行を考慮し、Java SE 8アプリケーションをモジュールに分割します。

アプリケーションをJavaプラットフォーム・モジュール・システム(JPMS)に移行することを命じられたと考えてください。前提は以下のとおりです。

  • app.jarおよびlib.jarという2つのJARファイルがある
  • メイン・クラスはapp.Mainという名前で、app.jarに格納されている
  • app.jarは、lib.jarの機能を使用する
  • これはボトムアップ移行であると知らされている

次のうち、移行中にこのアプリケーションの実行に使用できるコマンドラインはどれですか。1つ選んでください。

A. java –module-path lib.jar –add-modules ALL-MODULE-PATH –class-path app.jar app.Main
B. java –add-modules lib –class-path app.jar;lib.jar app.Main
C. java -cp app.jar -p lib.jar app.Main
D. java -p app.jar;lib.jar -m app/app.Main

解答:まず、Javaプラットフォーム・モジュール・システム(JPMS)の背景について確認します。システムの目的を知ることは、その使用方法を理解するうえで役立ちます。

Java 9で導入されたJPMSによって、JavaとJVMの重要なアーキテクチャの一部が改善されています。中でも特に重要なのは、個々のモジュール(モジュールは、JARファイルのライブラリと似たものだと考えてもよいでしょう)・レベルで強力なカプセル化が提供され、依存性管理が改善されている点です。

強力なカプセル化を行えば、個々のモジュールでその内容を保護し、外部からの(誤った)使用を防ぐことができます。これは、クラスのプライベート機能が保護されるのとよく似た仕組みです。ただし、モジュールは一般的に、リフレクション・メカニズムからの保護を目的としても使われる点が異なります。対照的に、通常、クラスのプライベート要素はリフレクションを通じた誤使用に対してぜい弱です。

依存性管理によって、必要なコンポーネントが所定の場所に存在することを開発者やランタイム・システムに保証するとともに、ランタイム・システムの動作が不要なコンポーネントによって妨害されることを防ぐことができます。なお、JPMSはビルド管理システムではない点に注意してください。

強力なカプセル化のもとでもモジュールを有用なものとするために、モジュールはその一部機能へのアクセスを許可しなければなりません。つまり、どの部分へのアクセスを許可する必要があるかを示すために、構成メカニズムを備える必要があります。この構成は、昔から使われているセキュリティの格言に従ったものになっています。すなわち、「明示的に許可されているもの以外は拒否」します。そのため、アクセスできないものを明示的に指定する必要はありません。

依存性管理をサポートするためには、どのモジュールがどのモジュールを必要とするかを判断できることが必要です。

以上の背景を踏まえれば、JPMSより前に作られた既存のプロジェクトを対象に、モジュールを使うように移行するという状況が発生することは避けられません。その状況に対処するためには、何が必要でしょうか。1つ言えるのは、すべての開発を停止し、すべての変更を一斉に行うという方法は現実的でないということです。もう1つは、サードパーティ製のライブラリには、すでにモジュール化されているものも、まだモジュールとしては利用できないものも存在する可能性があるということです。

以上の点から、大きく2つのアプローチが可能だと考えられます(実際はこれらを混ぜ合わせることになる可能性もあります)。モジュール化されているライブラリがあり、そのライブラリをまだモジュール化されていないアプリケーションで使いたいと思うかもしれません。その逆は、アプリケーションのモジュール化に手いっぱいで、ライブラリがまだモジュール化されていない場合でしょう。前者のアプローチをボトムアップ移行、後者をトップダウン移行と呼びます。いずれにせよ、Java 9以降を使っているのであれば、コアJavaライブラリはすでにモジュール化されています。

この設問では、ボトムアップ移行のアプローチを使用すると述べられています。つまり、アプリケーションの中核部はモジュール化されていないものの、その他のライブラリ(この場合はlib.jar)はモジュール化されているということになります。

ボトムアップ、トップダウンという用語は、コア・ドキュメントで明確に示されているものではありません。しかし、試験対象には含まれています。さらに、Mark Reinholdによる解説「The State of the Module System」と、2016年のJavaOneでのプレゼンテーション「Advanced Modular Development」という少なくとも2つのオラクル公式リソースに登場しています。

いずれのリソースも、Java 9が実際にリリースされるよりもかなり前の時点のものです。JPMSの細部にはその後変更された部分もありますが、前述の2つでは移行の概念が十分に説明されています。

したがって、この設問では、ボトムアップという言葉が以下を暗示していることを知っておくべきです。この点は、実際の試験にも当てはまります。

  • システムのライブラリには、モジュール化されるものもあれば、されないものもある
  • メイン・アプリケーションは(まだ)モジュール化されていない
  • 将来的には、さらに多くのコードがモジュール化される
  • メイン・アプリケーションのモジュール化は最後に行われ、それにより移行が完了する

実際には、このようにきれいで予測可能な形では進展しない可能性があることに注意してください。

おそらく、この設問の解答を考えるうえで最初に解決しなければならない問題は、モジュールが含まれているJARファイルがあるとすればそのファイルはどれなのか、そして従来型のJARファイルはどれなのかを判断することです。

設問で触れられていないため、現在が移行のどの段階にあるのかを100%断言することはできません(ただし、この点については、かなり妥当な解釈が存在します)。しかし、現在の段階を断言できないことは問題ではありません。

まず、「妥当な解釈」として、ライブラリはおそらくモジュールであり(そうでなければ、移行を始めてもいないことになります)、app.jar(ここにapp.Mainクラスが含まれていると記述されています)はおそらくモジュールではありません(そうでなければ、移行が完了したことになります)。

移行のどの段階にあるかがわからなくても問題がない理由の2つ目は、どの段階でも動作可能なコマンドは1つだけで、先ほどの解釈が正しい場合に限られるとわかるからです。それでは、lib.jarはモジュール化されていて、app.jarはモジュール化されていないという前提に立ったとき、どういうことが言えるかについて考えてみます。

このコードを実行するためには、JVMが次に示す重要な情報を取得できることが必要です。

  • 通常のクラスを探す場所
  • モジュールを探す場所
  • ロードするモジュール
  • プログラムを開始する方法

メイン・プログラムは従来型のJARファイルからロードしたクラスより起動されるという前提で考えれば、そのJARファイルはクラスパス上に存在していなければなりません。そうなるためには、app.jarファイルがクラスパス(-cp、-classpath、–class-pathのいずれかで指定します)上にあることが必要です。この条件を満たすのは、選択肢A、B、Cです。選択肢Dでは、-p(これは–module-pathの省略形です)を使ってapp.jarファイルをモジュール・パスで指定しています。

次にJVMが知る必要があるのは、モジュールをロードする場所です。ここでも、lib.jarはモジュールであるという前提を使用します。つまり、モジュール・パスにlib.jarが含まれていることが必要です。これを満たすのは、選択肢A、C、Dです。しかし、選択肢Bでは、lib.jarをクラスパスで指定しています。先ほどの前提が正しい場合、この配置は誤っていることになります。

モジュールを使うコードと従来のコードには、大きく違う点があります。非モジュール環境で実行中のコードがクラスパス上にある任意のクラスを呼び出した場合、そのクラスがロードされます。一方のモジュール環境では、JVMはモジュール・グラフ内にあるモジュールしか検索しません。

モジュール・システムにおいてJVMは、必要だと特定したすべてのモジュールのグラフを構築します。アプリケーションのメイン・クラスを含むモジュールから始まるグラフを考えてください。そのモジュールは他のモジュールが必要である(requires)と宣言しているため、それらのモジュールが依存性グラフに追加されます。次に、追加されたモジュールが必要とするすべてのモジュールを追加します。この処理が推移的に繰り返され、必要なすべてのモジュールのグラフが構築されます。当然のことながら、これらのモジュールも実際にモジュール・パス上に存在しなければなりません。

ここで繰り返します。JVMの実行中に、コードがあるクラスを呼び出した場合、JVMではこのグラフ上にあるモジュールのみをチェックします。これやや単純化されていますが、前述のとおり、JVMでは非モジュールのJARファイルに含まれるメイン・クラスでは動かない可能性があります。そういったメイン・クラスを使用できないのは、非モジュールJARファイルにはモジュール・ディスクリプタが含まれていないため、requiresディレクティブが含まれることはないからです。問題の本質は、requiresディレクティブがない場合、必要となるモジュールはどうすればわかるのかということです。つまり、この例を実行するためには、libモジュールの必要性を何らかの形で示す必要があります。もしそうしないならば、app.Mainでlibモジュールのクラスが必要となったときに、JVMは「class not found」エラーをスローします。

このエラーを解消するためには、コマンドラインにrequiresディレクティブと同等なものを明示的に追加する必要があります。これを行うのが、–add-modulesコマンドライン・オプションです。このオプションを使う場合は、(選択肢Bのように)明示的なモジュール名を指定することも、3つの特殊な値であるALL-DEFAULT、ALL-SYSTEM、ALL-MODULE-PATHのいずれかを指定することもできます。ALL-MODULE-PATHを指定した場合は、モジュール・パス上にあるすべてのモジュールがルート・モジュール・セットに追加されます(したがって、必要なすべてのモジュールのグラフを構築するために使われます)。この場合、強制的にlibモジュールが利用可能になるという効果があります。このコマンドライン・オプションは、選択肢Aと選択肢Bに存在します。このことから、先ほどの前提が正しい場合、選択肢CとDは誤りと考えられると結論づけることができます。

ただし、自信を持って結論を出すにはまだ早すぎます。そもそも、選択肢AとBのその他の部分は問題なく動作するでしょうか。前提は正しいということが証明できるでしょうか。

選択肢Aをもう少し詳しく見てみます。JVMでは、プログラムのエントリ・ポイントも知る必要があります。app.jarが、クラスパス上にある非モジュールのJARファイルである場合、javaコマンドの要素の最後(当然ながら、プログラム自体に引数がある場合は除きます)にメイン・クラスの完全修飾名を指定するだけで済みます。選択肢Aはまさにそのようになっています。そのため、先ほどの大前提が正しい場合、選択肢Aは問題なく動作します。

選択肢Bでは–add-modulesを使っており、モジュールlibを明示的に呼び出しています。この部分は問題ありません。しかし、lib.jarファイルがモジュール・パス上ではなく、クラスパス上にあります。通常のJARファイルは、モジュール・パス上に配置することでモジュールとして振る舞うことができますが、クラスパス上にある何かが名前付きモジュールのように振る舞うことは決してありません。そのため、lib.jarはモジュールで、app.jarはモジュールでないという前提が正しいかどうかにかからわらず、ここで使用している–add-modulesは機能しません。先ほど、lib.jarをクラスパス上に配置しても期待どおりには動作しないと述べましたが、ここまで来れば、選択肢Bに関する内容に矛盾があることがわかるでしょう。そのため、選択肢Bは誤りです。

選択肢Cは選択肢Aとよく似ています。省略形の-p使ってlib.jarをモジュール・パスに追加して、app.jarをクラスパスに追加しています(ここでも、省略形の-cpを使っています)。さらに、選択肢Aと同じようにしてメイン・エントリ・ポイントを呼び出しています。lib.jarをモジュール、app.jarを従来型として扱っているため、期待できる選択肢です。しかし、–add-modulesは使われていません。その結果、lib.jarのクラスはロードできないことになり、プログラムは実行できません。lib.jarがモジュールであるか従来型であるかは関係ありません。JARファイルがモジュール・パス上にあったとしても、そこに含まれるクラスを利用できるのは、そのモジュールがモジュール・グラフ上にある場合のみです。このことから、選択肢Cは誤りであることがわかります。

選択肢Dでは、app.jarをモジュール・パス上に配置しようとしています。app.jarがmodule-infoを含むモジュールであれば、これは機能するでしょう。しかし、その場合、移行が完了しているか、トップダウン型であることになってしまいます。この設問では、ボトムアップ移行が進行中であることが明記されているため、この選択肢はあり得ません。従来型のapp.jarをモジュール・パスに追加した場合、自動モジュールとして扱われます。しかし、コマンドラインに–add-modulesがやはり不足しています。そのため、選択肢Dは誤りです。

正解は選択肢Aです。


Simon Roberts

Simon Roberts:Sun Microsystemsがイギリスで初めてJavaの研修を行う少し前にSunに入社し、Sun認定Javaプログラマー試験とSun認定Java開発者試験の作成に携わる。複数のJava認定ガイドを執筆し、現在はフリーランスでPearson InformITにおいて録画やライブによるビデオ・トレーニングを行っている(直接またはO’Reilly Safari Books Onlineサービス経由で視聴可能)。OracleのJava認定プロジェクトにも継続的に関わっている。

 

Mikalai Zaikin

Mikalai Zaikin:ベラルーシのミンスクを拠点とするIBA IT ParkのリードJava開発者。OracleによるJava認定試験の作成に携わるとともに、複数のJava認定教科書のテクニカル・レビューを行っている。Kathy Sierra氏とBert Bates氏による有名な学習ガイド『Sun Certified Programmer for Java』では、3版にわたってテクニカル・レビューを務めた。