Взаимодействие между Java и JavaScript уже давно является целью сообщества Java. В Rhino, а затем и в Nashorn были предприняты две смелые попытки добавить взаимодействие сценариев к JDK и JVM. Теперь, благодаря GraalVM, появилась лучшая альтернатива для запуска кода JavaScript из приложений Java. Само взаимодействие быстрее, надежнее и более «нативное» (в отличие от встроенного). Для разработчиков взаимодействие проще реализовать. И в качестве бонуса: взаимодействие, которое позволяет GraalVM от Java до JavaScript, также доступно для любого другого языка, с которым может работать среда выполнения GraalVM, включая R, Ruby, Python и LLVM (C, C++, Rust, Swift и другие).

Выбрав GraalVM 1.0 (на основе JDK 8) в качестве среды выполнения, вы обеспечиваете совместимость Java с любым из языков, на которых может работать GraalVM. Для взаимодействия с JavaScript не требуется дополнительной настройки; если вы хотите обращаться к Python, вам сначала нужно установить graalpython.

19 ноября 2019 года мы увидим выпуск GraalVM 19.3 с поддержкой Java 11. Примечание. GraalVM можно внедрить в вашу виртуальную машину Java в качестве предпочтительного JIT-компилятора, повышая производительность большинства современных приложений Java.

В этой статье я расскажу о ряде сложностей, с которыми приходится сталкиваться, когда код Java взаимодействует с кодом JavaScript. В несколько возрастающих уровнях сложности я покажу:

  • Оценка фрагментов кода JavaScript
  • Загружайте исходники JavaScript из отдельных файлов и вызывайте определенные в них функции.
  • Обмен данными и объектами между Java и JavaScript
  • Разрешить коду JavaScript, вызываемому из Java, выполнять обратный вызов к объектам Java
  • Запускайте несколько потоков JavaScript параллельно

В следующей статье я расскажу о том, как сделать функциональность многофункционального модуля NPM доступной в моем Java-приложении. И после этого я рассмотрю маршрут в другом направлении в следующей статье: вызов Java из приложения Node (NodeJS) или JavaScript.

Я предполагаю, что среда выполнения GraalVM (19.2.1 — выпуск в октябре 2019 г.) настроена, и возьму ее оттуда. Исходники этой статьи находятся на GitHub: https://github.com/AMIS-Services/jfall2019-graalvm/tree/master/polyglot/java2js

1. Оцените фрагменты кода JavaScript

Пакет Graal org.graalvm.polyglot содержит большую часть того, что нам нужно для взаимодействия с Java на других языках. Используя класс Context из этого пакета, мы должны настроить контекст Polyglot в нашем коде. Затем мы можем использовать этот контекст для оценки любого фрагмента кода — в данном случае фрагмента js (имеется в видуJavaScript или ECMA Script). Команда print в этом фрагменте выполняется как System.out.println — вывод строки на системный вывод.

Когда результатом оценки фрагмента является объект — например, функция, как в строке 10 примера — этот объект возвращается в Java как значение полиглота. В зависимости от типа значения мы можем делать с ним разные вещи. В этом случае, поскольку код JavaScript разрешен для работы, значение полиглота в переменной helloWorldFunction может быть выполнено, как это делается в строке 13. Входные параметры исполняемого объекта передаются как параметры функции execute для Value, и результатом выполнения снова является значение Polyglot. В этом случае тип значения — String, и мы можем легко преобразовать его в строку Java.

2. Загружайте исходники JavaScript из отдельных файлов и вызывайте определенные в них функции

Вместо того, чтобы загрязнять наши исходные коды Java встроенным JavaScript (на мой взгляд, плохая практика), мы можем загружать исходные коды JavaScript из автономных файлов и оценивать их. Впоследствии мы можем получить доступ к объектам данных и функциям, определенным в этих файлах, из кода Java.

Снова создается полиглот-контекст. Затем объект File определяется для исходного файла JavaScript (расположенного в корневом каталоге приложения Java). Метод eval в контексте выполняется для объекта File для загрузки и оценки фрагмента исходного кода. Это добавит две функции fibonacci и squareRoot к объекту bindings в контексте Polyglot. Этот объект содержит записи для объектов, которые оцениваются как из встроенных фрагментов, так и из оцениваемых исходных файлов. Обратите внимание, что мы можем оценивать больше объектов File — чтобы загружать функции JavaScript и объекты данных из нескольких файлов.

Затем мы можем получить объект функции из объекта привязки в контексте и выполнить его.

3. Обмен данными и объектами между Java и JavaScript

Между мирами Java и JavaScript существует многоязычная золотая середина, слой интерфейса, к которому можно получить доступ с обеих сторон языкового барьера. Здесь мы находим объект bindings — в двустороннем взаимодействии. Этот объект привязки может использоваться для чтения, изменения, вставки и удаления элементов в самой верхней области действия языка. Мы уже видели объект привязки как карту, в которой хранятся все функции, полученные в результате оценки исходных кодов JavaScript, загруженных в наш контекст Polyglot в Java.

(Примечание. Кроме того, существуют объекты привязок полиглотов, которые можно использовать для обмена символами между хостом и несколькими гостевыми языками. Все языки имеют неограниченный доступ к привязкам полиглотов. Гостевые языки могут размещать и получать элементы через API для конкретных языков)

Есть несколько различных ситуаций, на которые мы могли бы обратить внимание. Например: когда оценивается фрагмент JavaScript, любая определяемая им функция или объект добавляется к объекту Bindings и, следовательно, доступна из Java. Здесь показан простой пример этого, где фрагмент кода JavaScript определяет константу PI, которая впоследствии становится доступной из Java:

и вывод из программы Java:

Вот пример Java, подготавливающий карту Java и помещающий эту карту в Bindings таким образом (как ProxyObject), который делает ее доступной как «обычный» объект JavaScript для кода JavaScript. Код JavaScript считывает значения из карты, а также добавляет собственное значение. Он также мог изменить или удалить записи на карте или с нее. Карта фактически открыта для чтения/записи из обоих миров — Java и JavaScript:

И вывод системы:

В следующем примере файл с данными в формате JSON загружается как ресурс JavaScript. Доступ к данным впоследствии осуществляется из Java.

То, как нам приходится иметь дело с массивами через языковые границы, не очень гладкое. Это можно сделать — и, возможно, лучшим способом, чем мне удалось обнаружить. Вот мой подход — файл JavaScript загружается и оценивается, в результате чего объект «страны» — массив объектов JavaScript — добавляется к объекту привязок. При извлечении в Java из объекта привязок мы можем проверить наличие ArrayElements в объекте Polyglot Value и выполнить итерацию по элементам ArrayElements. Каждый элемент — объект JavaScript — можно преобразовать в карту Java, а свойства можно прочитать:

Выход:

Примечание: вот как выглядит файл. Это не простой JSON — это JavaScript, который определяет переменную countries, используя данные, указанные в формате JSON — и копирует/вставляет из интернет-ресурса:

4. Разрешить коду JavaScript, вызываемому из Java, обращаться к объектам Java.

Если мы поместим Java-объекты в Bindings, то методы этих объектов можно будет вызывать из JavaScript. То есть: если мы указали в классе, что методы «доступны хосту». Простой пример этого сценария показан здесь:

Наше Java-приложение создало объект из класса FriendlyNeighbour и добавило этот объект в Bindings под ключом friend. Впоследствии, когда фрагмент JavaScript выполняется из Java, этот фрагмент может получить доступ к объекту friend на карте Bindings и вызвать метод для этого объекта.

Код Java-приложения показан здесь:

Класс FriendlyNeighbour довольно прост — за исключением аннотации @HostAccess, которая требуется, чтобы сделать метод доступным из языка встраивания.

Вывод, который мы получим в консоли, не должен вас удивить:

Это демонстрирует, что код JavaScript, вызванный из Java, обратился к миру Java — в частности, к методу в объекте, экземпляр которого был создан объектом Java, вызывающим JavaScript. Этот объект находится в том же потоке и является взаимно доступным. Результат вызова объекта Java из JS печатается на выходе и, конечно, также может быть возвращен в Java.

5. Запускайте несколько потоков JavaScript параллельно

Несколько контекстов JavaScript могут быть инициированы из приложения Java. Они могут быть связаны с параллельными потоками Java. Косвенно эти контексты JavaScript также могут работать параллельно. Однако они не могут получить доступ к одному и тому же объекту Java без надлежащей синхронизации в Java.

В этом примере объект Java cac (на основе класса CacheAndCounter) создается и добавляется к объекту привязок в двух разных объектах контекста JavaScript. Это один и тот же объект, доступный из двух областей JS. Каждый из двух контекстов JavaScript может выполнять код JS параллельно друг другу. Однако, когда два мира сталкиваются — потому что они хотят получить доступ к одному и тому же объекту Java (например, cac) — тогда им приходится использовать синхронизацию в коде Java, чтобы предотвратить возможные условия гонки.

Вот несколько сложный фрагмент кода, который содержит создание двух потоков, которые создают контекст JavaScript, используя один и тот же код JavaScript (не приводит к одному и тому же объекту JavaScript), и оба обращаются к одному и тому же объекту Java — объекту cac, который создается и добавляется к картам Binding в обоих контекстах JavaScript. Это позволяет «потокам» JavaScript даже взаимодействовать друг с другом, но это взаимодействие должно регулироваться синхронизацией на стороне Java.

Вывод показывает, что два потока выполняются параллельно. У них обоих есть случайный сон в их коде. Иногда основной поток получает несколько последовательных обращений к cac, а в других случаях второй поток получает доступ через несколько раундов. Оба они получают доступ к одному и тому же объекту cac из соответствующих контекстов JavaScript — даже эти контексты различны. Мы могли бы даже иметь один контекст JavaScript, взаимодействующий со вторым контекстом JavaScript, который выполняется через другой поток Java, через общий объект.

Для полноты картины код класса CacheAndCounter:

Ресурсы

Репозиторий GitHub с исходниками для этой статьи: https://github.com/AMIS-Services/jfall2019-graalvm/tree/master/polyglot/java2js

Почему сообщество Java должно использовать GraalVM — https://hackernoon.com/why-the-java-community-should-embrace-graalvm-abd3ea9121b5

Многопоточная совместимость Java ←→JavaScript в GraalVM https://medium.com/graalvm/multi-threaded-java-javascript-language-interoperability-in-graalvm-2f19c1f9c37b

#ЧТО?: GraalVM — RieckPIL — https://rieckpil.de/whatis-graalvm/

GraalVM: святой Грааль многоязычной JVM? — https://www.transposit.com/blog/2019.01.02-graalvm-holy/

JavaDocs для GraalVM Polyglot — https://www.graalvm.org/truffle/javadoc/org/graalvm/polyglot/package-summary.html

Документы GraalVM — Polyglot — https://www.graalvm.org/docs/reference-manual/polyglot/

Смешивание NodeJS и OpenJDK — Взаимодействие языков и вертикальная архитектура — Майк Хирн — https://blog.plan99.net/vertical-architecture-734495f129c4

Расширьте возможности своего приложения Java Spring с помощью науки о данных R Олег Шелаев — https://medium.com/graalvm/enhance-your-java-spring-application-with-r-data-science-b669a8c28bea

Архивы GraalVM на Medium — https://medium.com/graalvm/archive

Репозиторий GraalVM GitHub — https://github.com/oracle/graal

Веб-сайт проекта GraalVM — https://www.graalvm.org/

Первоначально опубликовано на https://technology.amis.nl 24 октября 2019 г.