Introducción a Nashorn, el motor de JavaScript de Java 8

0
13112

En este tutorial vamos a ver cuáles son las principales características de Nashorn, el motor JavaScript incluido en el JDK 1.8, mediante algunos ejemplos. Además analizaremos el rendimiento de Nashorn y veremos cuál es la situación actual de este nuevo motor.

Índice de contenidos

1. Introducción

La llegada de Java 8 trae consigo una nueva versión del motor de JavaScript de Java. El obsoleto Rhino es sustituido por una nueva versión llamada Nashorn, que introduce mejoras de rendimiento y compatibilidad con las nuevas características del lenguaje.

Este tutorial pretende repasar cuáles son las principales características del motor Nashorn, ver algunos ejemplos que demuestren cómo hacer uso de dichas características, hacer un repaso a cuál es la situación actual de Nashorn, y medir su rendimiento frente a otros motores JavaScript modernos.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2.5 Ghz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Yosemite 10.10.5
  • Entorno de desarrollo: Eclipse Mars 4.5.0
  • Java: JDK 1.8, update 60

3. Características de Nashorn

Nashorn es el motor de JavaScript integrado en Java 8. Sustituye al obsoleto Rhino, utilizado hasta la fecha, ya que se había quedado desfasado respecto a los motores más modernos y mucho más eficientes basados principalmente en el motor V8 de Google Chrome.

Entre sus principales características podemos encontrar:

  • Motor JavaScript ECMA, es decir, compatible con el estándar.
  • Consola jjs para ir ejecutando linea a linea o lanzar ficheros JavaScript
  • Compatibilidad con shell script (disponible sólo en algunos entornos), incluyendo «Heredocs».
  • Integración dentro de aplicaciones Java, usando la Java Scripting API (JSR 223). Entre otras cosas, podemos:
    • Extender tipos de JavaScript dentro de Java
    • Ejecutar librerías JavaScript
  • Integración de Java dentro de JavaScript

Por otro lado, es importante destacar que Nashorn no incluye soporte para el manejo del DOM, ni tiene disponibles los típicos objetos dentro de un navegador, como «console» o «window». Ya de por sí tampoco tenía mucho sentido utilizar una herramienta como esta en la interfaz de usuario, pero al no tener esta capacidad, menos aún.

A continuación vamos a ver algunos ejemplos de cómo sacar partido a estas características.

3.1. Uso de la consola Nashorn

La consola Nashorn se encuentra disponible en la carpeta «bin» del directorio donde esté instalado Java. Para poder usarla desde cualquier lugar podemos incluir dicha carpeta en la variable PATH del entorno o crear un enlace simbólico en la carpeta «/usr/bin». Igual que usamos el comando «java» o «javac» para ejecutar y compilar aplicaciones Java respectivamente, para ejecutar el motor Nashorn debemos usar el comando «jjs». Tecleándolo en una ventana de terminal, veremos el prompt de la consola de JavaScript:

$ jjs
jjs> 

A continuación vamos a ejecutar nuestro primer comando en la consola:

jjs> print("hola mundo");
hola mundo
jjs> 

además, podemos crear programas en JavaScript y lanzarlos a la consola de la siguiente forma:

$ jss fichero.js

3.2. Integración de JavaScript dentro de Java

Una de las características que pueden resultar de interés es la posibilidad de ejecutar código JavaScript dentro de nuestras aplicaciones Java. A continuación podemos ver un primer ejemplo de cómo ejecutar dicho código:

NashornExample.java
public class NashornExample {

    private static final String NASHORN_ENGINE_NAME = "nashorn";

    private final ScriptEngine engine;

    public static void main(final String[] args) throws Exception {
        final NashornExample nashornExample = new NashornExample();
        nashornExample.example01();
     }

    public NashornExample() {
        final ScriptEngineManager manager = new ScriptEngineManager();
        engine = manager.getEngineByName(NASHORN_ENGINE_NAME);
    }

    public void example01() throws Exception {
        engine.eval("print('Hola mundo')");
    }
}

Como podemos ver, para ejecutar código JavaScript necesitamos instanciar un ScriptEngineManager y de éste, obtener el motor específico Nashorn. Una vez tenemos la instancia del motor, podemos llamar al método «eval» con un parámetro que contenga el código JavaScript a ejecutar. El resultado de ejecutar el programa anterior muestra por pantalla el mensaje «Hola mundo»

También podemos ejecutar un programa en JavaScript que esté almacenado en un fichero externo. Para ello, vamos a crear otro método en la clase definida anteriormente, de la siguiente forma:

public void example02() throws Exception {
    engine.eval(new FileReader("src/main/resources/example02.js"));
}

En este caso, en lugar de pasar un objeto String con el comando a ejecutar, usamos la versión sobrecargada de «eval» que acepta un objeto tipo Reader, en este caso un FileReader que apunta a un fichero JavaScript cuyo contenido es el siguiente:

example02.js
print("Hola javascript");

El resultado que obtenemos al ejecutar el programa es que se muestra por pantalla «Hola javascript».

Complicando un poco las cosas, podemos ejecutar una función definida en un fichero JavaScript desde Java. Para ello podríamos crear un método adicional y refactorizar ligeramente el código inicial de la clase anterior, quedando el código como sigue a continuación:

NashornExample.java
public class NashornExample {

    private static final String NASHORN_ENGINE_NAME = "nashorn";

    private final ScriptEngine engine;

    private Invocable invoker;

    public static void main(final String[] args) throws Exception {
        final NashornExample nashornExample = new NashornExample();
        nashornExample.example01();
        nashornExample.example02();
        nashornExample.example03();
    }

    public NashornExample() {
        final ScriptEngineManager manager = new ScriptEngineManager();
        engine = manager.getEngineByName(NASHORN_ENGINE_NAME);
        invoker = (Invocable)engine;
    }

    public void example01() throws Exception {
        engine.eval("print('Hola mundo')");
    }

    public void example02() throws Exception {
        engine.eval(new FileReader("src/main/resources/example02.js"));
    }

    public void example03() throws Exception {

        engine.eval(new FileReader("src/main/resources/example03.js"));
        // la siguiente linea invoca la función "greeter" con parámetro "Autentia"
        final String greetingMessage = (String)invoker.invokeFunction("greeter", "Autentia");
        System.out.println(greetingMessage);
    }
}

Ahora, el constructor incluye la instanciación de un «Invocable», que nos permite hacer llamadas al script y recibir los resultados. En el código del método «example03» tenemos la llamada al método «invokeFunction» del objeto «invoker». El primer parámetro nos indica el nombre de la función a la que queremos llamar dentro del script, mientras que el segundo parámetro es ya el primer parámetro de la función. Este método puede recibir un número variable de parámetros, tantos como sean necesarios para completar la llamada al script. El contenido del fichero «example03.js» es el siguiente:

example03.js
function greeter(name){
    return "Hola " + name;
}

Si en lugar de llamar a una función, queremos llamar a un método de un objeto, tendríamos que complicar ligeramente el código. En este caso, y para tratar de abordar más posibilidades, en lugar de definir nuestro propio objeto en el código JavaScript, vamos a utilizar un objeto predefinido del lenguaje, como es un «Array». El código queda de la siguiente forma:

public void example04() throws Exception {
    engine.eval(new FileReader("src/main/resources/example04.js"));
    final Object vehiculos = engine.get("vehiculos");
    final String[] vehiculosSeleccionados = ((ScriptObjectMirror)invoker.invokeMethod(vehiculos, "slice", 0, 2))
            .to(String[].class);

    System.out.println("Vehiculos seleccionados: ");
    for (final String vehiculo : vehiculosSeleccionados) {
        System.out.println("* " + vehiculo);
    }
}

El fichero JavaScript correspondiente sería el siguiente:

