Java para tareas de scripting con JBang y Gradle

Escrito por picodotdev el , actualizado el .
java planeta-codigo programacion
Enlace permanente Comentarios

Para programar pequeños scripts normalmente se suele emplear el intérprete de comandos en GNU/Linux bash o si es algo complejo un lenguaje interpretado como Python, Ruby o Groovy. Pero no pienses que Java no puede ser empleado para tareas de scripting, en este artículo muestro que problemas presentan los lenguajes interpretados o dinámicos, que ventajas tiene usar Java y finalmente como usarlo con la misma sencillez que un lenguaje interpretado para el nicho funcional de los scripts.

Java puede emplearse para cualquier propósito desde aplicaciones web en la parte servidora, aplicaciones de escritorio como escribí en dos artículos introductorios sobre JavaFX, juegos con alta calidad gráfica incluso para dispositivos de capacidades más reducidas como IoT o embebidos y verdaderamente limitados. Para cualquier plataforma en la que haya disponible una JVM se pueden ejecutar aplicaciones programadas con el lenguaje Java. Sin embargo, hay multitud de otros lenguajes aunque solo una docena con un porcentaje de uso significativo que tratan de ocupar nichos de funcionalidades específicas. Unos pueden ser el análisis de datos como con el lenguaje R y otro la programación de pequeñas tareas, los scripts o lenguajes de scripting.

Para los scripts normalmente se han utilizado intérpretes como Bash por su disponibilidad en cualquier sistema GNU/Linux o si se necesita un lenguaje más avanzado Python, Ruby o Groovy. Cualquiera de estas opciones son empleadas para realizar tareas de scripting que involucran desde generación de informes o archivos, envío de correos electrónicos hasta actualización de una base de datos relacional o nosql, cualquier cosa que se desee automatizar. Al no necesitar compilarse ni generar un artefacto extraño como los archivos .class o .jar de Java basta con escribir el código fuente del script con cualquier editor de texto y ejecutarlo con el intérprete correspondiente directamente desde el código fuente sin una compilación previa.

El despliegue en un entorno de pruebas o producción de un script es sencillo, basta con copiar el código fuente a la máquina correspondiente donde se vaya a ejecutar y ejecutarlos, únicamente es necesario instalar la versión del intérprete adecuado y las dependencias adicionales del script si necesita alguna como en Python posiblemente en un virtualenv. En entornos más avanzados es posible realizar la planificación de las tareas de scripting con Nomad y Docker.

Java para tareas de scripting

Pero al contrario de lo que piensa mucha gente Java puede ser usado perfectamente como lenguaje de scripting con igual simpleza o más que Python, Ruby o Groovy. Java es más verboso sí pero en mi opinión estas son 10 razones para seguir usando Java entre ellas el compilador, en el artículo Java for everything dan una buena extensa descripción de porque usar Java también para los scripts y donde se expone una forma de usarlo, en este artículo mostraré otras dos creo que mejores.

El compilador de Java validará al menos que el código no tienen errores léxicos o de sintaxis que en un lenguaje interpretado son fáciles de introducir cuando hace meses que no modificas el script olvidando gran parte del código, cuando no has escrito tú el script, cuando hay prisa y presión para hacer las modificaciones por un error en producción apremiante, cuando el tamaño del código empieza a ser considerable, es modificado por varias personas o cuando no se domina el lenguaje profundamente. ¿Qué prefieres, algo más verbosidad (ahora menos con varias de las novedades introducidas en Java 8) o evitar que un script importante en producción se quede a medio ejecutar por un error de compilación al interpretarlo y provoque alguna catástrofe? Yo lo tengo claro, que el compilador me salve el culo.

En cuanto al número de líneas necesarias para hacer un script en Java puede que se necesiten alguna más por su menor azúcar sintáctico o la falta de algún método de utilidad no disponible en la propia API del JDK ajustado a la necesidad pero si el script es pequeño ¿realmente importan unas pocas lineas más? y si el script es grande el compilador y el IDE serán de gran ayuda.

Entrando a discutir el apartado de la simplicidad de ejecución de un lenguaje interpretado, en Java se puede conseguir lo mismo con JBang y algo similar con Gradle suficiente para muchos casos. Estas herramientas encargan de descargar la dependencias que necesite la aplicación y junto con un wrapper no es necesario tener nada más instalado que la máquina virtual de Java. Si el script tiene algún error de compilación el compilador lo indicará y no se ejecutará. La herramienta SDKMAN permite instalar software de la plataforma Java entre este software está tanto el JDK de Java como JBang y Gradle incluso varias versiones diferentes al mismo tiempo.

Los ejemplos en cada caso consisten en tres pequeños scripts programados en Java con una dependencia de una librería. Son lo más sencillos posible emitiendo en la consola simplemente un mensaje pero podrían hacer cualquier cosa desde realizar peticiones a una API REST con Retrofit a generar archivos Excel o CSV con Apache POI y OpenCSV por mencionar dos cosas habituales que pueden realizan los scripts.

Scripting en Java con JBang

JBang es una utilidad que proporciona una experiencia de uso igual que un script de Python o Groovy. Un script de Java con JBang no requiere ninguna estructura de directorios y es posible ejecutarlo desde el código fuente. SDKMAN permite instalar JBang de forma sencilla con el siguiente comando.

1
2
$ sdk install jbang 0.78.0

sdkman-install-jbang.sh

Si el script tiene dependencias sobre librerías hay que añadir un comentario que empiece por //DEPS seguido por el grupo, artefacto y versión de la dependencia, al ejecutar el script JBang la descarga y añade al classpath.

En GNU/Linux los intérpretes de comandos como Bash permiten ejecutar todo archivo que tiene el permiso de ejecución establecido, en el caso de archivos de texto el programa encargado de interpretar el código fuente se especificar en la secuencia de caracteres shebang. El shebang habitualmente se especifica de la siguiente forma #!/usr/bin/env groovy, para hacer compatible secuencia de caracteres del shebang con el código fuente Java hay que usar la forma que se muestra en el código fuente del script de ejemplo. Esta directivas especificadas como comentarios en el código fuente de Java son interpretadas por JBang antes de ejecutar el script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
///usr/bin/env jbang --quiet "$0" "$@" ; exit $? 

//DEPS org.apache.commons:commons-lang3:3.4

package io.github.picodotdev.blogbitix.javascripts;

import org.apache.commons.lang3.StringUtils;

public class Script1 {

    public static void main(String[] args) {
        System.out.printf("Script1 %s%n", StringUtils.join(args));
    }
}
Script1-jbang.java
1
2
3
$ chmod +x ./Script1.java
$ ./Script1.java
$ jbang Script1.java
Script1-run.sh

El resultado del script es simplemente una cadena en la salida estándar de la terminal.

1
2
Script1

Script1.out

En Java la tarea de edición de archivos de código fuente se realiza con un entorno integrado de desarrollo o IDE ya sea IntelliJ IDEA, eclipse o NetBeans. Un IDE proporciona asistencia de código y muestra errores de compilación según se escribe, esto permite escribir código Java de una forma bastante rápida aún con la verbosidad de Java.

Los scripts con JBang no tienen una estructura de proyecto que los IDEs entiendan, sin embargo JBang posee un comando para generar a partir del script un proyecto temporal para editar el código fuente desde un IDE. Este es el comando para editar el script desde IntelliJ IDEA instalado como una aplicación de Flatpak o con el editor Visual Studio Code.

1
2
3
$ flatpak run com.jetbrains.IntelliJ-IDEA-Community `jbang edit Script1.java`
$ # Editar el script con Visual Studio Code
$ jbang edit --open=code Script1.java
Script1-intellij.sh

Otras opciones que permite JBang es especificar en el código fuente la versión de Java necesaria para ejecutar el script, también permite crear un lanzador wrapper y usar JBang sin que este esté instalado previamente, el wrapper lo descarga e instala si no lo está incluso descarga e instala un JDK si no está instalado previamente.

Para proporciona la misma experiencia de usuario que un script interpretado desde el código fuente aparte de descargar las dependencias indicadas se encarga de compilar y crear un archivo jar que se pueda ejecutar con el JDK. Para no repetir el proceso de creación del jar en cada ejecución lo guarda en una caché ubicada en el directorio ~/.jbang/cache. Al hacer modificaciones y ejecutar un script en la caché se un jar por cada versión, un comando permite purgar los archivos de esta caché.

En caso necesario también permite exportar el script a un archivo jar o instalar el script como un comando del sistema.

1
2
3
4
$ jbang wrapper install
$ jbang cache clear
$ jbang export local Script1.java
$ jbang export portable Script1.java
jbang-1.sh

Scripting en Java con Gradle

Gradle requiere un archivo build.gradle y cierta estructura de directorios pero no es algo suficiente complejo como para descartar Java como lenguaje para este propósito más teniendo en cuenta sus ventajas. Gradle es una solución algo más complicada que JBang pero a cambio no tiene la limitación de dependencias entre clases de diferente paquetes y adicionalmente ofrece poder tener teses unitarios ejecutables de forma sencilla.

Al igual que JBang a parte de la JVM el proyecto de scripts será autocontenido incluso para las dependencias con lo que su despliegue en un entorno de producción será muy sencillo basta con copiar archivos (FTP, wget, …) o hacer git clone y git pull directamente del repositorio de código fuente para actualizarlo, si se usan los scripts lanzadores después de actualizar el código fuente del proyecto será necesario reconstruirlo con ./gradlew build.

El plugin application de Gradle solo permite definir un único programa con su main pero creando tareas de tipo JavaExec es posible tener cuantos diferentes scripts se deseen.

Para los 3 pequeños scripts de ejemplo el archivo build.gradle necesario es el siguiente definiendo de forma dinámica una tarea runScript que permite lanzar el script con el lanzador wrapper de Gradle y usar Gradle sin que este esté instalado previamente descargándolo e instalándolo si no lo está previamente. Si no se desea usar el wrapper de Gradle como forma de ejecutar los scripts las tareas createStartScripts crean varios archivos de Bash que lanzan los programas de Java estableciendo de forma adecuada los argumentos de Java para añadir al classpath las dependencias.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
apply plugin: 'eclipse'
apply plugin: 'java'
apply plugin: 'application'

mainClassName = 'io.github.picodotdev.blogbitix.javascripts.Script1'

repositories {
    mavenCentral()    
}

dependencies {
    compile 'org.apache.commons:commons-lang3:3.4'
}

3.times { i ->
    def id = i + 1
    task "runScript${id}"(type: JavaExec, dependsOn: ':build') {
        main = "io.github.picodotdev.blogbitix.javascripts.Script${id}"
        classpath = sourceSets.main.runtimeClasspath
    }

    task "createStartScripts${id}"(type: CreateStartScripts) {
        outputDir = file('.')
        mainClassName = "io.github.picodotdev.blogbitix.javascripts.Script${id}"
        applicationName = "script${id}"
        classpath = files('build/libs/*')
        // See https://discuss.gradle.org/t/classpath-in-application-plugin-is-building-always-relative-to-app-home-lib-directory/2012
        doLast {
            def windowsScriptFile = file getWindowsScript()
            def unixScriptFile = file getUnixScript()
            windowsScriptFile.text = windowsScriptFile.text.replace('%APP_HOME%\\lib\\*', '%SAVED%\\build\\libs\\*')
            unixScriptFile.text = unixScriptFile.text.replace('$APP_HOME/lib/*', '$SAVED/build/libs/*')
        }
    }
}

task createStartScripts(dependsOn: ['createStartScripts1', 'createStartScripts2', 'createStartScripts3'])

task copyToLib(type: Copy) {
    into "$buildDir/libs"
    from configurations.runtime
}

build << {
	tasks.copyToLib.execute()
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.8'
}
build.gradle
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package io.github.picodotdev.blogbitix.javascripts;

import org.apache.commons.lang3.StringUtils;

public class Script1 {

    public static void main(String[] args) {
        System.out.printf("Script1 %s%n", StringUtils.join(args));
    }
}
Script1-gradle.java

La tarea de Gradle createStartScripts genera todos los lanzadores de Bash para los scripts.

1
2
$ ./gradlew build
$ ./gradlew createStartScripts
gradle-build.sh

La ejecución de cada uno de los scripts con gradlew y usando los scripts lanzadores es la siguiente:

1
2
3
4
5
6
7
$ ./gradlew runScript1
$ ./gradlew runScript2
$ ./gradlew runScript3

$ ./script1
$ ./script2
$ ./script3
gradle-run.sh

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./Script1.java, ./gradlew runScript1


Comparte el artículo: