Grape: Manejo de dependencias en Groovy

El manejo de dependencias generalmente es una pesadilla, incluso cuando usamos herramientas como Maven o Ivy. Hay que crear archivos de configuración y de construcción de proyectos, y eso ayuda mucho para proyectos complejos, pero cuando tenemos un script en Groovy, un programita rápido, pensamos que no queda otra opción que poner las dependencias en la línea de comandos.

Pero no es así.

Groovy incluye un excelente mecanismo para manejo de dependencias, llamado Grape, y es muy simple de usar. Supongamos que queremos hacer un script para conectarse a una base de datos y procesar algunos registros, y enviar al final un correo (ah y por supuesto dejar todo en un log con SLF4J).

Aquí pongo un ejemplo sencillo (sin lo del correo), usando la clase Sql de Groovy:

import org.slf4j.*
import groovy.sql.*

Logger log = LoggerFactory.getLogger(getClass())

log.info('Creando conexion a base de datos')
Sql sql = Sql.newInstance('jdbc:postgresql://localhost/javamexico', 'javamex', 'javamex2', 'org.postgresql.Driver')

log.info('Leyendo tabla')
sql.eachRow('SELECT * FROM usuario') {
  println "Usuario $it.uname (${it.email}) registrado el dia $it.fecha_alta tiene reputacion $it.reputacion"
}
log.info('Listo.')

Si ejecutamos este script obtendremos un error:

$ groovy Ejemplo.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
/Users/ezamudio/Projects/Personal/ejemplos/Ejemplo.groovy: 4: unable to resolve class Logger 
 @ line 4, column 8.
   Logger log = LoggerFactory.getLogger(getClass())
          ^

1 error

Necesitamos ejecutar el script indicando las dependencias en el classpath, algo como esto:

$ groovy -cp /usr/java/lib/postgresql-8.4-702.jdbc4.jar:/usr/java/lib/slf4j/slf4j-api-1.6.1.jar:/usr/java/lib/slf4j/slf4j-simple-1.6.1.jar Ejemplo.groovy
17 [main] INFO Ejemplo - Creando conexion a base de datos
245 [main] INFO Ejemplo - Leyendo tabla
Usuario uname2 (b@c.com) registrado el dia 2009-12-22 09:59:23.825 tiene reputacion 48
Usuario domix (x@y.com) registrado el dia 2009-12-22 10:42:01.628 tiene reputacion 41
Usuario ezl (z@w.com) registrado el dia 2009-12-22 10:42:01.421 tiene reputacion 52
Usuario jb (x@g.c) registrado el dia 2009-12-22 10:42:01.477 tiene reputacion 79
Usuario uname1 (a@b.com) registrado el dia 2009-12-22 09:59:23.66 tiene reputacion 27
Usuario ecamacho (e@t.c) registrado el dia 2009-12-22 10:42:01.563 tiene reputacion 30
378 [main] INFO Ejemplo - Listo.

Bastante latoso, eso de poner el classpath en la línea de comando. Qué pasa si fue un script que hicimos al aventón, en un servidor remoto, mediante una sesión de ssh, por un enlace lento, y resulta que NO tenemos a la mano las bibliotecas necesarias? Nos falta un driver, o el logger simple de SLF4J...

Usando Grab

La manera más sencilla de usar Grab es por medio de anotaciones. Grab funciona de manera muy similar a Ivy o Maven, pero en tiempo de ejecución obtiene las dependencias necesarias. Si tienen instalado ya Groovy (y si no, qué esperan), pueden teclear en su línea de comando simplemente grab para comprobar que tienen la herramienta. Con ella pueden instalar dependencias directamente en la línea de comando, y se guardan en un repositorio para el usuario que ejecuta el programa, de modo que quedará disponible para todos sus scripts en Groovy, siempre y cuando lo solicite por medio de la anotación @Grab o usando directamente la clase Grape. Así que al script agregamos esto:

import org.slf4j.*
import groovy.sql.*

@Grapes([
  //Este va sin version, por lo tanto bajará el más nuevo
  @Grab('postgresql:postgresql'),
  //Este va con versión
  @Grab('org.slf4j:slf4j-simple:1.6.1'),
  //Esto es para que agregue las dependencias al classLoader del sistema
  //Es necesario por el driver de PostgreSQL
  @GrabConfig(systemClassLoader=true)
])
Logger log = LoggerFactory.getLogger(getClass())
...
//resto del script

Con lo anterior, al momento que ejecutamos el script, veremos que el script aparentemente tarda en arrancar... eso es porque se están descargando las dependencias que no se tienen instaladas aún y luego se ejecuta el script, sin necesidad de especificar las dependencias en la línea de comando.

Si solamente necesitan una dependencia, pueden poner directamente la anotación @Grab sin tener que poner la de @Grapes; esta última sirve para poder juntar varios @Grab, opcionalmente con la de @GrabConfig.

Como ya mencioné, se maneja un cache local. Para verlo, podemos ejecutar el comando grape para ver qué tiene nuestro sistema (el mío se ve así porque ya lo he usado antes, pero borré algunas dependencias que vamos a usar):

$ grape list

commons-logging commons-logging  [1.1.1]
org.apache apache  [4]
org.apache.commons commons-parent  [5]
org.slf4j slf4j-api  [1.6.1]
org.slf4j slf4j-parent  [1.6.1]
org.slf4j slf4j-simple  []
org.springframework spring-asm  [3.0.5.RELEASE]
org.springframework spring-core  [3.0.5.RELEASE]
org.springframework spring-parent  [3.0.5.RELEASE]
postgresql postgresql  [8.4-702.jdbc4]

10 Grape modules cached
9 Grape module versions cached
$ groovy Ejemplo.groovy
17 [main] INFO Ejemplo - Creando conexion a base de datos
245 [main] INFO Ejemplo - Leyendo tabla
Usuario uname2 (b@c.com) registrado el dia 2009-12-22 09:59:23.825 tiene reputacion 48
Usuario domix (x@y.com) registrado el dia 2009-12-22 10:42:01.628 tiene reputacion 41
Usuario ezl (z@w.com) registrado el dia 2009-12-22 10:42:01.421 tiene reputacion 52
Usuario jb (x@g.c) registrado el dia 2009-12-22 10:42:01.477 tiene reputacion 79
Usuario uname1 (a@b.com) registrado el dia 2009-12-22 09:59:23.66 tiene reputacion 27
Usuario ecamacho (e@t.c) registrado el dia 2009-12-22 10:42:01.563 tiene reputacion 30
378 [main] INFO Ejemplo - Listo.

La siguiente vez que se ejecute el script, ya no se descarga nada porque se queda en el cache local y por tanto el script arrancará más rápido. Y después de ejecutarlo, si revisamos las dependencias instaladas en cache local:

$ grape list

commons-logging commons-logging  [1.1.1]
org.apache apache  [4]
org.apache.commons commons-parent  [5]
org.slf4j slf4j-api  [1.6.1]
org.slf4j slf4j-parent  [1.6.1]
org.slf4j slf4j-simple  [1.6.1]
org.springframework spring-asm  [3.0.5.RELEASE]
org.springframework spring-core  [3.0.5.RELEASE]
org.springframework spring-parent  [3.0.5.RELEASE]
postgresql postgresql  [8.2-504.jdbc3, 8.4-702.jdbc4, 9.0-801.jdbc4]

10 Grape modules cached
12 Grape module versions cached

Podemos ver que se reinstaló slf4j-simple, y dos versiones de los drivers de PostgreSQL.

Grape no solamente sirve para manejo de dependencias en scripts; también se pueden anotar clases, de modo que por ejemplo si tienen una clase que se encarga de enviar correos, y le ponen un @Grab con Java Mail, entonces si usan esa clase en un script, se cargará la dependencia de Java Mail.

Dado que Grape usa internamente Ivy, también se hace resolución de dependencias transitivas. Es decir, si en su script indican que quieren usar la biblioteca A pero para poder usar esa biblioteca se necesita cargar B y C, y para poder usar B se necesita cargar la biblioteca D, entonces un simple @Grab('grupo:A') causará que se carguen las bibliotecas A, B, C y D.

Limitaciones

Por el momento, el uso de @Grab tiene algunas limitaciones, que afectan principalmente la integración con aplicaciones Java. Este ejemplo que puse aquí es un caso, de hecho. Si compilamos el script:

groovyc Ejemplo.groovy

Obtendremos un par de clases, Ejemplo.class y Ejemplo$_run_closure1.class. Si corremos esto desde Java, agregando al classpath las dependencias de Ivy y Groovy, obtendremos un error:

$ java -cp $GROOVY_HOME/embeddable/groovy-all-1.7.10.jar:$GROOVY_HOME/lib/ivy-2.2.0.jar:. Ejemplo
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.RuntimeException: No suitable ClassLoader found for grab
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
	at org.codehaus.groovy.reflection.CachedConstructor.invoke(CachedConstructor.java:77)
	at org.codehaus.groovy.runtime.callsite.ConstructorSite$ConstructorSiteNoUnwrapNoCoerce.callConstructor(ConstructorSite.java:102)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallConstructor(CallSiteArray.java:52)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:190)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:198)
	at groovy.grape.GrapeIvy.chooseClassLoader(GrapeIvy.groovy:173)
...
...

Esto es un problema del cual el equipo de Groovy está consciente, espero que para la versión 1.8 lo arreglen. Será genial ver que una clase Groovy anotada con @Grab se pueda agregar a una aplicación Java y que con eso, al ejecutar la aplicación se carguen las dependencias faltantes al classpath.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de Abaddon

Muy bueno Grape

Es una gran herramienta, aunque no he visto como hacer que vaya notificando que librerias esta bajando y que porcentaje lleva. Esto me causo muchos problemas en un principio porque no sabia porque tardaba TANTO la ejecución de un script y como pense que no estaba ejecutandose bien pues mataba la ejecución del mismo y no dejaba que se bajaran todas las librerias correctamente.

Imagen de ezamudio

log?

Si usas un sistema de log como log4j o slf4j, tal vez haya que configurar algo para org.apache.ivy (ponerle nivel DEBUG) y con eso sale lo que se está descargando...

Imagen de greeneyed

No parece que tengan el log solucionado, todavía.

Viendo este tema de discusión, no parece que todavía esté solucionado:
http://groovy.329449.n5.nabble.com/Grape-log-output-when-using-annotatio...
pero al menos esta claro que lo tendrán en cuenta.

Ojalá en Java también se pudiera hacer algo así :)

Re: No parece que tengan el log solucionado, todavía.

Ese es el problema con Java, a Java no lo configuras con Java...Filosofía que manejan lenguajes cómo Groovy, Python o Ruby; hacer las configuraciones con el mismo lenguaje y no con el "auxilio" de un XML. Por ejemplo con Python, manejas el fichero __init__.py en donde toda la configuración (constantes de la aplicación) van aquí y son utilizables dentro del proyecto (cosa que con Maven o JPA por ejemplo, la configuración es vía XML).

En Java sería súper cómodo tener algo cómo Grape pero con Ivy o Maven. Sin XML para configurar y Java para programar, simplemente Java para configurar y Java para programar.

Imagen de Sr. Negativo

Vientos funciona muy bien :D !!

Pues ya no me quedé con la duda y lo probé:

Conecta.groovy

import groovy.sql.*
import groovy.grape.Grape

inicio()

@Grapes([
@Grab('mysql:mysql-connector-java:5.1.12'),
@GrabConfig(systemClassLoader=true)
])
def inicio(){
        if(args){
               
                def usuario=args[0]
                println "bienvenido: ${usuario}"
                Sql sql=Sql.newInstance("jdbc:mysql://localhost/agenda",usuario,"rooter","com.mysql.jdbc.Driver")
                sql.eachRow("select * from usuarios") {
                        println "Usuarios de la base: ${it.nombre}"
                }
       
        }else{
                println "error, debes introducir tu nombre"
        }
}

Compilar y ver la lista

groovyc Conecta.grovvy
//ver la lista
grape list

Ejecutar y esperar el milagro
groovy Conecta.groovy

Problemas con java

Hola encantado de hablar contigo he visto que eres una máquina con java. Tengo un gran problema con java del cual no tengo ni idea. Bueno algo se, resulta que he notado que se estan conectanto a mi pc por medio de java (creo ven lo que hago) en una red local, pues es un internet compartido por mucha gente.

Hay un archivo java.policy que se supone que regula esto (o eso creo).

En fin es un problema grave de seguridad, pues ya me han estropeado varias veces mi email y demas.

Prodrias hecharme una mano y decirme que tendría que hacer para impedir conexiones java a mi pc desde otro pc (además he notado cosas raras en el navegador opera). Sin desintalar java, pues muchas webs lo usan.

Por favor ayudame estoy desesperado y no tengo ni idea de aumentar la seguridad.

Un saludo

????

?

Imagen de ezamudio

tema

rastas, esas dudas nada tienen que ver con el tema de este post, por lo que deberías abrir un post aparte para esa duda.

Pero esa duda realmente tampoco tiene que ver con Java, según yo... no sé cómo determinaste que "se están conectando a tu pc por medio de Java" pero no entiendo bien qué quiere decir. De cualquiera manera es un tema aparte y probablemente no tenga nada que ver con Java, pero si abres un post nuevo donde expliques a detalle tu problema, ya todos podremos leerlo y probablemente sugerir soluciones.