Empaquetar una aplicación Java en un archivo jar ejecutable incluyendo sus dependencias

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

Las aplicaciones Java se distribuyen en uno o varios archivos jar. Si queremos facilitar la distribución de la aplicación con un único archivo jar existe la posibilidad de reempaquetar el jar de la aplicación y sus dependencias en tiempo de ejecución en un nuevo archivo jar que lo contenga todo, a este nuevo jar se le conoce como uberjar o fatjar.

Java

La forma de distribuir el código compilado a bytecode en Java es a través de archivos de extensión jar. Los archivos jar no son más que archivos comprimidos den formato zip. Si se les cambia de extensión y se descomprimen se extrae su contenido seguramente con una buena cantidad de archivos de extensión class que es la extensión para los archivos Java compilados a bytecode y que la máquina virtual interpreta para su ejecución. Las librerías que use la aplicación también se deben distribuir junto a esta para que funcione, por lo que la aplicación se distribuirá en forma de una colección de archivos jar.

Una aplicación distribuida en forma de múltiples archivos archivos jar se ejecuta con una línea de comandos como la siguiente en la que el parámetro -cp indica las ubicaciones donde se buscarán librerías jar y archivos class si se distribuyen de forma individual, el segundo parámetro indica la clase que contiene el método main que inicia la aplicación. Previamente hay que generar el artefacto del proyecto con Gradle.

1
2
$ ./gradlew assemble

gradlew-assemble.sh

En este caso se trata de una aplicación que emite un arte en formato de caracteres ASCII en la terminal donde cada linea aplica un color diferente mediante la librería Jansi en la terminal que la aplicación tiene como dependencia.

1
2
$ java -cp build/classes/java/main/:"build/dependencies/jansi-1.17.1.jar" io.github.picodotdev.blogbitix.uberjar.Main

java-cp.sh

Aplicación de ejemplo ejecutada con classpath

Aplicación de ejemplo ejecutada con classpath

Cuando la aplicación está contenida en un archivo jar y se ejecuta con la opción -jar se ignora el parámetro -cp y no se indica la clase main del punto de entrada de la aplicación. En el caso de las aplicaciones distribuidas en un archivo jar tanto la clase main como las dependencias se indican en un archivo de manifiesto incluido en el propio archivo jar. El archivo se ubica en META-INF/MANIFEST.MF dentro del jar, es un archivo de texto donde se indican varias propiedades en forma de atributo y valor, una en cada linea. Un ejemplo de archivo de manifiesto sería el siguiente:

1
2
3
4
Manifest-Version: 1.0
Created-By: picodotdev
Main-Class: io.github.picodotdev.blogbitix.uberjar.Main
Class-Path: jansi-1.17.1.jar
MANIFEST.MF

La propiedad Manifest-Version y Created-By son informativas de la versión del archivo de manifiesto y el autor de la librería jar. La propiedad Main-Class indica la clase main de la librería o aplicación y la propiedad Class-Path es una lista separada por espacios de librerías adicionales. Las propiedades Main-Class y Class-Path son los parámetros que indicamos como parámetros en el comando java anterior. Con el archivo jar, su manifiesto y las librerías la aplicación Java se inicia de forma un poco más sencilla que antes al no tener que indicar ni la clase main ni el classpath.

1
2
$ java -jar build/libs/JavaUberjar.jar

java-jar.sh

Como en este caso, si Java no se encuentra la dependencia de Jansi y se produce la siguiente excepción que indica que no se ha encontrado una clase necesaria.

1
2
3
4
5
6
7
8
Exception in thread "main" java.lang.NoClassDefFoundError: org/fusesource/jansi/AnsiConsole
	at io.github.picodotdev.blogbitix.uberjar.Main.main(Main.java:9)
Caused by: java.lang.ClassNotFoundException: org.fusesource.jansi.AnsiConsole
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 1 more
Exception.out

Sin embargo, para distribuir la aplicación aún hay que distribuir varios archivos jar, el de la aplicación y los jar de las librerías que necesite la aplicación. En este caso solo es un jar adicional ya que la aplicación solo tiene una dependencia y esta transitivamente no tiene ninguna otra pero en una aplicación más compleja el número de dependencias puede llegar a la centena.

Para hacer la distribución más sencilla hay una posibilidad que usan algunos programadores de Java que es reempaquetar todas las clases del jar de la aplicación y de las librerías en un nuevo jar, a esta opción de reempaquetar las clases se le conoce como uberjar o fatjar. En la herramienta de construcción Gradle existe un plugin para realizar esta tarea de creación del uberjar pero también se puede hacer definiendo una tarea sin necesidad del plugin. El archivo de Gradle adaptado para producir un uberjar de forma automatizada es el siguiente. La tarea importante en el caso del ejemplo es uberJar.

 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
plugins {
    id 'java'
    id 'application'
}

mainClassName = 'io.github.picodotdev.blogbitix.uberjar.Main'

repositories {
    jcenter()
}

dependencies {
    compile 'org.fusesource.jansi:jansi:1.17.1'

}

jar {
    manifest {
        attributes 'Main-Class': mainClassName
        attributes 'Class-Path': configurations.compile.collect { it.name }.join(' ')
    }
}

task copyDependencies(type: Copy) {
    group = 'Custom'
    description = 'Copies runtime dependencies'
    from configurations.runtime
    into "$buildDir/dependencies"
}

task uberJar(type: Jar) {
    manifest {
        attributes 'Main-Class': mainClassName
    }
    classifier 'uberjar'
    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
    with jar
}

task sourcesJar(type: Jar) {
    group = 'Custom'
    description = 'Builds sources jar'
    classifier 'sources'
    from sourceSets.main.allSource
}

task javadocJar(type: Jar, dependsOn: javadoc) {
    group = 'Custom'
    description = 'Builds javadoc jar'
    classifier 'javadocdoc'
    from javadoc.destinationDir
}

artifacts {
    archives uberJar
    archives sourcesJar
    archives javadocJar
}

distributions {
    main {
        baseName = "$archivesBaseName-bin"
    }
    docs {
        baseName = "$archivesBaseName-docs"
        contents {
            from(libsDirName) {
                include sourcesJar.archiveName
                include javadocJar.archiveName
            }
        }
    }
}

jar.dependsOn copyDependencies
distZip.dependsOn sourcesJar, javadocJar
build.gradle

Con el siguiente comando la ejecución de la aplicación empaquetada como uberjar produce el mismo resultado. Con el uberjar en el archivo de manifiesto no es necesario incluir el atributo Class-Path ya que todas las clases necesarias tanto de la aplicación como de las dependencias ha sido empaquetadas en el jar.

1
2
$ ./gradlew assemble

gradlew-assemble.sh
1
2
$ java -jar build/libs/JavaUberjar-uberjar.jar

java-jar-uberjar.sh

Aplicación de ejemplo ejecutada con uberjar

Aplicación de ejemplo ejecutada con uberjar

Hay un plugin de Gradle para generar uberjars que ofrece varias opciones para filtrar los archivos que se incluyen en el fatjar, fusionar los archivos de servicios que permiten extender funcionalidades y otras tareas para realizar generar el distribuible con distShadowZip y distShadowTar. Spring Boot ofrece algo similar con la tarea bootRepackage pero si no se trata de una aplicación que use Spring Boot lo anterior sirve para cualquier otra aplicación Java como sería el caso de una aplicación de escritorio que utiliza JavaFX.

Que la aplicación sea un único jar tiene la ventaja que el distribuible es más sencillo y facilita desplegarlo en un entorno de producción, en el caso de usar Docker también es más adecuado un único archivo.

Entre la documentación hay unas muy buenas guías prácticas sobre Java, una de ellas sobre el empaqueado de aplicaciones en archivos jar. La información que se encuentra en estas guías y tutoriales es muy valiosa para cualquier programador que use el lenguaje Java.

Terminal

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:
./gradlew assemble && java -jar build/libs/JavaUberjar-uberjar.jar


Comparte el artículo: