torutkのブログ

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

JavaFXアプリケーションクラスにmainメソッドがなくてもよい訳

単独のJavaプログラムを起動するには、staticなmainメソッドを持つクラスをjavaコマンドに指定します。

public class Hello {
    public static void main(String... args) {
        System.out.println("Hello, world");
    }
}
~$ java Hello
Hello, world
~$

ところが、JDK 8でJavaFXアプリケーションを作った場合、mainメソッドがないのに実行できてしまいます。

import javafx.application.Application;
import javafx.stage.Stage;

public class HelloFx extends Application {
    @Override
    public void start(Stage stage) {
        stage.show();
    }
}
~$ java HelloFx

コンパイルされたHelloFx.classを調べても、mainメソッドはありません。

~$ javap HelloFx
Compiled from "HelloFx.java"
public class HelloFx extends javafx.application.Application {
  public HelloFx();
  public void start(javafx.stage.Stage) throws java.lang.Exception;
}

~$

javaコマンドに何か細工が施されて、JavaFXだけmainメソッドなしでも実行できるようになっているのでしょうか?
JDK 8のソースコードを取得して中を調べてみました。

~$ hg clone http://hg.openjdk.java.net/jdk8u/jdk8u; cd jdk8u; sh get_source.sh
destination directory: jdk8u
requesting all changes
adding changesets
adding manifests
adding file changes
added 1312 changesets with 1709 changes to 141 files
updating to branch default
85 files updated, 0 files merged, 0 files removed, 0 files unresolved
# Repositories:  corba jaxp jaxws langtools jdk hotspot nashorn 
    :(後略)
jdk8u$

javaコマンドのソースコードは、jdk/src/share/bin にあります。

  • jdk/src/share/bin/main.c
  • jdk/src/share/bin/java.c
    • JLI_Launch()
      • JVMをロード後、JVMInitに処理を渡す
  • jdk/src/windows/bin/java_md.c
    • JVMInit()
      • ShowSplashScreenを呼び、次にContinueInNewThreadに処理を渡す
  • jdk/src/share/bin/java.c
    • ContinueInNewThread()
      • ContinueInNewThread0に処理を渡す
  • jdk/src/windows/bin/java_md.c
    • ContinueInNewThread0()
      • スレッドを生成し、スレッドでJavaMainを実行
  • jdk/src/share/bin/java.c
    • JavaMain()
      • InitializationJVMを呼び、
      • LoadMainClassを呼び、[*1]
      • GetApplicationClassを呼び、
      • PostJVMInitを呼び
      • GetStaticMethodIDで[*1]で取得したmainClassからメソッド名"main"、シグニチャが"([Ljava/lang/String;)V"のメソッドを取得[*2]
      • CreateApplicationArgsを呼び
      • CallStaticVoidMethodで[*1]のmainClassをターゲットに[*2]のメソッドを実行

という流れです。

ここまでで、やはりmainメソッドがないとプログラム起動ができないことが分かります。メソッド名mainがハードコーディングされているので、ここが揺らぐことはないでしょう。
すると、次に怪しむ箇所はmainメソッドを呼び出すターゲットのmainClassです。これが差し替えられている疑いがあります。

そこで、jdk/src/share/bin/java.cにあるLoadMainClass関数を追いかけてみます。

  • jdk/src/share/bin/java.c
    • LoadMainClass()
      • GetLauncherHelperClassを呼び(これはsun.launcher.LauncherHelperクラスを返却)
      • このクラスのメソッドcheckAndLoadMainを実行

ここからJavaのコードに替わります。

  • jdk/src/share/classes/sun/launcher/LauncherHelper.java
    • checkAndLoadMain()

このメソッドのコメントを見ると、次の記述があります。

* 4. if no main method and if the class extends FX Application, then call
* on FXHelper to determine the main class to launch

訳すと、「もしmainメソッドが定義されておらず、かつ FXのApplicationを継承したクラスであれば、そのときは起動するメインクラスを決定するためFXHelperを呼び出す」ということです。

checkAndLoadMainメソッドの中から、関係しそうな処理を抜き出して見ます。

        if (mainClass.equals(FXHelper.class) ||
                FXHelper.doesExtendFXApplication(mainClass)) {
            // Will abort() if there are problems with the FX runtime
            FXHelper.setFXLaunchParameters(what, mode);
            return FXHelper.class;
        }

FXHelperは、LauncherHelperのネストクラスです。doesExtendFXApplicationメソッドは、引数に指定されたクラスが、javafx.application.Applicationクラスをスーパークラスに持つときにtrueを返します(次のコード)。

        private static boolean doesExtendFXApplication(Class<?> mainClass) {
            for (Class<?> sc = mainClass.getSuperclass(); sc != null;
                    sc = sc.getSuperclass()) {
                if (sc.getName().equals(JAVAFX_APPLICATION_CLASS_NAME)) {
                    return true;
                }
            }
            return false;
        }

そして、FXHelperクラスをリターンしています。FXHelperはmainメソッドを定義しています。
FXHelperのmainメソッドでは、com.sun.javafx.application.LauncherImplクラスのlaunchApplication(String, String, String[])メソッドを呼び出しています。

    public static void main(String... args) throws Exception {
        if (fxLauncherMethod == null
                || fxLaunchMode == null
                || fxLaunchName == null) {
            throw new RuntimeException("Invalid JavaFX launch parameters");
        }
        // launch appClass via fxLauncherMethod
        fxLauncherMethod.invoke(null,
                new Object[] {fxLaunchName, fxLaunchMode, args});
    }

このあとは、LauncherImplクラスのlaunchApplicationWithArgsメソッドが呼び出され、そこでJavaFXの初期化、もともとjavaコマンドで指定したアプリケーションクラスのロードとそのmainメソッド実行が行われます。mainメソッドがないときは、アプリケーションクラスをインスタンス化し、init()を呼び、Stageをインスタンス化し、startメソッドの引数に指定して呼び出しています。

まとめると

javaコマンドに指定したクラスがjavafx.application.Applicationのサブクラスだった場合、sun.launcher.LauncherHelper.FXHelperクラスのmainメソッドが呼ばれ、ここからcom.sun.javafx.application.LauncherクラスのlaunchApplicationに処理が移ります。

このため、javaコマンドに指定したJavaFXクラスにmainメソッドがなくても実行できることになります。