In the past, JPoint promised to write an article about using GraalVM to mix Java and JS. Here she is.
What is the problem? In everyday practice, applications are often found that consist of two parts: a JavaScript front end and a Java backend. The organization of interop between them requires effort. As a rule, they are made by people from different sides of the barricades, and when they try to get into a foreign area, they start to suffer. There is still a full stack of web developers, but everything is clear about them: they must always suffer.
In this article, we will look at a new technology that can make the process a bit less painful. More precisely, there is a way for a long time, but somehow passed by the attention of the masses.
If one of the javistes has not yet written on React, then there will be a tutorial allowing it to be done. If one of the Java scripters did not try to write in Java, then in the same tutorial you will be able to touch it (albeit with just one line and through the JS bindings).
If you want interop Java-> JS, this technology in the JDK was a long time ago, and it is called Nashorn (read: "Nashorn").
Let's take some real situation. People from time to time, from year to year, continue to write "server" validators in Java and "client" validators in JS. The special cynicism here is that checks often coincide by 80%, and all this activity, in fact, is a special form of ineptly wasted time.
Imagine that we have a very stupid validator:
var validate = function(target) { if (target > 0) { return "success"; } else { return "fail"; } };
We can run it on all three platforms:
In a browser, this is trivial. Just embed this piece of code anywhere, and it works.
In Node.js, you must either respect their feng shui by using require, or hack it to hell with such simple code:
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);
I have a ready-made example on GitHub .
Get ready for the fact that if you use these techniques, then pretty soon colleagues can begin to consider you a stuffed animal. We, javistam - are no strangers, but professional javascripters can be embarrassed.
Now it's all the same, but near Nashorno in 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; } }
I also have this example on GitHub .
As you can see, you can pull an arbitrary code, or a separate function by its name.
There are, of course, such problems that can be solved only manually. For example, you can concoct a polyfill like this:
var global = this; var window = this; var process = {env:{}}; var console = {}; console.debug = print; console.log = print; console.warn = print; console.error = print;
If you want the validator code to be perfectly identical both on the server and on the client, you will have to write stubs for “server” methods, which are pushed only into the browser. These are details of a specific implementation.
By the way, ab
on my laptop ( ab -k -c 10 -n 100 http://localhost:3000/?id=2
) shows 6-7 thousand requests per second to this code, and it doesn't matter what it is running on - on Nashorn or Node.js. But there is nothing interesting in this: firstly, ab
on localhost measures weather on Mars, and secondly, we both believe that there are no obvious mistakes in these engines, they are competitors.
It is clear that if you live in the “red zone” of the curve named after Sh., You cannot use Nashorn without turning on the brain and writing benchmarks. If you think carefully, you can write a benchmark where Nashorn will sink, and it would be more correct to write native code. But we must clearly understand that the world is not limited to the highload and performance themes, sometimes the convenience of writing is more important than any performance.
Let's try to push the data in the opposite direction, from Java to JS.
Why it may be necessary?
First, and most importantly, in many companies, there is a non-negotiable axiom: we use Java. In some banks. Secondly, in the course of solving everyday problems such problems arise constantly.
Consider a real-life toy case. Imagine: you need to generate a front webpack, and you want to write in the upper right corner of the web page the current version of the application. It is likely that the version of the backend can be pulled out in a normal way only by calling up some kind of java code (the same). So, you need to create such a Maven project that will work in two passes: nail to the Maven Build Lifecycle phase the assembly of a pair of classes and their launch, which will generate the properties file with the version number, which in the next phase will be picked up manually by npm.
I will not give an example of such pom.xml here, because it is disgusting :)
More globally, the problem is that modern culture supports and encourages polyglot programmers and projects written in many languages. From this arise the following points:
Sometimes there are ready-made solutions - for example, the Java / C border transition is done using JNI.
The use of such integration is also good because, as programmers-functionals like to say, “that which does not exist will not break.” If we in our code support the hellish pom.xml, properties and xml-files and other manual interop, then they tend to break in the most unpleasant moments. If this layer was written by some real battle nerds, such as Oracle or Microsoft, it hardly breaks, and when it breaks, it’s not for us to fix it.
Returning to the previous example: why do we have to get up twice and do wonders with Nashorny, if you can not get up at all and write the whole UI only on Noda?
But how to do this, given that you need to transparently suck data from Java?
The first thought that comes to mind is to continue using Nashorn. Suck all the necessary libraries into it, file it with a file, and maybe they even run. If among them there are no those who need native extensions. And manually emulate the entire infrastructure of the Node. And something else. It seems to be a problem. In general, such a project has already been, called Project Avatar, and, unfortunately, it is bent. If the developers from Oracle were unable to complete it, then what is the chance that you can do it yourself?
Fortunately, we have another fairly new and interesting project - Graal.js. That is the part of Graal responsible for running JavaScript.
Innovative projects from the world of JDK are often perceived as something far and unreal. Graal is different in this regard - very suddenly he came on the scene as a mature competitor.
Graal is not part of OpenJDK, but a separate product. It is known for the fact that in recent versions of OpenJDK, you can switch the JIT compiler from C2 to the one that comes with Graal. In addition, Graal comes with the Truffle framework, with which you can implement various new languages. In this case, the developers at Oracle Labs have implemented JavaScript support.
To feel how easy and convenient it is, let's look at a toy example project.
Imagine that we are cutting UFOs in Habré.
In the first version of Cutting, a UFO will be able to ban random people, and the button will be called “Ban someone!”. In the second version, the button will ban either trolls or spammers, and who exactly we are currently banning will be loaded from Java. In order to minimize the example, only the inscription on the button will be changed, we will not pass the business logic.
To make a reactor application, you need to perform many actions, so they are neatly divided into steps. In the end it turns out a working application, I checked.
The enterprise version is needed because only GraalJS is in it.
You can, for example, write this in the .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 }
And then after rebooting the shell, call this function: graalvm
.
Why do I propose to make a separate bash function and call it as needed, and not immediately? It's all very simple: after GraalVM gets into PATH, your normal system npm (for example, /usr/local/bin/npm
at macOS) will be replaced with our special Java version ( $JDK_HOME/bin/npm
). If you are a JS developer, such a permanent change is not a good idea.
mkdir -p ~/git/habrotest cd ~/git/habrotest
npm init
(fill wisely, but you can just click the enter button) 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
Note that npm may be a slightly outdated version (relative to the “present”) and will ask for an update. It is not worth updating.
mkdir -p src/client/app mkdir -p src/client/public mkdir -p loaders
./.babelrc
:
{ "presets" : ["es2015", "react"] }
./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;
./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>
./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'));
./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;
webpack -d
Everything should successfully gather and bring something like:
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
./src/client/index.html
and enjoy the following view:The first part of the tutorial is completed, now you need to learn how to change the label on the button.
buttonCaption
) and “list of options” ( buttonVariants
), about which nothing is known in JS. In the future, they will pull up from Java, but for now just check that using them results in an error: 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;
We observe an honest mistake:
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
First, you need to rewrite the webpack configuration a bit to conveniently load custom loaders:
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; };
(Note that you can slip any data into the options
loader and then take it using loaderUtils.getOptions(this)
from the loader-utils
module)
Well, now, actually, we write loader. The loader is stupid: the initial code comes to the source parameter, we change it at will (we can not change it) and then return it back.
./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
with webpack -d
.
Everything works fine, there are no errors.
The interesting thing here is that our loader is executed for a reason, and under the Grail. So, using an API similar to Nashorn, you can work from JS with Java types.
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}`; };
And of course, webpack -d
.
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)
It arises because Java types are not available by default and are turned on by the special flag - --jvm
, which is available only in GraalJS, but not in the “normal” Node.
Therefore, you need to collect a special team:
node --jvm node_modules/.bin/webpack -d
Since typing all this is quite a chore, I use alias in bash. For example, the following line can be inserted into .bash_profile
:
alias graal_webpack_build="node --jvm node_modules/.bin/webpack -d"
Or, somehow, even shorter, to recruit was pleasant.
The result can be viewed in my repository on GitHub . The collected files are committed directly to the repository, so you can see without even going through the tutorial.
In such a simple and convenient way, we can now integrate Java and JS. All this is far from an isolated case, you can think of many ways to use it.
Finally, a drop of tar in a barrel of honey. What is the catch?
Minute advertising. As you probably know, we do conferences. The nearest JavaScript conference is HolyJS 2018 Piter , which will be held May 19-20, 2018 in St. Petersburg. You can come there, listen to the reports (what reports are there - described in the conference program ), talk live with practicing experts from JavaScript and the frontend, developers of various modern technologies. In short, come in, we are waiting for you!
Source: https://habr.com/ru/post/353624/
All Articles