torutkのブログ

ソフトウェア・エンジニアのブログ

Java Module Systemのmain classを持つJARファイル作成の落とし穴

発生した事象

IntelliJ IDEA で作成していたJavaFXアプリケーションがあります。ビルド成果物でJARを作成し、Main Classを指定すると、実行可能JARファイルが生成されます。

このアプリケーションのプロジェクトにmodule-info.javaを追加してJava Module System対応をしました。ビルドでJARファイルを生成し、そのJARファイルをコマンドラインから実行すると、−mオプションに明示的にmain classを指定しないとエラーとなってしまいます。実行可能JAR(-jarオプション)としての実行はできているのですが・・・

D:\work\myapp> java -p out\artifacts\myapp_jar -m com.torutk.myapp
モジュールcom.torutk.myappにModuleMainClass属性がありません。-m <module>/<main-class>を使用してください

生成されたJARファイルのMANIFEST.MFを見ると、Main-Class属性は記述されています。

ここで、エラーメッセージには「ModuleMainClass属性がありません」と出ているのがミソで、実行可能JARファイルのMANIFEST.MFに記述する「Main-Class属性」とは名前が似ているが異なっていた点に注意が向かなかったのが痛かったです。

調査

簡単なモジュールのサンプルを作り、次の2つの方法でJARファイルを生成します。

  1. MANIFEST.MFの雛形にMain-Class属性を記述し、jarコマンドの-mオプションで雛形を指定
  2. jarコマンドの--main-classオプションで実行するメインクラス名を指定
D:\work\myapp> jar --create --file out1\myapp.jar --manifest src\META-INF\MANIFEST.MF -C classes .
D:\work\myapp> jar --create --file out2\myapp.jar --main-class com.torutk.myapp.Main -C classes .

1つ目のjarファイル(out1\myapp.jar)は、エラーとなります。

D:\work\myapp> java -p out1 -m com.torutk.myapp
モジュールcom.torutk.myappにModuleMainClass属性がありません。-m <module>/<main-class>を使用してください

2つ目のjarファイル(out2\myapp.jar)は実行できます。

D:\work\myapp> java -p out2 -m com.torutk.myapp
Hello, Java Module System

この両者の違いを追うべく、2つのJARファイルの違いをwinmergeを使って確認したところ、MANIFEST.MFとmodule-info.classに差異が検出されました。

MANIFEST.MFは属性の記述順が異なるだけでした。 module-info.classは、javacでコンパイルした出力結果をjarコマンドでアーカイブしているだけと思い込んでいたので、jarファイルに含めるときに変更が行われていたとは少々驚きました。

javacでコンパイルしたあとに、jarコマンドでオプションを変えて2種類のjarを生成しています。これらのjarに含まれるmodule-info.classは、もとのjavacでコンパイルしたmodule-info.classとはサイズが異なっており、さらにそれぞれのJARファイルに格納されたmodule-info.classにも違いがありました。

今回の事象の原因となっている違いは、module-info.classファイルの中にModuleMainClass情報が含まれているか否かの違いです。

javapコマンドでmodule-info.classを調べる

jarコマンドの--describe-moduleオプションを指定すると、モジュール情報を確認できます。

D:\work\myapp> jar --describe-module --file out1\myapp.jar
com.torutk.myapp jar:file:///D:/work/myapp/out1/myapp.jar/!module-info.clas
requires java.base mandated
contains com.torutk.myapp
D:\work\myapp> jar --describe-module --file out2\myapp.jar
com.torutk.myapp jar:file:///D:/work/myapp/out2/myapp.jar/!module-info.clas
requires java.base mandated
contains com.torutk.myapp
main-calss com.torutk.myapp.Main

となり、--main-classオプションを指定して生成したJARファイルの中に含まれるmodule-info.classには、メインクラスに関する情報が追加されています。

--main-classオプションを指定して生成したJARファイルの中にあるmodule-info.classを取り出して、バイナリエディタで覗くと「ModuleMainClass」の文字列が確認できます。

また、このJARファイルから取り出したmodule-info.classをjavapにかけてみたところ、次の様にクラスファイルの中にModuleMainClassという情報が追加されていることが分かります。

D:\work\myapp> javap -v module-info.class
Classfile /D:/work/myapp/module-info.class
  Last modified 2020/03/10; size 266 bytes
  MD5 checksum 865c4ebb10b566b887dd80d3783de624
  Compiled from "module-info.java"
module com.torutk.myapp
  minor version: 0
  major version: 55
  flags: (0x8000) ACC_MODULE
  this_class: #2                          // "module-info"
  super_class: #0
  interfaces: 0, fields: 0, methods: 0, attributes: 4
Constant pool:
   #1 = Utf8               module-info
   #2 = Class              #1             // "module-info"
   #3 = Utf8               module-info.java
   #4 = Utf8               com.torutk.myapp
   #5 = Module             #4             // "com.torutk.myapp"
   #6 = Utf8               ModuleMainClass
   #7 = Utf8               com/torutk/myapp/Main
   #8 = Class              #7             // com/torutk/myapp/Main
   #9 = Utf8               ModulePackages
  #10 = Utf8               com/torutk/myapp
  #11 = Package            #10            // com/torutk/myapp
  #12 = Utf8               java.base
  #13 = Module             #12            // "java.base"
  #14 = Utf8               11.0.6
  #15 = Utf8               SourceFile
  #16 = Utf8               Module
{
}
SourceFile: "module-info.java"
Module:
  #5,0                                    // "com.torutk.myapp"
  #0
  1                                       // requires
    #13,8000                                // "java.base" ACC_MANDATED
    #14                                     // 11.0.6
  0                                       // exports
  0                                       // opens
  0                                       // uses
  0                                       // provides
ModuleMainClass: #8                     // com.torutk.myapp.Main
ModulePackages:
  #11                                     // com.torutk.myapp

結論

mainクラスの指定を省略して実行できるモジュールJARを生成するときは、jarコマンドの--main-classオプションを使って生成する必要があります。

この仕組みに言及している文献等

現在Java読書会BOFで主催しているJava読書会で読んでいる書籍「The Java Module System」には、4.5.3項 Defining an entry point に記載がありました(p.101 上4行目からの段落を抜粋)。

When jar is used to package class files into an archive, you can define a main class with --main-class ${class}, where ${class} is the fully qualified name of the class with the main method. It will recorded in the module descriptor and used by defaults as the main class when the module is the initial module for launching an application.

粗訳

jarコマンドを使ってクラスファイル群をアーカイブする際、--main-class ${class}を指定してメインクラスを定義することができます。${class}には、mainメソッドを持つクラスの完全限定クラス名(FQCN)を指定します。そして、これはモジュール記述子に記録され、モジュールがアプリケーション起動時の初期モジュールとなるときのデフォルトのメインクラスとして適用されます。

この箇所は、前回(2月1日)の読書会で読んだ範囲にあるのですが、悲しいかな全く記憶にありませんでした。

この仕組みへのビルドツールの対応状況