- JEP 330: Launch Single-File Source-Code Programs
- Source-file mode
- Source-file mode の処理の流れ
- ランタイム・オプション
- まとめ
JEP 330: Launch Single-File Source-Code Programs
Java11 より単一のファイルの Java コードを、明示的なコンパイル操作無しで直接実行できるようになりました。
小さなユーティリティプログラムなどをスクリプト言語のような取り回しで実行することができます。
ただし、public static void main(String[] args)
は依然として必要で、Java 言語自体をスクリプト言語として進化させるものではなく、あくまでも Java ランチャーにおける機能強化になります。
以下のようなソース・ファイルを Hello.java として作成します。
public class App { public static void main(String... args) { System.out.println("Hello World"); } }
java
コマンドから以下のように実行することができます。
$ java Hello.java Hello World
クラス名とファイル名の対応関係が不要な点に注意してください。
"Shebang" ファイルとすることで、java
コマンドをコマンドラインから指定することも不要にできます。
Hello
というファイルを作成して実行権限を付与します。
% touch Hello % chmod +x Hello
以下のように、ファイルの先頭に "Shebang" としてコマンドを指定します(コマンドのパスはそれぞれの環境に応じたパスを設定します)。
#!/usr/bin/java --source 11 public class App { public static void main(String... args) { System.out.println("Hello World"); } }
シェルスクリプトと同様に、以下のように実行することができます。
$ ./Hello Hello World
ソースファイルに渡すコマンドライン引数がある場合には、単純にファイル名の後に続けるだけです。
$ ./Hello <args>
Source-file mode
ここまでで見てきた java
コマンドによる実行は、Source-file mode と呼ばれます。
java
コマンドが Source-file mode で動作する条件は以下のいずれかです。
- コマンドライン中のクラス名が指定される場所で、
.java
拡張子のファイルが指定された --source version
オプションが指定された
java
コマンドのヘルプに有る通り、4つ目のモードが追加されたことになります。
Usage: java [options] <mainclass> [args...] (to execute a class) or java [options] -jar <jarfile> [args...] (to execute a jar file) or java [options] -m <module>[/<mainclass>] [args...] java [options] --module <module>[/<mainclass>] [args...] (to execute the main class in a module) or java [options] <sourcefile> [args] (to execute a single source-file program)
java
コマンドが Source-file mode で動作した場合、jdk.compiler モジュールのソース・ランチャー実装クラスがプログラム的にコンパイラを呼び出すことで、ソース・ファイルをインメモリでコンパイルしてクラスファイルをメモリ展開します。
ソース・ランチャー実装クラスはカスタム・クラスローダを作成してメモリ展開されたクラスファイルのメインメソッドを呼び出します。
Source-file mode の処理の流れ
具体的には、jdk.compiler モジュールにある com.sun.tools.javac.launcher.Main.java
が実行され、この中でソース・ファイルのコンパイルと実行が処理されます。
少し処理の流れを追ってみましょう。
com.sun.tools.javac.launcher.Main.java
のメインは以下のようになっています。
public static void main(String... args) throws Throwable { try { new Main(System.err).run(VM.getRuntimeArguments(), args); } catch (Fault f) { //... } }
ランタイム引数とコマンドライン引数で run()
メソッドを呼んでいます。
run()
メソッドでは、引数で指定されたソース・ファイルを compile()
でコンパイルして execute()
で実行する単純なものです。
public void run(String[] runtimeArgs, String[] args) throws Fault, InvocationTargetException { Path file = getFile(args); Context context = new Context(file.toAbsolutePath()); String mainClassName = compile(file, getJavacOpts(runtimeArgs), context); String[] appArgs = Arrays.copyOfRange(args, 1, args.length); execute(mainClassName, appArgs, context); }
compile()
でコンパイルして execute()
に先立ち、Context
をインスタンス化しています。
これは以下のようになっています。
private static class Context { private final Path file; private final Map<String, byte[]> inMemoryClasses = new HashMap<>(); Context(Path file) { this.file = file; } JavaFileManager getFileManager(StandardJavaFileManager delegate) { return new MemoryFileManager(inMemoryClasses, delegate); } ClassLoader getClassLoader(ClassLoader parent) { return new MemoryClassLoader(inMemoryClasses, parent, file); } }
インメモリでコンパイルとクラスロードを行う MemoryFileManager
と MemoryClassLoader
を扱っています。
これらのクラスは、同インナークラスとして定義されています。
compile()
メソッドは以下のようになっています。
private String compile(Path file, List<String> javacOpts, Context context) throws Fault { JavaFileObject fo = readFile(file); JavacTool javaCompiler = JavacTool.create(); StandardJavaFileManager stdFileMgr = javaCompiler.getStandardFileManager(null, null, null); try { stdFileMgr.setLocation(StandardLocation.SOURCE_PATH, Collections.emptyList()); } catch (IOException e) { throw new java.lang.Error("unexpected exception from file manager", e); } JavaFileManager fm = context.getFileManager(stdFileMgr); JavacTask t = javaCompiler.getTask(out, fm, null, javacOpts, null, List.of(fo)); MainClassListener l = new MainClassListener(t); Boolean ok = t.call(); if (!ok) { throw new Fault(Errors.CompilationFailed); } if (l.mainClass == null) { throw new Fault(Errors.NoClass); } String mainClassName = l.mainClass.getQualifiedName().toString(); return mainClassName; }
MemoryFileManager
を経由してコンパイル処理されます。
そして execute()
メソッドは以下のようになっています。
private void execute(String mainClassName, String[] appArgs, Context context) throws Fault, InvocationTargetException { System.setProperty("jdk.launcher.sourcefile", context.file.toString()); ClassLoader cl = context.getClassLoader(ClassLoader.getSystemClassLoader()); try { Class<?> appClass = Class.forName(mainClassName, true, cl); Method main = appClass.getDeclaredMethod("main", String[].class); int PUBLIC_STATIC = Modifier.PUBLIC | Modifier.STATIC; if ((main.getModifiers() & PUBLIC_STATIC) != PUBLIC_STATIC) { throw new Fault(Errors.MainNotPublicStatic); } if (!main.getReturnType().equals(void.class)) { throw new Fault(Errors.MainNotVoid); } main.setAccessible(true); main.invoke(0, (Object) appArgs); } catch (ClassNotFoundException e) { // ... } }
MemoryClassLoader
にてクラスがロードされて、ソース・ファイルを main.invoke()
で実行する流れです。
OpenJDKのコミットhttps://github.com/openjdk/jdk/commit/fe24730ed99f3b12bc0ae72de8654b96412427b7
が該当します。
ランタイム・オプション
com.sun.tools.javac.launcher.Main.java
の getJavacOpts()
を見るのが手っ取り早いです。
以下のようなランタイム・オプションが利用できます。
switch (opt) { // The following options all expect a value, either in the following // position, or after '=', for options beginning "--". case "--class-path": case "-classpath": case "-cp": case "--module-path": case "-p": case "--add-exports": case "--add-modules": case "--limit-modules": case "--patch-module": case "--upgrade-module-path": if (value == null) { value = runtimeArgs[++i]; } if (opt.equals("--add-modules") && value.equals("ALL-DEFAULT")) { // this option is only supported at run time; // it is not required or supported at compile time break; } javacOpts.add(opt); javacOpts.add(value); break; case "--enable-preview": javacOpts.add(opt); break; default: // ignore all other runtime args }
依存ライブラリなどがある場合は --class-path
で必要な jar を指定することで実行が可能です。
コンパイルされたクラスは --add-modules=ALL-DEFAULT
と指定されたものと同等で、無名モジュール(unnamed module)扱いとなります。
まとめ
Java11 で追加された Single-File Source-Code Programs について具体的な実装を交えて紹介しました。
クラス定義や public static void main(String[] args)
が依然として必要なため、スクリプト言語のと同等な扱いはできませんが、Java エコシステムとしての充実したライブラリを簡単な取り回しで利用できるのはうれしいものですね。