跳到主要内容

JEP 330:启动单文件源代码程序

概括

增强java启动器以运行作为单个 Java 源代码文件提供的程序,包括通过“shebang”文件和相关技术在脚本内使用。

非目标

更改 Java 语言规范 (JLS) 或 javac 以适应 shebang 文件并不是我们的目标。同样,将 Java 语言发展为通用脚本语言也不是目标。

此 JEP 的目标不是更改 Java 语言规范以适应编写小程序的更简单方法,例如消除对标准public static void main(String[] args)方法的需要。然而,预计对 Java 语言的任何此类更改都可以与此功能结合使用。

动机

单文件程序(整个程序放在一个源文件中)在学习 Java 的早期阶段以及编写小型实用程序时很常见。在这种情况下,在运行程序之前必须编译程序是纯粹的仪式。此外,单文件程序可能会声明多个类,从而编译为多个class文件,这为“运行此程序”的简单目标增加了打包开销。最好能够使用java启动器直接从源代码运行程序:

java HelloWorld.java

描述

从 JDK 10 开始,java启动器以三种模式运行:启动类文件、启动 JAR 文件的主类或启动模块的主类。这里我们添加了一种新的第四种模式:启动在源文件中声明的类。

源文件模式是通过考虑命令行上的两项来确定的:

  1. 命令行上的第一项既不是选项也不是选项的一部分。 (换句话说,之前是类名的项目。)
  2. 版本选项(如果存在--source

如果“类名”标识具有.java扩展名的现有文件,则选择源文件模式,并编译和运行该文件。该--source选项可用于指定源代码的源版本。

如果文件没有.java扩展名,则--source必须使用该选项强制源文件模式。这是针对诸如源文件是要执行的“脚本”并且源文件的名称不遵循 Java 源文件的正常命名约定等情况。 (请参阅下面的“shebang”文件。)

使用该选项时,还必须使用该--source选项来指定源代码的源版本。 --enable-preview(参见JEP 12。

在源文件模式下,效果就好像源文件被编译到内存中,并且执行在源文件中找到的第一个类。例如,如果名为 的文件HelloWorld.java包含名为 的类hello.World,则命令

java HelloWorld.java

非正式地相当于

javac -d <memory> HelloWorld.java
java -cp <memory> hello.World

原始命令行中源文件名后面放置的任何参数都会在执行时传递给已编译的类。例如,如果名为 的文件Factorial.java包含一个名为Factorial计算其参数的阶乘的类,则命令

java Factorial.java 3 4 5

非正式地相当于

javac -d <memory> Factorial.java
java -cp <memory> Factorial 3 4 5

在源文件模式下,任何附加命令行选项均按如下方式处理:

  • 启动器扫描源文件之前指定的选项以查找相关的选项,以便编译源文件。这包括:--class-path--module-path--add-exports--add-modules--limit-modules--patch-module--upgrade-module-path以及这些选项的任何变体形式。它还包括JEP 12--enable-preview中描述的新选项。

  • 没有规定将任何附加选项传递给编译器,例如-processor-Werror

  • 命令行参数文件(@-files)可以标准方式使用。 VM 或正在调用的程序的长参数列表可以放置在通过在文件名前添加字符前缀来在命令行上指定的文件中@

在源文件模式下,编译过程如下:

  • 与编译环境相关的任何命令行选项都会被考虑在内。

  • 没有找到并编译其他源文件,就好像源路径设置为空值一样。

  • 注释处理被禁用,就像-proc:none有效一样。

  • 如果通过选项指定了_版本_,则该值将用作编译--source隐式选项的参数。--release这设置了编译器接受的源版本和源文件中的代码可以使用的系统API。

  • 源文件是在未命名模块的上下文中编译的。

  • 源文件应包含一个或多个顶级类,其中第一个类作为要执行的类。

  • 编译器不强制执行JLS §7.6末尾定义的可选限制,即命名包中的类型应存在于名称由类型名称后跟扩展名组成的文件中.java

  • 如果源文件包含错误,则适当的错误消息将写入标准错误流,并且启动程序以非零退出代码退出。

在源文件模式下,执行过程如下:

  • 要执行的类是在源文件中找到的第一个顶级类。它必须包含标准方法的声明public static void main(String[])

  • 已编译的类由自定义类加载器加载,该类加载器委托给应用程序类加载器。 (这意味着应用程序类路径上出现的类不能引用源文件中声明的任何类。)

  • 已编译的类在未命名模块的上下文中执行,并且就像--add-modules=ALL-DEFAULT有效一样(除了--add-module可能已在命令行上指定的任何其他选项之外。)

  • 命令行上文件名后面出现的任何参数都会main以明显的方式传递给标准方法。

  • 如果应用程序类路径中存在与要执行的类同名的类,则会出错。

请注意,使用简单的命令行(例如java HelloWorld.java.以前,HelloWorld.java会被解释为java在名为 的包中调用的类HelloWorld,但现在已解析为有利于调用的文件(HelloWorld.java如果存在此类文件)。鉴于这样的类名和这样的包名都违反了几乎普遍遵循的命名约定,并且考虑到这样的类不太可能位于类路径上并且类似名称的文件位于当前目录中,这似乎是可以接受的妥协。

执行

源文件模式需要模块的存在jdk.compiler。当请求文件的源文件模式时Foo.java,启动器的行为就像命令行被转换为:

java [VM args] \
-m jdk.compiler/<source-launcher-implementation-class> \
Foo.java [program args]

源启动器实现类以编程方式调用编译器,该编译器将源编译为内存中的表示形式。然后,源启动器实现类创建一个类加载器来从内存中的表示加载已编译的类,并调用main(String[])在源文件中找到的第一个顶级类的标准方法。

源启动器实现类可以访问任何相关的命令行选项,例如定义类路径、模块路径和模块图的选项,并将这些选项传递给编译器以配置编译环境。

如果调用的类抛出异常,则该异常将被传递回启动器以正常方式进行处理。但是,导致类执行的初始堆栈帧将从异常的堆栈跟踪中删除。其目的是,异常的处理类似于类由启动器本身直接执行的处理。初始堆栈帧在对堆栈的任何直接访问中都是可见的,包括(例如)Thread.dumpStack()

用于加载已编译类的类加载器本身对引用类加载器定义的资源的任何 URL 使用特定于实现的协议。获取此类 URL 的唯一方法是使用getResource或等方法getResources;不支持从字符串创建任何此类 URL。

“Shebang”文件

当手头的任务需要一个小型实用程序时,单文件程序也很常见。在这种情况下,希望能够使用“#!”直接从源代码运行程序。 Unix 衍生系统(例如 macOS 和 Linux)上的机制。这是操作系统提供的一种机制,允许将单文件程序(例如脚本或源代码)放置在任何方便命名的可执行文件中,该文件的第一行以 开头,#!并指定要“执行”的程序的名称“文件的内容。此类文件称为“shebang 文件”。

希望能够用这种机制来执行Java程序。

使用源文件模式调用 Java 启动器的 shebang 文件必须以以下内容开头:

#!/path/to/java --source version

例如,我们可以获取“Hello World”程序的源代码,并将其放入名为 的文件中hello,在初始行 后#!/path/to/java --source 10,然后将该文件标记为可执行文件。然后,如果该文件位于当前目录中,我们可以使用以下命令执行它:

$ ./hello

或者,如果该文件位于用户 PATH 中的目录中,我们可以使用以下命令执行它:

$ hello

命令的任何参数都会传递给main所执行的类的方法。例如,如果我们将计算阶乘的程序的源代码放入名为 的 shebang 文件中factorial,我们可以使用如下命令执行它:

$ factorial 6

--source在以下情况下,必须在 shebang 文件中使用该选项:

  • shebang 文件的名称不遵循 Java 源文件的标准命名约定。
  • 需要在 shebang 文件的第一行指定其他 VM 选项。在这种情况下,--source应首先指定选项,然后指定可执行文件的名称。
  • 需要指定文件中源代码所使用的 Java 语言的版本。

shebang 文件也可以由启动器显式调用,也许还可以使用其他选项,使用如下命令:

$ java -Dtrace=true --source 10 factorial 3

Java 启动器的源文件模式为 shebang 文件提供了两种适应方式:

  1. 当启动器读取源文件时,如果该文件不是Java源文件(即不是名称以 结尾的文件.java)并且第一行以 开头#!,则该行的内容直到但不包括第一行在确定要传递给编译器的源代码时,换行符将被忽略。第一行之后出现的文件内容必须包含_Java 语言规范_CompilationUnit版本中第 7.3 节定义的有效内容,该版本适合选项中给出的平台版本(如果存在)或如果该选项不存在,则用于运行程序的平台。--source``--source

    第一行末尾的换行符将被保留,以便任何编译器错误消息中的行号在 shebang 文件中都有意义。

  2. 某些操作系统将可执行文件名称后面第一行的文本作为可执行文件的单个参数传递。考虑到这一点,如果启动器遇到一个以空格开头--source且包含空格的选项,它会被分成一系列由空格分隔的单词,然后由启动器进一步分析。这允许将附加参数放在第一行上,尽管某些操作系统可能会对行的总长度施加限制。不支持使用引号来保留此类值中的空格。

无需对 JLS 进行任何更改即可支持此功能。

在 shebang 文件中,前两个字节必须是0x23 0x21,即 的两字符 ASCII 编码#!。所有后续字节均使用有效的默认平台字符编码读取。

#!仅当需要使用操作系统的 shebang 机制执行文件时才需要第一行开头。当显式使用 Java 启动器来运行源文件中的代码时,不需要任何特殊的第一行,如上面给出的HelloWorld.javaFactorial.java示例中所示。事实上,不允许使用 shebang 机制来执行遵循 Java 源文件标准命名约定的文件。

备择方案

这种现状已经持续了 20 多年;我们可以继续这样做。

#!可以将支持 shebang 文件的系统配置为使用不同的前缀,例如//!,而不是使用。这样的前缀将被视为javac单行注释,不需要任何特殊处理即可忽略它。然而,在 macOS 和 Linux 等操作系统上引入新的幻​​数需要对此类系统进行手动或自动更新,并且超出了本 JEP 的范围。

可以编写一个包含 Java 源代码的 shell 脚本作为可传递到 Java 源启动器的此处文档,而不是使用 shebang 机制。虽然这最终是比 shebang 机制更灵活的机制,但它也比在简单情况下使用 shebang 的开销更大。

我们可以创建一个源启动器,但可以将其称为 以外的其他名称java,例如jrun.考虑到启动器已有的执行模式数量,这可能会被视为无端差异。

我们可以将“一次性运行”的任务委托给该jshell工具。虽然这乍一看似乎很明显,但这是 设计中明确的非目标jshell。该jshell工具被设计为交互式外壳,并且做出了许多设计决策以支持提供更好的交互体验。如果让它承受批处理运行程序的额外限制,就会降低交互体验。

我们也可以使用该jrunscript工具。然而,该工具提供的用于与运行时环境交互的设施有限,并且没有满足提供使用 Java 的简单介绍的愿望。