La herramienta jlink para generar runtimes de Java incluyendo exclusivamente los módulos que usa una aplicación

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

Java

En versiones anteriores de Java 9 había un único runtime para ejecutar cualquier aplicación que debía ser instalado previamente para la ejecución de la aplicación. Aunque la aplicación no usase Swing por ser una aplicación web o cosas como CORBA ya en desuso aún estaban disponibles en el runtime por motivos de no romper la compatibilidad con versiones anteriores.

Esto hacía que las aplicaciones no fuesen lo más eficiente posible y aumenta el tamaño necesario ahora de las imágenes de contenedores como Docker estas tuviesen un tamaño mayor del imprescindible, además de hacer que el tiempo de arranque sea algo mayor e incluir clases que aumentan la superficie de ataque ante un fallo de seguridad. Entre las novedades de Java 8 se añadieron los compact profiles que eran subconjuntos más reducidos del runtime pero eran conjuntos prefijados, las aplicaciones debían usar el mínimo que necesitasen si querían usar alguno. La solución de Java 8 fue una solución intermedia, los módulos de Java 9 han sido la solución completa.

Con la incorporación de la modularidad a Java 9 se posibilita generar runtimes con exclusivamente los módulos que necesite la aplicación, si la aplicación solo necesita el módulo java.base por ser muy sencilla se puede generar un runtime con solo este módulo. Los módulos son una mejora conveniente para la tendencia en el desarrollo de aplicaciones en forma de microservicios y ejecución con contenedores. La herramienta que posibilita generar runtimes personalizados con jlink que pueden ser ejecutados sin instalar previamente ningún JDK en el sistema y posibilitando que cada aplicación pueda usar su propio runtime.

Usando como ejemplo el caso de la aplicación con el cliente de HTTP/2 que tiene como dependencias únicamente el módulo jdk.incubator.httpclient de forma explícita y java.base de forma implícita en su definición de módulo la forma de generar un runtime específico para esta aplicación con jlink es la siguiente que utiliza el archivo con las dependencias de módulos declarados en el archivo module-info.java.

1
2
3
module blogbitix.http {
    requires jdk.incubator.httpclient;
}
module-info.java

Usando la linea de comandos o con una tarea de Gradle se genera el runtime para la aplicación con jlink. El módulo ha de compilarse previamente. Con la opción --launcher se crea un script para lanzar la aplicación con la clase que contiene el método main indicada como punto de entrada, la opción --output indica donde se genera el contenido del runtime.

1
2
3
$ jlink --module-path build/classes/java/main --add-modules "blogbitix.http" --launcher "run=blogbitix.http/io.github.picodotdev.blogbitix.http.Main" --output build/distributions/jlink

$ ./gradew jlink
jlink.sh

El espacio total del runtime en este caso de es de solo 38 MiB. Comparados con los casi 200 MiB que ocupa el OpenJDK comprimido que incluye todos los módulos, aproximadamente 500 MiB instalado y alrededor de entre 600 y 900 MiB dependiendo de la imagen base de Docker que se use se aprecia que el ahorro de espacio es considerable lo que redunda en tiempos de transferencia por red menores y un inicio más rápido de las aplicaciones.

El contenido del runtime y de su estructura de directorios es la indicada a continuación. Listando los módulos incluidos en este runtime en vez de todos los del JDK se observa que solo están incluidos los necesarios por la aplicación.

 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
77
$ du -sh build/distributions/jlink
 38M	build/distributions/jlink

$ build/distributions/jlink/bin/java --list-modules
blogbitix.http
java.base@10
jdk.incubator.httpclient@10

$ tree build/distributions/jlink
build/distributions/jlink
├── bin
│   ├── java
│   ├── keytool
│   └── run
├── conf
│   ├── net.properties
│   └── security
│       ├── java.policy
│       ├── java.security
│       └── policy
│           ├── README.txt
│           ├── limited
│           │   ├── default_US_export.policy
│           │   ├── default_local.policy
│           │   └── exempt_local.policy
│           └── unlimited
│               ├── default_US_export.policy
│               └── default_local.policy
├── include
│   ├── classfile_constants.h
│   ├── darwin
│   │   └── jni_md.h
│   ├── jni.h
│   ├── jvmti.h
│   └── jvmticmlr.h
├── legal
│   ├── java.base
│   │   ├── COPYRIGHT
│   │   ├── LICENSE
│   │   ├── aes.md
│   │   ├── asm.md
│   │   ├── cldr.md
│   │   ├── icu.md
│   │   ├── public_suffix.md
│   │   └── zlib.md
│   └── jdk.incubator.httpclient
│       ├── COPYRIGHT -> ../java.base/COPYRIGHT
│       └── LICENSE -> ../java.base/LICENSE
├── lib
│   ├── classlist
│   ├── jli
│   │   └── libjli.dylib
│   ├── jrt-fs.jar
│   ├── jspawnhelper
│   ├── jvm.cfg
│   ├── libjava.dylib
│   ├── libjimage.dylib
│   ├── libjsig.dylib
│   ├── libnet.dylib
│   ├── libnio.dylib
│   ├── libosxsecurity.dylib
│   ├── libverify.dylib
│   ├── libzip.dylib
│   ├── modules
│   ├── security
│   │   ├── blacklist
│   │   ├── blacklisted.certs
│   │   ├── cacerts
│   │   ├── default.policy
│   │   ├── public_suffix_list.dat
│   │   └── trusted.libraries
│   ├── server
│   │   ├── Xusage.txt
│   │   ├── libjsig.dylib
│   │   └── libjvm.dylib
│   └── tzdb.dat
└── release
info.sh
 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
apply plugin: 'java'
apply plugin: 'application'

ext.moduleName = 'blogbitix.http'
mainClassName = "$moduleName/io.github.picodotdev.blogbitix.http.Main"
println sourceSets.main.runtimeClasspath.asPath
repositories {
    jcenter()
}

dependencies {
}

compileJava {
    inputs.property("moduleName", moduleName)
    doFirst {
        options.compilerArgs = [
            '--module-path', classpath.asPath,
        ]
        classpath = files()  
    }
}

run {
    inputs.property("moduleName", moduleName)
    doFirst {
        jvmArgs = [
            '--module-path', classpath.asPath,
            '--module', mainClassName 
        ]
        classpath = files()
    }
}

task clearJlink(type: Delete) {
  delete "${distsDir}/jlink"
}

task jlink(dependsOn: [clearJlink, compileJava], type: Exec) {
     commandLine "jlink", "--module-path", sourceSets.main.runtimeClasspath.asPath, "--add-modules", moduleName, "--launcher", "run=${mainClassName}", "--output", "${distsDir}/jlink"
}
build.gradle

Una vez generado el runtime su uso es el siguiente:

1
2
$ build/distributions/jlink/bin/run

run.sh

En el vídeo Java in a World of Containers se comenta otra serie de características y opciones incluidas en Java para hacer de esta plataforma más consciente de las condiciones de ejecución propias de los contenedores.

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:
./gradew jlink


Comparte el artículo: