JEP 330: Launch Single-File Source-Code Programs
Запуск программ из одного файла исходного кода
| Ответственный | Jonathan Gibbons |
| Тип | Feature |
| Область | JDK |
| Статус | Closed / Delivered |
| Выпуск | 11 |
| Компонент | tools / javac |
| Обсуждение | compiler dash dev at openjdk dot java dot net |
| Связан с | JEP 458: Launch Multi-File Source-Code Programs |
| Рецензенты | Alan Bateman, Alex Buckley, Mandy Chung |
| Одобрен | Brian Goetz |
| Создан | 2017/12/01 19:04 |
| Обновлён | 2023/11/02 21:46 |
| Задача | 8192920 |
Аннотация
Доработать лаунчер java, чтобы он мог запускать программу, представленную одним файлом исходного кода на Java, в том числе из скрипта с помощью файлов с «shebang» и похожих приёмов.
Что не является целью
Целью не является изменение ни Java Language Specification (JLS, спецификация языка Java), ни javac ради поддержки файлов с shebang. Также целью не является превращение языка Java в скриптовый язык общего назначения.
Целью этого JEP не является изменение Java Language Specification ради более простых способов писать небольшие программы, например чтобы стандартный метод public static void main(String[] args) стал необязательным. Однако ожидается, что любые такие изменения языка Java можно будет использовать вместе с этой возможностью.
Мотивация
Программы из одного файла, в которых вся программа умещается в одном исходном файле, часто встречаются на ранних этапах изучения Java и при написании небольших служебных программ. В этом случае необходимость компилировать программу перед запуском — чистая формальность. Кроме того, программа из одного файла может объявлять несколько классов и поэтому компилироваться в несколько файлов class, что добавляет хлопот с упаковкой к простой задаче «запустить эту программу». Желательно иметь возможность запускать программу прямо из исходного кода с помощью лаунчера java:
java HelloWorld.java
Описание
Начиная с JDK 10 лаунчер java работает в трёх режимах: запуск файла класса, запуск главного класса JAR-файла или запуск главного класса модуля. Здесь мы добавляем новый, четвёртый режим: запуск класса, объявленного в исходном файле.
Режим исходного файла определяется по двум элементам командной строки:
- Первый элемент командной строки, который не является ни опцией, ни частью опции. (Иными словами, элемент, который раньше был именем класса.)
- Опция
--sourceversion, если она указана.
Если «имя класса» указывает на существующий файл с расширением .java, выбирается режим исходного файла, и этот файл компилируется и запускается. С помощью опции --source можно указать версию исходного кода.
Если у файла нет расширения .java, для принудительного включения режима исходного файла нужно использовать опцию --source. Это нужно, например, когда исходный файл — исполняемый «скрипт» и его имя не следует обычным соглашениям об именовании исходных файлов Java. (См. файлы с «shebang» ниже.)
Когда используется опция --enable-preview, версию исходного кода также нужно указывать опцией --source. (См. 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и все варианты записи этих опций. Сюда же относится новая опция--enable-preview, описанная в JEP 12. -
Возможности передать компилятору какие-либо дополнительные опции, например
-processorили-Werror, не предусмотрено. -
Файлы аргументов командной строки (@-файлы) можно использовать обычным образом. Длинные списки аргументов как для VM, так и для вызываемой программы можно поместить в файлы, которые указываются в командной строке с символом
@перед именем файла.
В режиме исходного файла компиляция выполняется так:
-
Учитываются все опции командной строки, относящиеся к среде компиляции.
-
Никакие другие исходные файлы не ищутся и не компилируются, как если бы путь к исходным файлам был пустым.
-
Обработка аннотаций отключена, как если бы была указана опция
-proc:none. -
Если опцией
--sourceуказана version, это значение используется как аргумент неявной опции--releaseпри компиляции. Так задаются и версия исходного кода, которую принимает компилятор, и системный API, который может использовать код в исходном файле. -
Исходный файл компилируется в контексте безымянного модуля.
-
Исходный файл должен содержать один или несколько классов верхнего уровня; первый из них считается классом, который нужно выполнить.
-
Компилятор не применяет необязательное ограничение, описанное в конце JLS §7.6, согласно которому тип в именованном пакете должен находиться в файле, имя которого состоит из имени типа и расширения
.java. -
Если в исходном файле есть ошибки, соответствующие сообщения об ошибках выводятся в стандартный поток ошибок, и лаунчер завершается с ненулевым кодом выхода.
В режиме исходного файла выполнение происходит так:
-
Выполняется первый класс верхнего уровня, найденный в исходном файле. Он должен содержать объявление стандартного метода
public static void main(String[]). -
Скомпилированные классы загружает специальный загрузчик классов, который делегирует загрузку загрузчику классов приложения. (Отсюда следует, что классы из class path приложения не могут ссылаться на классы, объявленные в исходном файле.)
-
Скомпилированные классы выполняются в контексте безымянного модуля и так, как если бы была указана опция
--add-modules=ALL-DEFAULT(в дополнение к любым другим опциям--add-module, которые могли быть указаны в командной строке.) -
Все аргументы, указанные в командной строке после имени файла, передаются стандартному методу
mainочевидным образом. -
Если в class path приложения есть класс с тем же именем, что и у выполняемого класса, это считается ошибкой.
Обратите внимание: при использовании простой командной строки вида java HelloWorld.java возможна небольшая неоднозначность. Раньше HelloWorld.java интерпретировалось бы как класс с именем java в пакете с именем HelloWorld, а теперь неоднозначность разрешается в пользу файла с именем HelloWorld.java, если такой файл существует. Поскольку и такое имя класса, и такое имя пакета нарушают почти повсеместно соблюдаемые соглашения об именовании и поскольку маловероятно, что такой класс окажется в class path, а файл с похожим именем — в текущем каталоге, этот компромисс кажется приемлемым.
Реализация
Для режима исходного файла нужен модуль jdk.compiler. Когда запрашивается режим исходного файла для файла Foo.java, лаунчер ведёт себя так, как если бы командная строка была преобразована в:
java [VM args] \
-m jdk.compiler/<source-launcher-implementation-class> \
Foo.java [program args]
Класс, реализующий запуск из исходного кода, программно вызывает компилятор, который компилирует исходный код в представление в памяти. Затем этот класс создаёт загрузчик классов, загружающий скомпилированные классы из этого представления в памяти, и вызывает стандартный метод main(String[]) первого класса верхнего уровня, найденного в исходном файле.
Класс, реализующий запуск из исходного кода, имеет доступ ко всем важным опциям командной строки, например к тем, что задают class path, module path и граф модулей, и передаёт эти опции компилятору для настройки среды компиляции.
Если вызванный класс выбрасывает исключение, это исключение передаётся обратно лаунчеру и обрабатывается обычным образом. Однако начальные кадры стека, ведущие к выполнению класса, удаляются из трассировки стека исключения. Цель в том, чтобы исключение обрабатывалось так же, как если бы класс выполнил непосредственно сам лаунчер. Начальные кадры стека будут видны при любом прямом доступе к стеку, в том числе (например) через Thread.dumpStack().
Загрузчик классов, который загружает скомпилированные классы, сам использует зависящий от реализации протокол для любых URL, указывающих на ресурсы, определённые этим загрузчиком классов. Получить такие URL можно только с помощью методов вроде getResource или getResources; создание такого URL из строки не поддерживается.
Файлы с «shebang»
Программы из одного файла также часто встречаются, когда для текущей задачи нужна небольшая служебная программа. В этом случае желательно иметь возможность запускать программу прямо из исходного кода с помощью механизма «#!» в Unix-подобных системах, таких как macOS и Linux. Это механизм операционной системы, который позволяет поместить программу из одного файла (например, скрипт или исходный код) в исполняемый файл с любым удобным именем, первая строка которого начинается с #! и указывает имя программы, которая «выполнит» содержимое файла. Такие файлы называются «файлами с shebang».
Желательно иметь возможность выполнять программы на Java с помощью этого механизма.
Файл с shebang, вызывающий лаунчер Java в режиме исходного файла, должен начинаться примерно так:
#!/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 в двух отношениях:
-
Когда лаунчер читает исходный файл, если файл не является исходным файлом Java (т. е. его имя не оканчивается на
.java) и если первая строка начинается с#!, то содержимое этой строки до первого символа новой строки (не включая его) игнорируется при определении исходного кода, передаваемого компилятору. Содержимое файла после первой строки должно представлять собой корректнуюCompilationUnitв соответствии с §7.3 той редакции Java Language Specification (спецификация языка Java), которая соответствует версии платформы, указанной в опции--source, если она задана, или версии платформы, на которой запускается программа, если опция--sourceне задана.Символ новой строки в конце первой строки сохраняется, чтобы номера строк во всех сообщениях компилятора об ошибках соответствовали строкам файла с shebang.
-
Некоторые операционные системы передают текст первой строки, следующий за именем исполняемого файла, этому исполняемому файлу как один аргумент. С учётом этого, если лаунчер встречает опцию, которая начинается с
--sourceи содержит пробельные символы, она разбивается на последовательность слов, разделённых пробельными символами, и только затем анализируется лаунчером дальше. Так в первую строку можно поместить дополнительные аргументы, хотя некоторые операционные системы могут ограничивать общую длину строки. Использование кавычек для сохранения пробельных символов в таких значениях не поддерживается.
Для поддержки этой возможности изменения в JLS не требуются.
В файле с shebang первые два байта должны быть 0x23 0x21 — это двухсимвольная кодировка ASCII для #!. Все последующие байты читаются в действующей кодировке символов платформы по умолчанию.
Первая строка, начинающаяся с #!, нужна, только если файл требуется выполнять с помощью механизма shebang операционной системы. Если лаунчер Java явно используется для запуска кода из исходного файла, как в приведённых выше примерах HelloWorld.java и Factorial.java, никакая специальная первая строка не нужна. Более того, использовать механизм shebang для выполнения файлов, которые следуют стандартному соглашению об именовании исходных файлов Java, не разрешается.
Альтернативы
Текущее положение дел работало более 20 лет; можно было бы его и сохранить.
Вместо #! можно было бы настроить системы, поддерживающие файлы с shebang, на использование другого префикса, например //!. Такой префикс javac воспринимал бы как однострочный комментарий, и для его игнорирования не требовалась бы никакая специальная обработка. Однако введение нового магического числа в таких операционных системах, как macOS и Linux, требует ручного или автоматического обновления этих систем и выходит за рамки этого JEP.
Вместо механизма shebang можно было бы написать shell-скрипт, содержащий исходный код Java в виде here-документа, который можно передать лаунчеру исходного кода Java. Хотя в конечном счёте это более гибкий механизм, чем shebang, в простых случаях он также требует больше накладных расходов, чем использование shebang.
Можно было бы создать лаунчер исходного кода, но назвать его иначе, чем java, например jrun. Учитывая, сколько режимов выполнения у лаунчера уже есть, такое различие, скорее всего, воспринималось бы как неоправданное.
Можно было бы поручить задачу «разовых запусков» инструменту jshell. Хотя на первый взгляд это может показаться очевидным, при проектировании jshell это было явно исключено из целей. Инструмент jshell создавался как интерактивная оболочка, и многие проектные решения принимались в пользу более удобной интерактивной работы. Если нагрузить его дополнительными ограничениями, связанными с ролью средства пакетного запуска, это ухудшило бы интерактивную работу.
Можно было бы также использовать инструмент jrunscript. Однако этот инструмент предоставляет ограниченные средства взаимодействия со средой выполнения и не решает задачу простого знакомства с Java.