Native Applications: Spring Native

Publicado por Omar N. Muñoz Mejía el

El mundo del desarrollo de aplicaciones sigue avanzando y con él la necesidad de obtener el mayor rendimiento de los recursos con que se cuenta para su ejecución ya que esto representa beneficios para todos los participantes, desde los económicos y de optimización para dueños del servicio hasta la experiencia final del usuario final.

Para lenguajes de programación como Java para conseguir esa eficacia y optimización de recursos en las aplicaciones existen varias posibilidades, y sus objetivos para este ámbito siempre han sido claros: conseguir mejores tiempos de arranque de las aplicaciones, aplicaciones más ligeras, obtener un rendimiento máximo instantáneo y por supuesto, reducir el consumo de memoria de estas.  

Hasta ahora hemos visto como proyectos como Quarkus o frameworks como Micronaut realizan grandes aportes a la comunidad de desarrollo para brindar herramientas de construcción de software que posibiliten la implementación de aplicaciones ligeras y optimizadas en cuanto a consumo de recursos. Aunque estos 2 proyectos antes mencionados llevan tiempo y soporte de la comunidad, existe una tercera opción; una iniciativa llamada Spring Native y que cada vez toma más fuerza y seguidores.

MicrosoftTeams-image--11--1

Overview

La iniciativa nace de la necesidad de minimizar el impacto por la adopción de un nuevo framework para el desarrollo de aplicaciones Java para creación de artefactos o aplicaciones nativas, y es de conocimiento general que Spring lleva muchos años haciendo un gran aporte en la generación de herramientas para la construcción de aplicaciones Java como puede ser el framework Spring Boot, que se ha convertido en uno de los marcos de trabajo más utilizados, en especial en el desarrollo de microservicios.

application-framework-java-technology
Fuente:Java Developer Productivity Report By JRebel

Viendo la participación de Spring Boot como marco de trabajo en más de un 60% de los desarrollos de microservicios Java, no sería del todo descabellado utilizar un set de librerías que nos den soporte para la construcción de aplicaciones nativas, que estén soportadas por una marca como Spring y que además genere un impacto mínimo en el desarrollo de las aplicaciones ya que se sigue utilizando el mismo framework comúnmente conocido por la mayoría de los desarrolladores, Sprint Boot.

Características

Algo muy importante y que hay que tener claro cuando hablemos de Spring Native es que es un proyecto experimental y que no tiene una versión oficial liberada por Spring.io, para ser más preciso, durante este artículo trabajamos con versiones release candidate y que según lo previsto Spring Boot incluirá una versión oficial con la liberación de Spring Boot 3 y Spring Framework 6, a finales de Noviembre del 2022.

Spring Boot 3, que finalmente traerá la versión oficial de Spring Native, requiere como mínimo Java 17 por lo que en este artículo se toma como base esta versión tanto para el compilador JDK como para la versión de GraalVM. Los procesos de compilación y empaquetado de aplicaciones native utilizando Spring Native y GraalVM demandan grandes recursos de máquina y tiempo de construcción del artefacto, por lo que lo recomendable es integrar estos procesos (CICD) de generación de artefactos nativos sólo para entornos productivos donde los tiempos de construcción no penalice el ciclo de vida de desarrollo y pruebas del proyecto.

graalvm1

Spring Native compila aplicaciones Spring a través del compilador GraalVM generando ejecutables nativos, que comparados con los artefactos generados por una JVM sus tiempos de arranque, consumos de memoria y rendimiento instantáneos son muchos mejores, lo que posibilita la utilización de estas aplicaciones nativas en arquitecturas de microservicios donde este tipo de características son muy necesitadas y aportan grandes ventajas en la disponibilidad de los servicios.

Hints

Spring Native porvee una serie de @Hints o "pistas" para el compilador GraalVM, en las que se declara y guía su proceso de compilado para las clases de Spring/Spring Boot y algunas integraciones de terceros. Por esto se puede decir que Spring Native genera soporte nativo para un cierto número de integraciones "out-of-the-box" sin tener que realizar ninguna configuración adicional para su funcionamiento en un compilado nativo. Ver todas las integraciones soportadas por Sprig Native aquí.

Spring Native es consciente de que no cubre todo el rango de integraciones que soporta Spring/Spring Boot por lo que deja un juego de herramientas para realizar configuraciones manuales en las aplicaciones para para darle soporte y poder ser llevadas a un artefacto nativo. Se usan estas configuraciones manuales cuando:

  • Spring Native no está soportando el uso de una característica o librería en particular.
  • Es necesario especificar una configuración particular de tu propia aplicación.

Existen 2 formas de utilizar @Hints, la primera a través de archivos de configuración y la segunda mediante un juego de clases (Anotaciones) que proporciona Spring Native.

Properties Hints

Conocido también como GraalVM Hints, permite que la configuración de clases para el compilador GraalVM se generan sobre archivos estáticos que son automáticamente cargados en tiempo de construcción por el Spring AOT plugin:

Ubicación: src/main/resources/META-INF/native-image/

Properties files (Ejemplos):

Definición de argumentos de compile/build de la aplicación nativa.

  • native-image.properties
Args = --enable-all-security-services \
-H:IncludeResourceBundles=oracle.net.jdbc.nl.mesg.NLSR,oracle.net.mesg.Message \
        --allow-incomplete-classpath \
…

Definición de acceso por reflexión.

  • reflect-config.json
[
 {
 "name":"oracle.jdbc.driver.T4CDriverExtension",
 "allDeclaredFields": true, 
 …
 }, 
 …
]

Definición de proxy.

  • proxy-config.json
[
 [
 "org.springframework.transaction.annotation.Transactional",
 "org.springframework.core.annotation.SynthesizedAnnotation"
 …
 ], 
 …
]

Definición de recursos empaquetados en la imágen nativa.

  • resource-config.json
[
 "resources":[
    {"pattern":"META-INF/services/java.sql.Driver"}, 
    {"pattern":"oracle/sql/converter_xcharset/lx20002.glb"}, 
    {"pattern":"oracle/sql/converter_xcharset/lx2001f.glb"}, 
    {"pattern":"oracle/sql/converter_xcharset/lx200b2.glb"}
  ] 
 …
]

Annotated Hints

También conocidas como Sprint AOT Typesafe Hints, permite lograr una configuración utilizando anotaciones que proporciona Spring Native, tienen el mismo resultado que el anterior método y pueden ser anotadas cualquier componente de @configuration de la aplicación.

Configuración

Definición de proxy.

@AotProxyHint 
@JdkProxyHint 

Definición de acceso por reflexión.

@TypeHint
@FieldHint
@MethodHint

Definición de recursos empaquetados en la imágen nativa.

@ResourceHint

Definición de tipos serializables.

@SerializationHint

Container

Es un extensión de tipo @TypeHint que ofrece más opciones.

@NativeHint

Initialización

Inicializado en tiempo de compilación/ejecución.

@InitializationHint

Spring Native Application [Caso Práctico]

Prerrequisitos

Para el caso práctico se escoge una base de datos cuya dependencia para el compilador genere problemas de reflexión al momento generar o ejecutar la versión nativa de la aplicación para poder incluir Hints de Spring Native como solución.

Proyecto y dependencias

El artefacto está basado en un proyecto Maven, perfectamente se pueden extrapolar sus dependencias a uno Gradle, y necesita algunas versiones mínimas de Spring Boot y Java para poder dar soporte a generación de aplicaciones nativas.

Visitar repositorio para obtener más información detallada del POM y sus dependencias.

Dependencias

Parent POM

Spring Native 0.12.1 solo soporta Spring Boot 2.7.1+ por lo que es necesario la dependencia padre como base del proyecto.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.1</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

Spring Native

Para dar soporte y poder generar compilaciones de aplicaciones Spring a ejecutables nativos usando el compilador de GraalVM se necesita agregar la dependencia de Spring Framework para Spring Native.

Versión 0.12.1

<dependency>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-native</artifactId>
    <version>${spring-native.version}</version>
</dependency>

Java 17

Es necesario intentar aproximar lo más posible las dependencias de la aplicación a las versiones requeridas por la RC que pretende liberar de Spring Native. Es aconsejable llevar nuestra versión Java a la mínima necesaria con que se liberará Spring Native, en este caso la versión Java 17 ya que vendrá de la mano de Spring Boot 3 de acuerdo con su roadmap.

<java.version>17</java.version>

Openapi Native

Para la documentación de Rest Services Openapi ha desarrollado su propia implementación para dar soporte a Spring Native y poder incluir estas dependencias en ejecutables nativos. Es por esto que necesitamos incluir estas librerías para continuar soportando este tipo de documentación.

Versión 1.6.11

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-native</artifactId>
    <version>${springdoc-openapi.version}</version>
</dependency>

Plugins

spring-aot-maven-plugin

Plugin necesario para ejecutar transformaciones AOT requeridas para lograr la compatibilidad a imágenes nativas de las aplicaciones.

<plugin>
    <groupId>org.springframework.experimental</groupId>
    <artifactId>spring-aot-maven-plugin</artifactId>
    <version>${spring-native.version}</version>
     <!-- ... -->
</plugin>    

spring-boot-maven-plugin

Plugin que otorga soporte de Spring Boot en Maven posibilitando la generación de artefactos ejecutables con los que luego iniciar la aplicación. Para Spring Native toma importancia los Buildpacks ya que permiten la generación de imágenes Docker con el ejecutable native embebido de la aplicación Spring Boot como resultado del proceso de empaquetado activando la variable de entorno BP_NATIVE_IMAGE.

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <name>localhost/${project.artifactId}:latest</name>
            <builder>paketobuildpacks/builder:tiny</builder>
            <env>
                <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
            </env>
            <pullPolicy>IF_NOT_PRESENT</pullPolicy>
        </image>
    </configuration>
</plugin>    

La versión Tiny de la imagen a utilizar para crear la imagen Docker de la aplicación Spring Boot puede ser modificada a base o full para conseguir más opciones para el desarrollador a la hora de construir las imágenes Docker. Más información.

native-maven-plugin

Para invocar la compilación de imagen nativa para la aplicación Spring Boot GraalVM ha creado un plugin que otorga esta capacidad en el proyecto. En el POM ejemplo, se creará un perfil de Maven para lanzar este tipo compilación solo cuando el usuario así lo requiera.

<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <version>0.9.13</version>
    <extensions>true</extensions>
    <executions>
        <execution>
            <id>build-native</id>
            <goals>
                <goal>build</goal>
            </goals>
            <phase>package</phase>
        </execution>
        <execution>
            <id>test-native</id>
            <goals>
                <goal>test</goal>
            </goals>
            <phase>test</phase>
        </execution>
    </executions>
    <configuration>
        <!-- ... -->
    </configuration>
</plugin>    

Repositorios

Spring Native requiere agregar el repositorio release para sus dependencias y por obvias razones la ubicación de este difiere a los ya conocidos de Maven Central donde se encuentran las versiones finales de Spring por lo que toca incluirlo de forma manual en el POM de la aplicación.

<repositories>
    <!-- ... -->
    <repository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </repository>
</repositories>

Plugin Repositorios

<pluginRepositories>
    <!-- ... -->
    <pluginRepository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </pluginRepository>
</pluginRepositories>

Implementación

La implementación del servicio rest a exponer consiste en realizar un desarrollo normal como cualquier otro proyecto Spring Boot, configurando los valores necesarios y definiendo los componentes de Spring (Components, Reposotories, Services,Controllers, etc) que la lógica de la aplicación requiere sin tener en cuenta ningún aspecto de una aplicación native ya que para este apartado en particular no se necesita. Para la aplicación ejemplo se implementan los siguientes componentes:

  • Repository: AlertRepository con la lógica para obtener las alertas a través de un template JDBC.
  • Alert: Clase para modelar la entidad alerta.
  • AlertController: Controlador rest para exponer los servicios de la aplicación.
  • AppSwaggerConfig: Clase de configuración para OpenAPI.

Los detalles de la implementación se encuentran en el repositorio.

Generación Ejecutable Nativo

Para la generación de un artefacto ejecutable nativo existen 2 posibilidades de acuerdo con lo configurado actualmente para el proyecto, generar una imagen Docker con el ejecutable nativo embebido o generar un ejecutable nativo utilizando GraalVM. A continuación, se detallará las 2 formas antes mencionadas:

Generación Docker Image

Importante: La máquina donde se pretende generar la imagen Docker debe tener instalado previamente Docker como se ha mencionado en los prerrequisitos.

Esta generación se hace mediante el plugin Cloud Native Buildpacks de Spring Boot y consiste en activar la varible de entorno BP_NATIVE_IMAGE, setear el builder a utilizar (para el caso práctico Tiny) y el nombre final de la imagen a generar, que para el ejemplo sería:

localhost/${project.artifactId}:latest

Para ejecutar la construcción se lanza el siguiente comando:

mvn spring-boot:build-image

El anterior comando compilará y construirá la imagen Docker con el artefacto nativo:

BuildWithDocker

Ejecución

Una vez la imagen Docker se ha construido con el artefacto deseado quedaría solo levantar la aplicación. Para el caso práctico se conseguiría con el siguiente comando:

docker run --rm -p 7080:7080 localhost/alert-service-native 

El servicio Rest ejemplo tiene dependencia con una base que se encuentra por fuera del contenedor levantado a partir de la imagen Docker por lo que para este caso en particular se necesita iniciar la aplicación indicando la ruta externa del servicio de base de datos:

docker run -p 7080:7080 localhost/alert-service-native --spring.datasource.plain-url=192.168.1.139:49161/xe

RunWithDocker2

Swagger del Servicio

Importante: La URL del swagger-ui usando imágenes nativas debe contener la versión del swagger-ui, para este caso sería:
http://localhost:7080/swagger-ui/4.14.0/index.html

Swagger

Generación con GraalVM

Importante: La máquina donde se pretende generar la aplicación nativa debe tener instalado previamente GraalVM para el OS en cuestión como se ha mencionado en los prerequisitos.

Esta generación se hace utilizando el profile "native" del POM de la aplicación y que a su vez utiliza el plugin native-maven-plugin de GraalVM. El plugin proporiciona diferentes opciones al desarrollador para la generación del AOT del artefacto. Más información.

Para ejecutar la construcción se lanza el siguiente comando:

mvn clean package -Pnative -DskipTests
Promp

El anterior comando creará un ejecutable en la carpeta target del proyecto, el cual variará dependiendo del sistema operativo de la máquina donde compile y empaquete.

Importante: La compilación y empaquetado son procesos que demandan una gran cantidad de recursos por lo que se aconseja tener una máquina de desarrollo con mínimo 6GB de RAM disponibles.
Para este ejercicio en particular se contó con una máquina con 32GB de RAM y el tiempo de generación del artefacto fue de 2m 36s. Pero estos tiempos van a depender del tamaño del aplicativo y los recursos disponibles de máquina en ese instante.

Ejecución

Una vez creado el ejecutable nativo quedaría solo levantar la aplicación. Para el caso práctico se conseguiría con el siguiente comando:

cd target
alert-service-native.exe
graalvm1

Swagger del Servicio

Swagger

Errores

Con Spring Native es posible encontrar errores en el proyecto por dependencias que no pueden ser resueltas en tiempo de compilación/arranque de la aplicación y aunque muchas de las dependencias, las más comunes, ya se encuentran soportadas por Spring Native, existen otras cuya configuración es necesaria realizarlas manualmente.

En el caso práctico se ha incluido la dependencia con la librería Oracle JDBC con el fin de replicar este escenario, ya que algunas de sus clases no pueden ser configuradas automáticamente en el empaquetado por GraalVM para ser detectadas en el arranque de la aplicación, presentado errores como el siguiente:

Error

Básicamente el error señala que por reflexión no puede ser encontrada la dependencia con la clase OracleDriver necesaria para la persistencia del servicio.

Solución

Para este caso lo más sencillo es utilizar las Hints de Spring Native, es una de las formas de decirle explícitamente a GraalVM en el momento de la construcción que tenga en cuenta ciertas clases para el arranque. Para el caso práctico basta con incluir en la clase main de la aplicación (RestNativeApplication) una anotación tipo TypeHint indicando el tipo de clase a incluir en AOT.

org.springframework.nativex.hint.TypeHint

@TypeHint(types = oracle.jdbc.driver.OracleDriver.class, typeNames = "oracle.jdbc.driver.OracleDriver")

Conclusiones

Con el artículo se ha tenido la oportunidad de conocer de manera general el proyecto de Spring Native, en qué consiste y cual son sus principales características. También se pudo conocer de primera mano cómo sería el proceso de generación de ejecutables nativas a partir de una aplicación Spring Boot.

Se puede decir que, aunque el proyecto Spring Native no ha sido liberado de manera oficial (esperemos que sea pronto con la Release de Spring Boot 3 y Spring FW 6), pero el trabajo realizado hasta el momento permite la incorporación de atributos en la mayoría de las aplicaciones basadas en Spring Boot de la actualidad que permitirán la generación de artefactos nativos y todas las ventajas que esto aporta a las arquitecturas basadas en microservicios.

Estaremos atentos a las últimas novedades sobre este proyecto y por supuesto esperamos que la versión final brinde un mayor rango de integraciones en Spring Native y que esta versión genere el menor impacto en incorporar este tipo de configuraciones en las aplicaciones Spring Boot que tengan como objetivo la construcción de artefactos nativos.

Código fuente del proyecto Github.

Referencias

Spring Native Documentación
Cloud Serverless
JRebel Productivity Report

Autor

Omar N. Muñoz Mejía

Arquitecto de soluciones en knowmad mood. Realmente me apasiona aportar soluciones simples a problemas complejos y saber el gran impacto que mi trabajo puede generar en muchas personas. #techlover.