[Перевод] Запускаем однофайловые программы в Java 11 без компилирования
Пусть исходный файл HelloUniverse.java содержит определение класса и статичный метод main
, который выводит в терминал одну строку текста:
public class HelloUniverse
}
Обычно для запуска этого класса требуется сначала скомпилировать его с помощью Java-компилятора (javac), который создаст файл HelloUniverse.class:
mohamed_taman$ javac HelloUniverse.java
Затем нужно с помощью команды виртуальной машины Java (интерпретатора) запустить получившийся файл:
mohamed_taman$ java HelloUniverse
Hello InfoQ Universe
Тогда сначала запустится виртуалка, которая загрузит класс и исполнит код.
Или вы новичок в Java (в данном случае это ключевой момент) и хотите поэкспериментировать с языком? А если вам нужно быстро проверить фрагмент кода? Описанные два этапа могут всё усложнить.
В сочетании с jshell получается прекрасный набор инструментов для обучения начинающих. В Java SE 11 можно напрямую запускать одиночные исходные файлы без промежуточной компиляции.
Эта возможность особенно полезна для новичков, которые хотят поработать с простыми программами.
На наш взгляд, лучше автоматизировать многие задачи, вроде написания Java-программ в виде скриптов с последующим исполнением из оболочки ОС. Профессионалы могут с помощью этих инструментов изучать нововведения в языке или тестировать незнакомые API. Поговорим об этом подробнее во второй части статьи. В результате мы можем гибко работать с shell-скриптами и пользоваться всеми возможностями Java.
Давайте обсудим. Эта прекрасная возможность Java 11 позволяет напрямую исполнять одиночный исходный файл без компилирования.
Что вам потребуется
Для запуска кода, приведённого в статье, вам понадобится версия Java не ниже 11. На момент написания статьи текущим релизом был Java SE Development Kit 12.0.1 — финальная версия находится здесь, достаточно принять условия лицензии и кликнуть на ссылку для вашей ОС. Если хотите поэкспериментировать с самыми свежими возможностями, то можете скачать JDK 13 early access.
Обратите внимание, что сейчас также доступны релизы OpenJDK разных вендоров, в том числе AdoptOpenJDK.
В этой статье мы будем вместо Java IDE применять обычный текстовый редактор, чтобы избежать всякой магии IDE, и использовать командную строку Java прямо в терминале.
Запускаем .java с помощью Java
Функция JEP 330 (запуск однофайловых программ с исходным кодом) появилась в JDK 11. Она позволяет напрямую исполнять исходные файлы с исходным Java-кодом, без использования интерпретатора. Исходный код компилируется в памяти, а затем исполняется интерпретатором без создания на диске .class-файла.
Вы не можете исполнять сразу несколько исходных файлов. Однако эта функция ограничена кодом, который хранится в одном файле.
Никаких ограничений на их количество нет. Чтобы обойти это ограничение, все классы нужно определять в одном файле. Кроме того, пока они находятся в одном файле, не имеет значения, являются ли они публичными или частными.
То есть важна очерёдность. Первый класс, определённый в файле, будет считаться основным, и в него нужно поместить метод main.
Первый пример
Начнём с классического простейшего примера — Hello Universe!
Мы продемонстрируем описываемую возможность на разных примерах, чтобы вы получили представление, как можно её использовать в повседневном программировании.
Затем удалите его, сейчас поймёте зачем: Создайте файл HelloUniverse.java с кодом из начала статьи, скомпилируйте и запустите получившийся class-файл.
mohamed_taman$ rm HelloUniverse.class
Если теперь с помощью Java-интерпретатора вы запустите class-файл без компиляции:
mohamed_taman$ java HelloUniverse.java
Hello InfoQ Universe
то увидите тот же результат: файл будет исполнен.
Мы передаём сам исходный код, а не class-файл: система внутри себя компилирует его, запускает и показывает в консоли сообщение. Это означает, что теперь можно просто выполнить java HelloUniverse.java
.
И в случае её ошибки мы получим уведомление об этом. То есть под капотом всё же выполняется компиляция. Можете проверить структуру директорий и убедиться, что class-файл не генерируется, компиляция выполняется в памяти.
Теперь давайте разберёмся, как это всё устроено.
Как интерпретатор Java выполняет программу HelloUniverse
В JDK 10 модуль запуска Java может работать в трёх режимах:
- Исполнение class-файла.
- Исполнение основного класса из JAR-файла.
- Исполнение основного класса модуля.
А в Java 11 появился четвёртый режим:
- Исполнение класса, объявленного в исходном файле.
В этом режиме исходный файл компилируется в памяти, а затем выполняется первый класс из этого файла.
Система определяет ваше намерение ввести исходный файл по двум признакам:
- Первый элемент в командной строке не является ни опцией, ни частью опции.
- В строке может присутствовать опция
--source <vеrsion>
.
В первом случае Java сначала выяснит, является ли первый элемент команды опцией или её частью. Если это имя файла, заканчивающееся .java, тогда система будет считать его исходным кодом, который нужно скомпилировать и запустить. Вы также можете добавлять в Java-команду опции перед именем исходного файла. Например, если нужно задать путь класса, когда исходный файл использует внешние зависимости.
Во втором случае выбирается режим работы с исходным файлом, и первый элемент в командной строке, который не является опцией, считается исходным файлом, который нужно скомпилировать и запустить.
Если файл не имеет расширения .java, то нужно использовать опцию --source
, чтобы принудительно перейти в режим работы с исходным файлом.
Это важно в случаях, когда исходный файл представляет из себя «скрипт», который нужно выполнить, а имя файла не соответствует обычным соглашениям о наименованиях исходных файлов с Java-кодом.
Об этом мы поговорим ниже. С помощью опции --source
можно определять версию языка исходника.
Можно ли передавать в командной строке аргументы?
Давайте расширим нашу программу Hello Universe, чтобы она выводила персональное приветствие любому пользователю, зашедшему на InfoQ Universe:
public class HelloUniverse2{ public static void main(String[] args){ if ( args == null || args.length< 1 ){
System.err.println("Name required");
System.exit(1); } var name = args[0]; System.out.printf("Hello, %s to InfoQ Universe!! %n", name); }
}
Сохраним код в файле Greater.java. Обратите внимание, что имя файла не соответствует имени публичного класса. Это нарушает правила спецификации Java.
Запустим код:
mohamed_taman$ java Greater.java "Mo. Taman"
Hello, Mo. Taman to InfoQ universe!!
Как видите, совершенно не важно, что имена класса и файла не совпадают. Внимательный читатель мог также заметить, что мы передали в код аргументы после обработки имени файла. Это означает, что любой аргумент в командной строке, идущий после имени файла, передаётся стандартному основному методу.
Определяем уровень исходного кода с помощью опции --source
Есть два сценария использования опции --source
:
- Определение уровня исходного кода.
- Принудительный перевод runtime-среды Java в режим работы с исходным файлом.
В первом случае, если вы не указали уровень исходного кода, за него принимается текущая версия JDK. А во втором случае файлы с расширениями, отличными от .java, можно передавать для компилирования и выполнения на лету.
Переименуем Greater.java просто в greater без расширения и попробуем выполнить: Давайте сначала рассмотрим второй сценарий.
mohamed_taman$ java greater "Mo. Taman"
Error: Could not find or load main class greater
Caused by: java.lang.ClassNotFoundException: greater
При отсутствии расширения .java интерпретатор команд ищет скомпилированный класс по имени, переданному в виде аргумента — это первый режим работы модуля запуска Java. Чтобы это не происходило, воспользуемся опцией --source
для принудительного переключения в режим работы с исходным файлом:
mohamed_taman$ java --source 11 greater "Mo. Taman"
Hello, Mo. Taman to InfoQ universe!!
Теперь перейдём к первому сценарию. Класс Greater.java совместим с JDK 10, поскольку содержит ключевое слово var
, но не совместим с JDK 9. Изменим source
на 10
:
mohamed_taman$ java --source 10 Greater.java "Mo. Taman"
Hello Mo. Taman to InfoQ universe!!
Снова запустим предыдущую команду, но в этот раз передадим --source 9
вместо 10
:
mohamed_taman$ java --source 9 Greater.java "Mo. Taman"
Greater.java:8: warning: as of release 10, 'var' is a restricted local variable type and cannot be used for type declarations or as the element type of an array
var name = args[0]; ^
Greater.java:8: error: cannot find symbol
var name = args[0]; ^ symbol: class var location: class HelloWorld
1 error
1 warning
error: compilation failed
Обратите внимание: компилятор предупреждает о том, что var
стала в JDK 10 ограниченным именем типа. Но поскольку у нас язык уровня 10, компиляция продолжается. Однако возникает сбой, потому что в исходном файле нет типа с именем var
.
Теперь рассмотрим использование нескольких классов. Всё просто.
Работает ли этот подход с несколькими классами?
Да, работает.
Код проверяет, является ли заданное строковое значение палиндромом. Рассмотрим пример с двумя классами.
Вот код, сохранённый в файле PalindromeChecker.java:
import static java.lang.System.*;
public class PalindromeChecker { public static void main(String[] args) { if ( args == null || args.length< 1 ){ err.println("String is required!!"); exit(1); } out.printf("The string {%s} is a Palindrome!! %b %n", args[0], StringUtils .isPalindrome(args[0])); }
}
public class StringUtils { public static Boolean isPalindrome(String word) { return (new StringBuilder(word)) .reverse() .toString() .equalsIgnoreCase(word); }
}
Запустим файл:
mohamed_taman:code$ java PalindromeChecker.java RediVidEr
The string {RediVidEr} is a Palindrome!! True
Запустим снова, подставив «RaceCar» вместо «MadAm»:
mohamed_taman:code$ java PalindromeChecker.java RaceCar
The string {RaceCar} is a Palindrome!! True
Теперь подставим «Mohamed» вместо «RaceCar»:
mohamed_taman:code$ java PalindromeChecker.java Taman
The string {Taman} is a Palindrome!! false
Как видите, можно добавлять в один исходный файл сколько угодно публичных классов. Следите только за тем, чтобы основной метод был определён первым. Интерпретатор будет использовать первый класс в качестве стартовой точки для запуска программы после компилирования кода в памяти.
Можно использовать модули?
Да, никаких ограничений. Скомпилированный в памяти код запускается как часть безымянного модуля с опцией --add-modules=ALL-DEFAULT
, которая даёт доступ ко всем модулям, поставляемым с JDK.
То есть код может использовать разные модули без необходимости явно определять зависимости с помощью module-info.java.
Обратите внимание, что эти API были представлены в Java SE 9 в качестве экспериментальной возможности, но теперь они имеют статус полноценной функции модуля java.net.http. Давайте рассмотрим код, делающий HTTP-вызов с помощью нового HTTP Client API, появившегося в JDK 11.
Обратимся к публичному сервису reqres.in/api/users?page=2. В этом примере мы вызовем простой REST API с помощью метода GET, чтобы получить список пользователей. Код сохраним в файл с именем UsersHttpClient.java:
import static java.lang.System.*;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandlers;
import java.net.*;
import java.io.IOException; public class UsersHttpClient{ public static void main(String[] args) throws Exception{
var client = HttpClient.newBuilder().build(); var request = HttpRequest.newBuilder()
.GET()
.uri(URI.create("https://reqres.in/api/users?page=2"))
.build(); var response = client.send(request, BodyHandlers.ofString());
out.printf("Response code is: %d %n",response.statusCode());
out.printf("The response body is:%n %s %n", response.body()); }
}
Запустим программу и получим результат:
mohamed_taman:code$ java UsersHttpClient.java
Response code is: 200
The response body is:
{"page":2,"per_page":3,"total":12,"total_pages":4,"data":[{"id":4,"first_name":"Eve","last_name":"Holt","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/marcoramires/128.jpg"},{"id":5,"first_name":"Charles","last_name":"Morris","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/stephenmoon/128.jpg"},{"id":6,"first_name":"Tracey","last_name":"Ramos","avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/bigmancho/128.jpg"}]}
Теперь вы можете быстро тестировать новые фичи, предоставляемые разными модулями, не создавая свой собственный модуль.
Почему скрипты так важны в Java?
Сначала давайте вспомним, что такое скрипты:
Скрипт — это программа, написанная для определённого runtime-окружения, которая автоматизирует исполнение задач или команд, которые человек может исполнять поочерёдно.
Из этого общего определения мы можем вывести простое определение скриптового языка — это язык программирования, использующий высокоуровневые конструкции для интерпретации и исполнения по одной команде (или команд) за раз.
Часто такие языки являются интерпретируемыми (а не компилируемыми) и придерживающимися процедурного стиля программирования (хотя некоторые скриптовые языки также обладают свойствами объектно-ориентированных языков). Скриптовый язык использует серии команд, записанных в файле.
К серверным скриптовым языкам относятся Perl, PHP и Python, а на клиентской стороне — JavaScript. В целом скриптовые языки легче в освоении и быстрее в наборе кода по сравнению с более структурированными компилируемыми языками вроде Java, C и С++.
Однако Java не так прост в изучении и прототипировании по сравнению с другими скриптовыми языками. Долгое время Java считался хорошо структурированным, сильно типизированным компилируемым языком, который интерпретируется виртуальной машиной для выполнения на любой вычислительной архитектуре.
В последних релизах добавили ряд новых возможностей, чтобы молодым программистам было легче изучать этот язык, а также чтобы пользоваться функциями языка и API без компилирования и IDE. Тем не менее, Java уже исполнилось 24 года, его использует около 10 млн разработчиков по всему миру. Например, в Java SE 9 появился инструмент JShell (REPL), который поддерживает интерактивное программирование.
А с выходом JDK 11 этот язык получил возможность поддержки скриптов, поскольку теперь вы можете исполнять код с помощью простого вызова команды java
!
В Java 11 есть два основных способа использования скриптов:
- Прямой вызов команды
java
. - Применение *nix-скриптов для командной строки, аналогичных Bash-скриптам.
Первый вариант мы уже рассмотрели, теперь разберёмся со вторым. Он открывает нам много возможностей.
Shebang-файлы: запускаем Java как shell-скрипт
Итак, в Java SE 11 появилась поддержка скриптов, включая традиционные shebang-файлы из мира *nix. Для их поддержки не потребовалось спецификации языка.
Это ASCII-кодировка символов #!.. В shebang-файле первые два байта должны быть 0x23 и 0x21. Все последующие байты в файле читаются на основе действующей по умолчанию на данной платформе системы кодировки.
Это означает, что нам не нужна какая-то особенная первая строка, когда модуль запуска Java явно используется для запуска кода из исходного файла, как в случае с HelloUniverse.java. Таким образом, чтобы файл исполнился с помощью встроенного в ОС shebang-механизма, необходимо соблюсти лишь одно требование: чтобы первая строка начиналась с #!..
14. Запустим следующий пример в терминале, работающем под macOS Mojave 10. Но сначала определим важные правила, которым нужно следовать при создании shebang-файла: 5.
- Не смешивать Java-код с кодом скриптового языка оболочки вашей ОС.
- Если вам нужно добавить опции виртуальной машины, необходимо после имени исполняемого файла в shebang-файле первой опцией задать
--source
. К опциям виртуальной машины относятся:--class-path
,--module-path
,--add-exports
,--add-modules
,--limit-modules
,--patch-module
,--upgrade-module-path
, а также любые их вариации. Также в этот список могут включить новую опцию--enable-preview
, описанную в JEP 12. - Вы должны задать версию Java, которая используется в исходном файле.
- Первая строка файла должна начинаться с shebang-символов (#!). Например:
#!/path/to/java --source <vеrsion>
- Применительно к исходным Java-файлам НЕЛЬЗЯ использовать shebang-механизм для исполнения файлов, которые соответствуют стандартному соглашению о наименованиях (заканчиваются на .java)
- Вы должны пометить файл как исполняемый с помощью команды:
chmod +x <Filеname>.<Extеnsion>
.
Создадим shebang-файл (скриптовую программу), который выведет список содержимое директории, чьё имя будет передано в качестве параметра. Если никаких параметров не передаётся, по умолчанию будет взята текущая директория.
#!/usr/bin/java --source 11
import java.nio.file.*;
import static java.lang.System.*; public class DirectoryLister { public static void main(String[] args) throws Exception { vardirName = "."; if ( args == null || args.length< 1 ){
err.println("Will list the current directory"); } else { dirName = args[0]; } Files .walk(Paths.get(dirName)) .forEach(out::println); }
}
Сохраним код в файл с именем dirlist без расширения, а затем пометим его как исполняемый: mohamed_taman:code$ chmod +x dirlist
.
Запустим файл:
mohamed_taman:code$ ./dirlist
Will list the current directory
.
./PalindromeChecker.java
./greater
./UsersHttpClient.java
./HelloWorld.java
./Greater.java
./dirlist
Запустим снова с помощью команды, которая передаёт родительскую директорию, и проверим результат.
mohamed_taman:code$ ./dirlist ../
Примечание: при оценке исходного кода интерпретатор игнорирует shebang-строку (первую строку). Таким образом, shebang-файл можно явно вызвать с помощью модуля запуска, например, с дополнительными опциями:
$ java -Dtrace=true --source 11 dirlist
Также нужно отметить: если скриптовый файл лежит в текущей директории, то вы можете выполнить его так:
$ ./dirlist
А если скрипт лежит в директории, путь которой указан в пользовательском PATH, то выполнить его можно так:
$ dirlist
И в завершение дам несколько советов, о чём нужно помнить при использовании скриптов.
Советы
- Некоторые опции, которые вы будете передавать в javac, могут не передаться (или не распознаться)
java
, например, опции-processor
или-Werror
. - Если в classpath есть файлы .class и .java, то модуль запуска заставит вас использовать class-файл.
mohamed_taman:code$ javac HelloUniverse.java
mohamed_taman:code$ java HelloUniverse.java
error: class found on application class path: HelloUniverse - Помните о возможности конфликта имён класса и пакета. Взгляните на эту структуру директорий:
mohamed_taman:code$ tree
.
├── Greater.java
├── HelloUniverse
│ ├── java.class
│ └── java.java
├── HelloUniverse.java
├── PalindromeChecker.java
├── UsersHttpClient.java
├── dirlist
└── greaterОбратите внимание на два файла
java.java
в пакете HelloUniverse и файл HelloUniverse.java в той же директории. Если вы попробуете выполнить:mohamed_taman:code$ java HelloUniverse.java
то какой файл будет выполнен первым, а какой вторым? Модуль запуска больше не ссылается на class-файл в пакете HelloUniverse. Вместо этого он загрузит и выполнит исходный файл HelloUniverse.java, то есть будет запущен файл в текущей директории.
Shebang-файлы открывают массу возможностей по созданию скриптов для автоматизации всевозможных задач с использованием средств Java.
Резюме
Начиная с Java SE 11 и впервые в истории программирования вы можете напрямую исполнять скрипты с Java-кодом без компилирования. Это позволяет писать скрипты на Java и исполнять их из *nix-командной строки.
Поэкспериментируйте с этой функцией и поделитесь знанием с другими.