DEV Community

Jorge
Jorge

Posted on • Originally published at jorge.aguilera.soy on

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)
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)

Great read:

Is it Time to go Back to the Monolith?

History repeats itself. Everything old is new again and I’ve been around long enough to see ideas discarded, rediscovered and return triumphantly to overtake the fad. In recent years SQL has made a tremendous comeback from the dead. We love relational databases all over again. I think the Monolith will have its space odyssey moment again. Microservices and serverless are trends pushed by the cloud vendors, designed to sell us more cloud computing resources.

Microservices make very little sense financially for most use cases. Yes, they can ramp down. But when they scale up, they pay the costs in dividends. The increased observability costs alone line the pockets of the “big cloud” vendors.