Хабрахабр

[Перевод] Запускаем однофайловые программы в 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 может работать в трёх режимах:

  1. Исполнение class-файла.
  2. Исполнение основного класса из JAR-файла.
  3. Исполнение основного класса модуля.

А в Java 11 появился четвёртый режим:

  1. Исполнение класса, объявленного в исходном файле.

В этом режиме исходный файл компилируется в памяти, а затем выполняется первый класс из этого файла.

Система определяет ваше намерение ввести исходный файл по двум признакам:

  1. Первый элемент в командной строке не является ни опцией, ни частью опции.
  2. В строке может присутствовать опция --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:

  1. Определение уровня исходного кода.
  2. Принудительный перевод 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 есть два основных способа использования скриптов:

  1. Прямой вызов команды java.
  2. Применение *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

И в завершение дам несколько советов, о чём нужно помнить при использовании скриптов.

Советы

  1. Некоторые опции, которые вы будете передавать в javac, могут не передаться (или не распознаться) java, например, опции -processor или -Werror.
  2. Если в classpath есть файлы .class и .java, то модуль запуска заставит вас использовать class-файл.

    mohamed_taman:code$ javac HelloUniverse.java
    mohamed_taman:code$ java HelloUniverse.java
    error: class found on application class path: HelloUniverse

  3. Помните о возможности конфликта имён класса и пакета. Взгляните на эту структуру директорий:

    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-командной строки.

Поэкспериментируйте с этой функцией и поделитесь знанием с другими.

Полезные источники

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»