example04.js
var vehiculos = new Array("Ciclomotor", "Motocicleta", "Coche", "Camión", "Tractor");

En este caso podemos ver que tras la carga del script, tenemos que obtener la referencia al objeto «vehículos» y almacenarla en la parte de Java para poderla pasar al método «invokeMethod», ya que el primer parámetro es el objeto donde se encuentra el método al que queremos llamar. En este caso «vehículos» es un objeto de tipo «Array», y llamamos al método «slice» para obtener dos elementos desde la posición 0. Este método nos devuelve un objeto de tipo «ScriptObjectMirror», que se puede convertir a otra clase Java haciendo uso del método «to». El resultado de ejecutar este programa es el siguiente:

Vehiculos seleccionados: 
* Ciclomotor
* Motocicleta

No es necesario que el código JavaScript esté programado por nosotros. Puede darse el caso que exista una librería en JavaScript que es muy útil para nuestro proyecto y no existe un equivalente en Java. Veamos un ejemplo de cómo haríamos eso con la librería Finance.js para cálculos financieros básicos. En nuestro caso, calcularemos la cuota mensual de un préstamo partiendo de la cantidad inicial de deuda, el tipo de interés y el número de meses que estaremos pagándolo. Añadiremos un nuevo método a nuestra clase, que quedaría de la siguiente forma:

public void example05() throws Exception {
    engine.eval(new FileReader("src/main/resources/Finance.js"));
    engine.eval("var finance = new Finance();");
    final Object finance = engine.get("finance");

    // calcula la cuota mensual de un préstamo de 100.000€ al 5.5% anual durante 30 años
    // (360 meses, ya que el último parámetro indica que este valor está expresado en meses)
    final double cuotaMensual = (Double)invoker.invokeMethod(finance, "AM", 100000, 5.5, 360, 1);

    System.out.println("La cuota mensual es: " + cuotaMensual);
}

El fichero Finance.js contiene la librería financiera «open source» disponible en http://financejs.org/. Como se puede ver, se trata de un código muy similar al ejemplo anterior. En este caso cargamos la librería y definimos un objeto «Finance». Por último invocamos el método «AM» para calcular la cuota mensual. En este caso, como el resultado obtenido es un objeto «Double», el cast es más sencillo. El resultado de ejecutar el código anterior es:

La cuota mensual es: 567.79

Otra característica interesante es que podemos instanciar un objeto con referencia a una interfaz en Java cuya implementación se encuentre definida en JavaScript. Esto que parece algo lioso se puede ver más fácilmente con un ejemplo:

public void example06() throws Exception {
    engine.eval(new FileReader("src/main/resources/example06.js"));
    final Object runnerjs = engine.get("runnerImpl");
    final Runnable runner = invoker.getInterface(runnerjs, Runnable.class);

    final Thread thread = new Thread(runner);

    thread.start();
    thread.join();
}

example06.js
var runnerImpl = {
    run: function(){
        for (var i = 0; i < 10; i++){
            print("paso "+i)
        }
    }
}

En este ejemplo vemos como instanciamos un objeto en JavaScript y lo traemos a Java mediante la variable "runnersjs". Después, utilizando el invoker, creamos una implementación de la interfaz pasada como segundo parámetro utilizando la definición del objeto del primer parámetro. El resultado es que, como es sabido la interfaz Runnable tiene definido un método "run()" y en la instancia "runner" este método queda definido con el código de la función "run" definida en JavaScript.

Cuando se crea un nuevo hilo a partir de esta instancia y se lanza, se ejecuta dicha función. El resultado que aparece por pantalla es el siguiente:

paso 0
paso 1
paso 2
paso 3
paso 4
paso 5
paso 6
paso 7
paso 8
paso 9

3.3. Integración de Java dentro de JavaScript

En el apartado anterior hemos visto como crear objetos dentro de JavaScript e importarlos al código Java y trabajar con ellos allí. En el siguiente ejemplo vamos a ver cómo hacer lo contrario, es decir, instanciar un objeto en Java y trabajar con él en JavaScript. Para ello, creamos un fichero en JavaScript y lo lanzamos usando la consola "jjs".

example07.js
var t = java.lang.System.currentTimeMillis();

var MyThread = Java.extend(Java.type("java.lang.Thread"));

var thread = new MyThread({
    run: function(){
        print("ejecutando thread");
        for (var i=0; i<0;i++){
            print("paso " + i)
        }
    }
})

thread.start();
print("hilo lanzado");
thread.join();

tFinal = java.lang.System.currentTimeMillis();

print("tiempo: " + (tFinal-t)/1000 + " s");

En el ejemplo anterior se crea una clase MyThread que hereda de Thread y se crea una instancia que define el método run. El hilo se lanza más abajo y se espera a la terminación del hilo para pintar el tiempo total de ejecución. Este ejemplo puede resultar interesante porque es una manera sencilla de añadir soporte para hilos en JavaScript. El resultado de ejecutar este script es el siguiente:

$ jjs example07.js
hilo lanzado
ejecutando thread
paso 0
paso 1
paso 2
paso 3
paso 4
paso 5
paso 6
paso 7
paso 8
paso 9
tiempo: 0.052 s

En el siguiente ejemplo vamos a ver cómo hacer uso de las nuevas características del lenguaje. En este caso, Streams y expresiones Lambda:

example08.js
var frutas = ["Manzana", "Pera", "Plátano"];

var frutasJavaArray = Java.to(frutas);

var frutasJava = new java.util.ArrayList();
frutasJava.addAll(java.util.Arrays.asList(frutasJavaArray));

print(frutasJava);

var frutasP = frutasJava.stream().filter(function(fruta){
    return fruta.startsWith("P");
}).collect(java.util.stream.Collectors.toList());

print("Frutas con P")
print(frutasP);

Además, en el ejemplo anterior, se pueden ver otras funcionalidades interesantes, como la posibilidad de pasar de un array de javascript ("frutas") a un array de Java usando la expresión "Java.to". Para hacer la conversión contraria tendríamos la expresón "Java.from". Dentro del stream, podemos ver que se usa el método filter. En Java usaríamos una expresión Lambda, mientras que en javascript simplemente pasamos como parámetro la definición de una función.

4. Nashorn en el Back-end

Este motor JavaScript puede resultar particularmente interesante para crear aplicaciones de back-end en JavaScript corriendo sobre la JVM, entrando en competencia con soluciones como Node.js. Con esta idea en mente, y gracias a la mejora que supuso Nashorn frente a su predecesor, Oracle intentó dar un impulso a su plataforma "Project Avatar", donde se pretendía crear una pila completa de servidor sustentada sobre una tecnología similar (y en teoría compatible) con Node.js. En el núcleo de dicho proyecto se encuentra el módulo llamado Avatar.js, que hace uso de Nashorn como motor. Las principales fortalezas de dicho proyecto eran la compatibilidad con los módulos de Node.js y las ventajas que supone que el servicio corra sobre Java, en términos de monitorización o compatibilidad de hardware.

Sin embargo, la respuesta de la comunidad fue muy fría. Además, surgieron problemas para compatibilizar los módulos existentes en Node.js y emergieron problemas de rendimiento. El resultado de esto fue que a principios de 2015 Oracle decidió dejar en suspenso el proyecto hasta ver la evolución de la industria.

Los problemas de rendimiento que lastraron a "Project Avatar" se abordaron en parte en la actualización 40 del la versión 1.8 del JDK con la introducción de "optimistic-types". Esta opción mejoró el rendimiento sensiblemente (a cambio de ralentizar el tiempo de las primeras ejecuciones o "warm-up") en determinados casos. Se espera que Java 1.9 mejore el warm-up, lo que podría ser un buen impulso para Nashorn.

5. Rendimiento de Nashorn

Con el objetivo de tratar de entender dónde está Nashorn en términos de rendimiento respecto a otros motores JavaScript, se ha ejecutado el benchmark "Octane 2.0" sobre la versión Nashorn del JDK 1.8 actualización 60. Para poder ejecutar este benchmark, debemos descargar el código JavaScript del benchmark disponible en https://github.com/chromium/octane y descomprimirlo preferentemente en una carpeta dentro del classpath de nuestra aplicación de ejemplo.

El test se ejecuta de dos formas diferentes. La primera directamente desde la linea de comandos, lanzando:

run.js
[...]
var base_dir = 'src/main/resources/octane/';
[...]

Además, tenemos que crear el código Java capaz de llamar a dicho benchmark. En este caso utilizamos el código que aparece a continuación:

public void runOctaneBenchmark() throws Exception {

    final NashornScriptEngine nashorn = (NashornScriptEngine)engine;

    final CompiledScript compiledOctane = nashorn.compile(new FileReader("src/main/resources/octane/run.js"));
    compiledOctane.eval(new SimpleScriptContext());
}

Este script podría haberse llamado utilizando una sintaxis similar a la del ejemplo 2 de este tutorial, pero se ha querido utilizar esta otra forma para ver otros elementos que pueden resultar de interés. En primer lugar, se fuerza al programa a pre-compilar el código antes de ejecutarlo. En este caso no aporta ninguna mejora de rendimiento ya que se realizan de forma secuencial la compilación y la llamada, cosa que pasaría igualmente si se evaluara el script directamente. Además, en la llamada a la evaluación del script se define un contexto de script nuevo. Esto puede resultar de interés si se quieren ejecutar varios scripts a la vez que usen diferentes contextos, con lo que las variables no serían compartidas. En todo caso, para la ejecución del benchmark, esta sintaxis no aporta ninguna mejora en los resultados respecto a la vista en el ejemplo 2.

Tras ejecutar varias veces ambas versiones (ejecución mediante consola y a través de una aplicación de Java), los resultados no son significativamente diferentes, pero se ha querido mantener ambos métodos en este tutorial para dar una mejor visión de las opciones que hay. Cabe destacar también que, en ambas versiones, el test "zlib" falla con el error "zlib: ReferenceError: "read" is not defined".

El benchmark se ejecuta de dos formas: usando "optimistic-types" (con leyenda "Nashorn optimistic") y sin dicha opción. Se puede ver en el resultado más abajo que ninguna de las dos opciones es mejor que la otra en todos los escenarios, por lo que conviene probar si en nuestra aplicación supone una mejora o no su introducción.

Para contrastar los resultados obtenidos al ejecutar el benchmark, se ejecuta el mismo test en el navegador Chrome, navegador Safari y un servidor Node.js instalados en la misma máquina y con la misma carga de trabajo. El resultado no es científico, ya que las condiciones de la prueba no están 100% controladas. Además, los resultados tienen variaciones importantes de unas ejecuciones a otras. Sin embargo, se puede apreciar que Nashorn está todavía bastante lejos de las herramientas basadas en el motor V8 en términos de rendimiento. Con el objetivo de hacerlos más fácilmente comparables, todos los resultados se muestran en relación a los obtenidos en Chrome

nashorn_benchmark

6. Conclusiones

En este tutorial hemos visto cuáles son las características fundamentales Nashorn, el nuevo motor de JavaScript incluido en la versión 1.8 del JDK, y con algunos sencillos ejemplos hemos visto cómo aprovechar algunas de esas características. Además, hemos realizado un pequeño repaso a "Project Avatar", el intento de Oracle de competir con soluciones tipo Node.js. Finalmente, hemos visto cuál es el rendimiento de Nashorn frente a otros motores JavaScript. Todo esto deja patente que, aunque es cierto que Nashorn tiene cierto interés en situaciones concretas donde la integración de Java y JavaScript sea recomendable, su rendimiento todavía queda lejos de otros motores JavaScript y puede resultar un problema para tareas con alta carga de trabajo.

7. Referencias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad