Graalvanizando un script de Groovy

En este post voy a publicar el código y pasos a seguir para convertir un script de Groovy en un binario de tal forma que para su ejecución no se necesita ni Groovy ni Java instalado. Además, aunque es simple, la velocidad de ejecución es sensiblemente mejor

En concreto el aplicativo va a subir un post a Linkedin (por ahora texto, luego veré de adjuntar imágenes) usando el nuevo api REST de esta plataforma. Para ello pedirá el author y el accessToken para poder publicar

Requisitos

Para completar todos los pasos se requiere tener instado:

Una vez instalado le indicaremos que nos instale las versiones de Java y Groovy necesarias para generar el binario:

sdk use groovy 4.0.11

sdk use java 22.3.r19-grl

Configuración

Para generar una imagen nativa de un script de groovy es necesario que este sea compilado en modo estático (lo que resta mucho de la expresividad de Groovy, pero …​)

compiler.config
withConfig(configuration) {
    ast(groovy.transform.CompileStatic)
    ast(groovy.transform.TypeChecked)
}

GroovyScript

Para mejorar la legibilidad del código he dividido el script en 2 ficheros .groovy, uno de ellos con métodos estáticos orientado a hacer las peticiones HTTP get y post y el otro que será el que contenga la "logica de negocio"

HttpUtil.groovy
import groovy.json.JsonOutput
import groovy.json.JsonSlurper

import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

class HttpUtil{

    static Map postJson(Map map, String url, String token) {
        println "-> ${JsonOutput.prettyPrint(JsonOutput.toJson(map).toString())}"
        def request = HttpRequest.newBuilder(new URI(url))
                .headers(
                        "Content-Type", "application/json",
                        "X-Restli-Protocol-Version", "2.0.0",
                        "LinkedIn-Version", "202301",
                        "Authorization", "Bearer $token"
                )
                .POST(HttpRequest.BodyPublishers.ofString(JsonOutput.toJson(map).toString()))
                .build()
        def response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofInputStream())
        if( response.statusCode() > 299 ){
            println response
            return null
        }
        def result = response.body().text
        println "<-${result}"
        if( !result ){
            return null
        }
        new JsonSlurper().parseText(result) as Map
    }

    static Map getJson(String url, String token){
        def request = HttpRequest.newBuilder(new URI(url))
                .headers(
                        "Content-Type", "application/json",
                        "X-Restli-Protocol-Version", "2.0.0",
                        "LinkedIn-Version", "202301",
                        "Authorization", "Bearer $token"
                )
                .GET()
                .build()
        def response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString())

        def result = response.body()
        println "<-${result}"
        new JsonSlurper().parseText(result) as Map
    }
}

Aunque parece tener mucho código en realidad es una implementación básica para usar las clases Http de Java y poder hacer un get y un post con unas cabeceras determinadas así como enviar y recibir Mapas como si fueran Json.

PostContent.groovy
@GrabConfig(systemClassLoader = true)
@Grab('info.picocli:picocli-groovy:4.7.3')

@picocli.groovy.PicocliScript2

import groovy.transform.Field
import static picocli.CommandLine.*

@Option(names = ["-a", "--author"], description = "nickname", required=true)
@Field String author = ''

@Option(names = ["-k", "--token"], description = "token", required=true)
@Field String token = ''

@Option(names = ["-t", "--test"], description = "run as test")
@Field boolean test = false

@Parameters
@Field List<String> content = []

String postURL = "https://api.linkedin.com/rest/posts"

if( test ){
        postURL="https://httpbin.org/delay/0"
}

Map post = [
        "author"                   : "urn:li:person:${author}",
        "commentary"               : content.join(' '),
        "visibility"               : "PUBLIC",
        "distribution"             : [
                "feedDistribution"              : "MAIN_FEED",
                "targetEntities"                : [],
                "thirdPartyDistributionChannels": []
        ],
        "lifecycleState"           : "PUBLISHED",
        "isReshareDisabledByAuthor": false
]

HttpUtil.postJson(post, postURL, token)

Este script usa Picocli para facilitar el parseo de comandos. Simplemente necesita un author y un token y además podemos indicar que se ejecute en modo test (-t) con lo que usará el servidor de httpbin como backend para testearlo

Por lo demás es un "simple" post con el formato que Linkedin espera para publicar un post

Graalvm

Las fases para convertir este script en binario las he separado en:

  • Pasar de Groovy a Java usando groovyc

  • Ejecutarlo con Java en modo test para que el agente genere la configuracion de Graalvm

  • Ejecutar el native-image para generar el binario

compile.sh
#!/bin/bash
set -e

# sdk use groovy 4.0.11
# sdk use java 22.3.r19-grl

echo compiling script
groovyc --configscript=compiler.groovy -d out PostContent.groovy

CP="$CP:./out"
CP="$CP:$HOME/.sdkman/candidates/groovy/4.0.11/lib/groovy-4.0.11.jar"
CP="$CP:$HOME/.sdkman/candidates/groovy/4.0.11/lib/groovy-json-4.0.11.jar"
CP="$CP:$HOME/.sdkman/candidates/groovy/4.0.11/lib/groovy-cli-picocli-4.0.11.jar"
CP="$CP:$HOME/.groovy/grapes/info.picocli/picocli/jars/picocli-4.7.3.jar"
CP="$CP:$HOME/.groovy/grapes/info.picocli/picocli-groovy/jars/picocli-groovy-4.7.3.jar"

echo generating graalvm configuration
java -Dgroovy.grape.enable=false -agentlib:native-image-agent=config-output-dir=conf/ \
    -cp "$CP" \
    PostContent -t -a test -k test test

echo building native image
native-image -Dgroovy.grape.enable=false \
    --no-server \
    --no-fallback \
    --report-unsupported-elements-at-runtime \
    --initialize-at-build-time \
    --initialize-at-run-time=org.codehaus.groovy.control.XStreamUtils,groovy.grape.GrapeIvy \
    -H:ConfigurationFileDirectories=out/conf/ \
    --enable-url-protocols=http,https \
    -cp "$CP" \
    -H:ConfigurationFileDirectories=conf/ \
    PostContent

Si todo va bien al final del proceso (poco más de 1 minuto) tendremos un binario postcontent que podremos ejecutar

./postcontent -a NH123123 -k A_BEARER_TOKEN_YOU_CAN_USE_MY_PREVIOUS_POST Hi this is a post from groovy script

INFO

Para obtener el token te remito a otro de mis post donde te cuento como generarlo, o si lo prefieres usar el servicio que he publicado.

Conclusion

Las nuevas versiones de Groovy y Graalvm permiten (con un poco de esfuerzo lo admito) poder crear comandos de consola binarios, lo que para mí abre la puerta a poder distribuir utilidades usando mi lenguaje favorito

Queda pendiente para un futuro post el poder adjuntar imágenes, que con el nuevo api Linkedin lo ha complicado un poco más.

Follow comments at Telegram group Or subscribe to the Channel Telegram channel

2019 - 2024 | Mixed with Bootstrap | Baked with JBake v2.6.7