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:
- sdkman, https://sdkman.io/install
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.
Top comments (0)