Хабрахабр

JavaScript, Java, какая теперь разница?

Вот она. На прошлом JPoint пообещал написать статью про использование GraalVM для смешивания Java и JS.

В повседневной практике часто встречаются приложения, состоящие из двух частей: JavaScript-фронтенд и Java-бэкенд. В чем проблема? Как правило, делают их люди с разных сторон баррикад, и при попытке залезть в чужую область они начинают страдать. Организация интеропа между ними требует усилий. Еще есть фуллстек веб-разработчики, но про них всё понятно: они должны страдать всегда.

Точнее, способ существует довольно давно, но как-то прошел мимо внимания широких народных масс. В этой статье мы рассмотрим новую технологию, которая может сделать процесс немного менее болезненным.

Если кто-то из джаваскриптеров не пробовал писать на Java, то в этом же туториале получится к ней прикоснуться (правда, всего одной строчкой и сквозь JS-биндинги). Если кто-то из джавистов еще не писал на React, то здесь будет туториал, позволяющий это сделать.

Если хочется интероп Java->JS, такая технология в JDK давным-давно была, и называется она Nashorn (читается: «Насхорн»).

Люди из раза в раз, из года в год, продолжают писать «серверные» валидаторы на Java и «клиентские» валидаторы на JS. Давайте возьмем какую-нибудь реальную ситуацию. Особый цинизм тут в том, что проверки зачастую совпадают на 80%, и вся эта активность, по сути, — особая форма бездарно потерянного времени.

Представим, что у нас есть очень тупой валидатор:

var validate = function(target) else { return "fail"; }
};

Запустить мы его можем на всех трех платформах:

  • Браузер
  • Node.js
  • Java

Просто встраиваем этот кусок кода куда угодно, и оно работает. В браузере это тривиально.

В Node.js надо либо уважать их феншуй по использованию require, либо хакнуть его к чертям вот таким простым кодом:

var fs = require('fs');
var vm = require('vm');
var includeInThisContext = function(path) {
var code = fs.readFileSync(path);
vm.runInThisContext(code, path); }.bind(this); includeInThisContext(__dirname + "/" + filename);

Готовый пример есть у меня на GitHub.

Нам, джавистам — не привыкать, а вот профессиональные джаваскриптеры могут и оконфузиться. Готовьтесь к тому, что если вы пользуетесь такими приемами, то довольно скоро коллеги могут начать считать вас чучелом.

Теперь долбанем всё то же самое, но под Насхорном в Java.

public class JSExecutor { private static final Logger logger = LoggerFactory.getLogger(JSExecutor.class); ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); Invocable invoker = (Invocable) engine; public JSExecutor() { try { File bootstrapFile = new ClassPathResource("validator.js").getFile(); String bootstrapString = new String(Files.readAllBytes(bootstrapFile.toPath())); engine.eval(bootstrapString); } catch (Exception e) { logger.error("Can't load bootstrap JS!", e); } } public Object execute(String code) { Object result = null; try { result = engine.eval(code); } catch (Exception e) { logger.error("Can't run JS!", e); } return result; } public Object executeFunction(String name, Object... args) { Object result = null; try { result = invoker.invokeFunction(name, args); } catch (Exception e) { logger.error("Can't run JS!", e); } return result; }
}

Этот пример тоже есть у меня на GitHub.

Как видите, можно дернуть как произвольный код, так и отдельную функцию по ее имени.

Например, можно состряпать полифилл типа такого: Есть, конечно, такие проблемы, которые можно решить только в ручном порядке.

var global = this;
var window = this; var process = {env:{}};
var console = {}; console.debug = print; console.log = print; console.warn = print; console.error = print;

Это уже детали конкретной реализации. Если хочется, чтобы код валидатора был идеально идентичным и на сервере, и на клиенте, придется для «серверных» методов написать заглушки, которые подпихиваются только в браузер.

Но в этом ничего интересного: во-первых, ab на локалхосте измеряет погоду на Марсе, во-вторых, мы и так верим, что явных ляпов в этих движках нет, они конкуренты. Кстати, ab на моем ноутбуке (ab -k -c 10 -n 100 http://localhost:3000/?id=2) на такой код показывает 6-7 тысяч запросов в секунду, и не важно, на чем он запущен — на Nashorn или Node.js.

Если хорошенько подумать, можно написать такой бенчмарк, где Насхорн будет проседать, и правильней будет написать нативный код. Понятно, что, если вы живете в «красной зоне» кривой имени Ш., использовать Nashorn без включения мозга и написания бенчмарков нельзя. Но надо четко понимать, что мир не ограничивается хайлоадом и перформансными темами, иногда удобство написания важней любого перформанса.

Попробуем пропихнуть данные в обратном направлении, из Java в JS.

Зачем это может быть нужно?

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

Представьте: нужно сгенерить фронт вебпаком, и хочется вписать в правый верхний угол веб-странички текущую версию приложения. Рассмотрим игрушечный случай из реальной жизни. Значит, нужно создать такой Maven-проект, который будет работать в два прохода: прибить к какой-нибудь фазе Maven Build Lifecycle сборку пары классов и их запуск, которые сгенерят properties-файл с номером версии, который на следующей фазе подхватит вручную вызванный npm. Вполне вероятно, что версию бэкенда можно нормальным способом вытащить только вызвав какой-то джавовый код (легаси же).

Приводить пример такого pom.xml я здесь не буду, потому что это мерзко 🙂

Из этого возникают следующие моменты: Более глобально проблема заключается в том, что современная культура поддерживает и поощряет программистов-полиглотов и проекты, написанные на множестве языков.

  • Разработчики хотят использовать тот язык, который более всего подходит к решаемой задаче. Очень больно писать на Java веб-интерфейс (по крайней мере до тех пор, пока JVM и OpenJDK не стабилизируются на WebAssembly), а на JS он делается просто и удобно.
  • Часто хочется параллельно развивать несколько кодовых баз. Например, есть одна база на JS — фронт, и другая база на Java — бэк. Хочется развивать проекты, потихоньку переписывая всё приложение на Node.JS, включая серверный код — в тех местах, где Java не нужна по смыслу. Не должно быть «дня номер ноль», когда весь Java-бэкенд или JS-фронтенд отправляется на свалку, и пусть весь мир подождет, пока мы напишем новый.
  • При пересечении границы языка приходится вручную писать множество мусорного кода, обеспечивающего интероп.

Иногда есть готовые решения — например, переход границы Java/С делается с помощью JNI.

Если мы в своем коде поддерживаем адовейшие pom.xml, properties и xml-файлы и другой ручной интероп, то они имеют свойство ломаться в самых неприятных моментах. Использование такой интеграции еще и тем хорошо, что, как любят говорить программисты-функционалы, «не сломается то, чего нет». Если же эту прослойку написали какие-нибудь реальные боевые ботаны, типа Oracle или Microsoft, оно почти не ломается, а когда ломается — чинить это не нам.

Возвращаясь к предыдущему примеру: зачем нам вставать два раза и делать чудеса с Насхорном, если можно не вставать вообще и писать весь UI только на Ноде?

Но как это сделать, учитывая, что нужно прозрачно посасывать данные из Java?

Засосать в него все нужные библиотеки, подпилить напильником, и, может быть, они даже запустятся. Первая мысль, которая приходит в голову — продолжать использовать Nashorn. И вручную сэмулировать всю инфраструктуру Ноды. Если среди них не будет таких, которым нужны нативные расширения. Кажется, это проблема. И еще что-то. Если разработчики из Oracle не смогли его довести до конца, то какой шанс, что получится сделать это самостоятельно? Вообще, такой проект уже был, назывался Project Avatar, и, к сожалению, он загнулся.

То есть часть Graal, ответственная за запуск JavaScript. К счастью, у нас есть еще один довольно новый и интересный проект — Graal.js.

Graal в этом плане отличается — очень внезапно он вышел на сцену как зрелый конкурент. Инновационные проекты из мира JDK зачастую воспринимаются чем-то далеким и нереальным.

Он известен тем, что в свежих версиях OpenJDK можно переключить JIT-компилятор из C2 на тот, что идет в составе Graal. Graal — это не часть OpenJDK, а отдельный продукт. В данном случае разработчики из Oracle Labs реализовали поддержку JavaScript. Кроме того, в составе Graal поставляется фреймворк Truffle, с помощью которого можно реализовывать разные новые языки.

Чтобы прочувствовать, насколько это просто и удобно, давайте рассмотрим игрушечный проект-пример.

Представим, что мы делаем рубку НЛО на Хабре.

Во второй версии кнопка будет банить или троллей, или спамеров, и кого именно мы сейчас баним — будет подгружаться из Java. В первой версии Рубки, НЛО сможет банить рандомных людей, и кнопка будет называться «Забанить кого-нибудь!». В целях минимализации примера меняться будет только надпись на кнопке, бизнес-логику прокидывать не будем.

В конце получается работающее приложение, я проверял. Чтобы сделать реактовое приложение, нужно выполнить много действий, поэтому они аккуратно разбиты на шаги.

1. Качаем «энтерпрайзную» GraalVM (по ссылке) и прописываем обычные для Java переменные окружения.

Энтерпрайзная версия нужна потому, что только в ней есть GraalJS.

Можно, например, в .bash_profile записать вот такое:

graalvm () { export LABSJDK=/Users/olegchir/opt/graalvm-0.33/Contents/Home export LABSJRE=/Users/olegchir/opt/graalvm-0.33/Contents/Home/jre export JDK_HOME=$LABSJDK export JRE_HOME=$LABSJRE export JAVA_HOME=$JDK_HOME export PATH=$JDK_HOME/bin:$JRE_HOME/bin:$PATH
}

И потом после перезагрузки шелла вызвать эту функцию: graalvm.

Тут всё очень просто: после того, как GraalVM попадет в PATH, ваш нормальный системный npm (например, /usr/local/bin/npm в macOS) будет подменён нашей особой джавовой версией ($JDK_HOME/bin/npm). Почему я предлагаю сделать отдельную баш-функцию и вызывать ее по мере необходимости, а не сразу? Если вы JS-разработчик, такая подмена на постоянку — не самая лучшая идея.

2. Делаем директорию для проекта

mkdir -p ~/git/habrotest
cd ~/git/habrotest

3. npm init (заполнить с умом, но можно и просто прощелкать кнопку enter)

4. Устанавливаем нужные модули: Webpack, Babel, React

npm i --save-dev webpack webpack-cli webpack-dev-server
npm i --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react
npm i --save react react-dom

Обновляться не стоит. Заметьте, что npm может оказаться слегка устаревшей версии (относительно «настоящего») и попросит обновиться.

5. Создаем директории, в которых будет происходить работа:

mkdir -p src/client/app
mkdir -p src/client/public
mkdir -p loaders

6. Учим Babel нашим языкам:

./.babelrc:

{ "presets" : ["es2015", "react"]
}

7. Настраиваем вебпак:

./webpack.config.js:

var p = require('path');
var webpack = require('webpack'); var BUILD_DIR = p.resolve(__dirname, 'src/client/public');
var APP_DIR = p.resolve(__dirname, 'src/client/app'); var config = { output: { path: BUILD_DIR, filename: 'bundle.js' }, entry: APP_DIR + '/index.jsx', module : { rules : [ { test : /\.jsx?/, include : APP_DIR, loader : 'babel-loader' } ] }
}; module.exports = config;

8. Создаем страничку для нашего приложения:

./src/client/index.html

<html> <head> <meta charset="utf-8"> <title>Добро пожаловать в рубку НЛО</title> </head> <body> <div id="app" /> <script src="public/bundle.js" type="text/javascript"></script> </body>
</html>

9. Создаем индекс (чтобы потом пихать в него демонстрационный компонент):

./src/client/app/index.jsx

import React from 'react';
import {render} from 'react-dom';
import NLOComponent from './NLOComponent.jsx'; class App extends React.Component { render () { return ( <div> <p>Добро пожаловать в рубку, НЛО</p> <NLOComponent /> </div> ); }
} render(<App/>, document.getElementById('app'));

10. Создаем компонент!

./src/client/app/NLOComponent.jsx

import React from 'react'; class NLOComponent extends React.Component { constructor(props) { super(props); this.state = {banned : 0}; this.onBan = this.onBan.bind(this); } onBan () { let newBanned = this.state.banned + 10; this.setState({banned: newBanned}); } render() { return (<div> Количество забаненных : <span>{this.state.banned}</span> <div><button onClick={this.onBan}>Забанить кого-нибудь!</button></div> </div> ); } } export default NLOComponent;

11. Запускаем сборку: webpack -d

Всё должно успешно собраться и вывести нечто вроде:

joker:habrotest olegchir$ webpack -d
Hash: b19d6529d6e3f70baba6
Version: webpack 4.5.0
Time: 19358ms
Built at: 2018-04-16 05:12:49 Asset Size Chunks Chunk Names
bundle.js 1.69 MiB main [emitted] main
Entrypoint main = bundle.js
[./src/client/app/NLOComponent.jsx] 3.03 KiB {main} [built]
[./src/client/app/index.jsx] 2.61 KiB {main} [built] + 21 hidden modules

12. Теперь можно открыть в браузере ./src/client/index.html и насладиться следующим видом:

Первая часть туториала пройдена, теперь нужно научиться менять надпись на кнопке.

13. Попробуем внедрить в наш компонент переменную «название кнопки» (buttonCaption) и «список вариантов» (buttonVariants), о которых ничего не известно в JS. В дальнейшем они будут подтягиваться из Java, но сейчас просто проверяем, что их использование приводит к ошибке:

import React from 'react'; class NLOComponent extends React.Component { constructor(props) { super(props); this.state = {banned : 0, button: buttonCaption}; this.onBan = this.onBan.bind(this); } onBan () { let newBanned = this.state.banned + 10; this.setState({banned: newBanned, button: buttonVariants[Math.round(Math.random())]}); } render() { return (<div> Количество забаненных : <span>{this.state.banned}</span> <div><button onClick={this.onBan}>{this.state.button}</button></div> </div> ); } } export default NLOComponent;

Наблюдаем честную ошибку:

NLOComponent.jsx?8e83:7 Uncaught ReferenceError: buttonCaption is not defined at new NLOComponent (NLOComponent.jsx?8e83:7) at constructClassInstance (react-dom.development.js?61bb:6789) at updateClassComponent (react-dom.development.js?61bb:8324) at beginWork (react-dom.development.js?61bb:8966) at performUnitOfWork (react-dom.development.js?61bb:11798) at workLoop (react-dom.development.js?61bb:11827) at HTMLUnknownElement.callCallback (react-dom.development.js?61bb:104) at Object.invokeGuardedCallbackDev (react-dom.development.js?61bb:142) at invokeGuardedCallback (react-dom.development.js?61bb:191) at replayUnitOfWork (react-dom.development.js?61bb:11302)
(anonymous) @ bundle.js:72 react-dom.development.js?61bb:9627 The above error occurred in the <NLOComponent> component: in NLOComponent (created by App) in div (created by App) in App

14. Теперь давайте познакомимся с легальным способом подсовывать переменные в Вебпаке. Это лоадеры.

Во-первых, нужно немного переписать конфиг вебпака, чтобы удобно грузить кастомные лоадеры:

var p = require('path');
var webpack = require('webpack');
var BUILD_DIR = p.resolve(__dirname, 'src/client/public');
var APP_DIR = p.resolve(__dirname, 'src/client/app'); let defaults = { output: { path: BUILD_DIR, filename: 'bundle.js' }, entry: APP_DIR + '/index.jsx', module : { rules : [ { test : /\.jsx?/, include : APP_DIR, loader : 'babel-loader' } ] }, resolveLoader: { modules: ['node_modules', p.resolve(__dirname, 'loaders')] }
}; module.exports = function (content) { let dd = defaults; dd.module.rules.push({ test : /index\.jsx/, loader: "preload", options: {} }); return dd;
};

(Заметьте, что в options лоадеру можно подсунуть любые данные и потом считать с помощью loaderUtils.getOptions(this) из модуля loader-utils)

Лоадер устроен тупо: на вход в параметр source нам приходит изначальный код, мы его изменяем по своему желанию (можем и не изменять) и потом возвращаем назад. Ну и теперь, собственно, пишем лоадер.

./loaders/preload.js:

const loaderUtils = require("loader-utils"), schemaUtils = require("schema-utils"); module.exports = function main(source) { this.cacheable(); console.log("applying loader"); var initial = "Забанить тролля!"; var variants = JSON.stringify(["Забанить тролля!", "Забанить спамера!"]); return `window.buttonCaption=\"${initial}\";` + `window.buttonVariants=${variants};` + `${source}`;
};

Выполняем пересборку с помощью webpack -d.

Всё отлично работает, нет никаких ошибок.

15. Теперь вы спросите: хорошо, мы выучили один маленький грязный хак Вебпака, но при чем здесь Java?

Значит, можно с помощью API, похожего на Nashorn'овский, работать из JS с джавовыми типами. Интересно здесь то, что наш лоадер выполняется не просто так, а под Граалем.

const loaderUtils = require("loader-utils"), schemaUtils = require("schema-utils"); module.exports = function main(source) { this.cacheable(); console.log("applying loader"); //Мы можем получать джавовые типы и содзавать объекты этого типа var JavaString = Java.type("java.lang.String"); var initial = new JavaString("Забанить тролля!"); //Мы можем конвертить данные туда, сюда, и обратно var jsVariants = ["Забанить тролля!", "Забанить спамера!"]; var javaVariants = Java.to(jsVariants, "java.lang.String[]"); var variants = JSON.stringify(javaVariants); //Но интероп не всегда хорош, и тогда приходится городить костыли return `window.buttonCaption=\"${initial}\";` + `window.buttonVariants=${variants};` + `${source}`;
};

Ну и конечно, webpack -d.

16. При попытке собрать вебпаком видим ошибку:

ERROR in ./src/client/app/index.jsx
Module build failed: ReferenceError: Java is not defined at Object.main (/Users/olegchir/git/habrotest/loaders/preload.js:9:19)

Она возникает потому, что джавовые типы недоступны по умолчанию и включаются специальным флагом --jvm, который имеется только в GraalJS, но не в «обычной» Ноде.

Поэтому собирать надо специальной командой:

node --jvm node_modules/.bin/webpack -d

Например, в .bash_profile можно вставить следующую строчку: Так как набирать всё это достаточно муторно, я использую алиас в баше.

alias graal_webpack_build="node --jvm node_modules/.bin/webpack -d"

Или как-нибудь еще короче, чтобы набирать было приятно.

17. PROFIT!

Собранные файлы закоммичены прямо в репозиторий, чтобы посмотреть можно было даже не проходя туториал до конца. Результат можно посмотреть в моем репозитории на GitHub.

Всё это — далеко не единичный случай, способов применения можно придумать множество. Вот таким простым и удобным способом мы теперь можем интегрировать Java и JS.

В чем же подвох? Напоследок, каплю дегтя в бочку меда.

  • GraalJS — пока не Open Source, хотя, по слухам, опенсорснуть его хотят;
  • Джавовый npm пока что подтормаживает. Почему — надо изучать. Тормозит именно npm, а не сам JS-движок;
  • Под капотом у всего этого находится лютая магия, и при попытке туда влезть придется изучать много всего дополнительно;
  • Всё это собрано относительно JDK8. Новых фишек из Java 11 придется дожидаться достаточно долго;
  • Graal — экспериментальный проект. Нужно учитывать это при попытке интегрировать его в совсем уж кровавый энтерпрайз без права на ошибку.

Как вы, наверное, знаете, мы делаем конференции. Минутка рекламы. Можно туда прийти, послушать доклады (какие доклады там бывают — описано в программе конференции), вживую пообщаться с практикующими экспертами JavaScript и фронтенда, разработчиками разных моднейших технологий. Ближайшая конференция про JavaScript — HolyJS 2018 Piter, которая пройдет 19-20 мая 2018 года в Санкт-Петербурге. Короче, заходите, мы вас ждём!

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть