Enmilocalfunciona

Thoughts, stories and ideas.

Spring Native [Spring Boot 3.0]-Evolución Caso Práctico

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

Arquitectura de SolucionesSpring BootSpring NativeHints

El desarrollo de aplicaciones Java ha empezado a prestarle realmente atención la obtención de un alto rendimiento con los recursos disponibles. En Java, existen opciones como Quarkus y Micronaut para lograr conseguir esa mayor eficiencia. Existe una tercera opción llamada Spring Native, que permite construir aplicaciones nativas de manera mínimamente disruptiva utilizando el conocido framework Spring Boot.

Spring Native, proyecto que dejó de ser experimental y en su versión oficial, liberado de la mano de Spring Boot 3 y Spring Framework 6, utiliza el compilador GraalVM para compilar aplicaciones Spring y generar ejecutables nativos con mejores tiempos de arranque, menor consumo de memoria y mejor rendimiento, especialmente beneficioso para arquitecturas de microservicios.


El artículo del día de hoy es una evolución del caso práctico visto en una entrada anterior, en el que todavía Spring Native estaba en fase de pruebas, de tal forma que ahora se construirá una versión incorporando la versión oficial productiva de Spring Native liberada con Spring Boot 3 de modo que podamos adaptar las aplicaciones con las nuevas configuraciones ya que no serán iguales a las que se tenían en fases experimentales.

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.

Tips

Usuarios Windows

Para conseguir un proceso de construcción más fácil, en cuanto a tiempo y consumo de recursos, aconsejo utilizar un sistema operativo Linux. Ayudaremos a conseguir esto con Windows Subsystem for Linux (WSL): Guía para instalación.

Todos
Una vez disponible el OS Linux, mi recomendación es Ubuntu (una de las distribuciones de Linux más populares y ampliamente utilizadas), se procede a instalar Docker para este OS.

Opcional: Executing the Docker Command Without Sudo

Otra herramienta que recomiendo para terminar de conseguir entornos de desarrollos más organizados y multifuncional es utilizar SDKMAN, herramienta para administrar en paralelo versiones de Kits de Desarrollo de Software en la mayoría de los sistemas basados en Unix, y que permitirá la instalación y utilización de GraalVM de forma más rápida.

Instalación SDKMAN

sudo apt-get update
sudo apt-get install curl 
sudo apt-get install zip unzip
curl -s "https://get.sdkman.io" | bash

Instalación GraalVM

sdk install java 22.3.r17-nik
sdk use java 22.3.r17-nik

Instrucciones instalar GraalVM para Windows aquí.

Verificar Instalaciones

Visualizar versión Java

java -version  

Output:

openjdk version "17.0.5" 2022-10-18 LTS
OpenJDK Runtime Environment GraalVM 22.3.0 (build 17.0.5+8-LTS)
OpenJDK 64-Bit Server VM GraalVM 22.3.0 (build 17.0.5+8-LTS, mixed mode, sharing)

Lista de los componentes instalados en GraalVM

gu list

Output:

ComponentId              Version             Component name                Stability                     
----------------------------------------------------------------------------------------------------------
graalvm                  22.3.0              GraalVM Core                  Supported
native-image             22.3.0              Native Image                  Early adopter

Generación Applicación Base

Inicialización

La manera más fácil de empezar con la construcción de la aplicación es utilizar Spring Initilizr, abrir la página y se escogen las siguientes opciones:

Spring Initilizr
Paso Opción Descripción
1 Tipo Proyecto Maven
2 Lenguaje Java
3 Versión Spring Boot 3.0.6
4 Metadata del proyecto Editar los valores para ajustarlo al ejemplo
5 Versión Java 17
6 Dependencias Seleccionar Native Support y Spring Web
7 Generar Crear proyecto con las especificaciones seleccionadas

Código Base


Agregar el sub-paquete controller al paquete base de la aplicación y crear dentro la clase SignalController.

@RestController
@RequestMapping("/services/v1")
public class SignalController {
	
	@GetMapping("/signals")
	public String getSignals() {
		return "Hello from enmilocal";

	}
}

Modificar la extensión del archivo application.properties a application.yaml. Luego agregar las siguientes configuraciones:

server:
  port: 7080
  
spring:
  devtools:
    restart:
      enabled: 'true'


En este punto, se compila e inicia la aplicación a través de comando MVN o del IDE de desarrollo para probar que la aplicación responda con el controlador básico creado.

Ejecutar con Spring Boot

mvn spring-boot:run

Ejecutar con Maven

mvn clean package
java -jar ./target/<ARTEFACT_NAME>.jar

Reemplazar ARTEFACT_NAME con el asignado en el POM de la aplicación.

Probar aplicación

curl http://localhost:7080/services/v1/signals

Output:

Hello from enmilocal

Creación y ejecución artefacto nativo

Para la creación del ejecutable nativo utilizaremos 2 aproximaciones, la primera utilizando Docker y la segunda utilizando directamente GraalVM.

Ambos procesos toman un tiempo significativo ya que la construcción requiere una cantidad considerable de recursos y tiempo.


Creación imagen Docker con ejecutable nativo

mvn -Pnative spring-boot:build-image -Dspring-boot.build-image.imageName=onoriel/demo-native:latest

Ejecución:

docker run --rm -p 7080:7080 onoriel/workshop-native

Creación ejecutable nativo con GraalVM

mvn -Pnative native:compile

Ejecución:

./target/demo-native

El nombre final del artefacto corresponde al asignado en el POM de la aplicación.

Agregar Native Swagger UI

Para facilitar las pruebas se utilizará Swagger UI para contar con un interfaz gráfica para interactuar de una forma más amigable con el API que define la aplicación.


Agregar la propiedad con la versión swagger al POM de la aplicación. En el tag de agregar uno nuevo con esta propiedad:

<springdoc-openapi.version>2.0.2</springdoc-openapi.version>

Agregar las dependencias necesarias para incluir la funcionalidad al POM de la aplicación. En el tag de agregar las siguientes dependencias:

<!-- SWAGGER -->
<dependency>
      <groupId>org.springdoc</groupId>
      <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
      <version>${springdoc-openapi.version}</version>
</dependency>
<!-- Spring Validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Crear el archivo de configuración para especificar las propiedades del API de la aplicación. Para ello, agregar el sub-paquete config al paquete base de la aplicación y crear dentro la clase AppSwaggerConfig.

@Configuration
public class AppSwaggerConfig {
	@Bean
	  public GroupedOpenApi publicApi() {
	      return GroupedOpenApi.builder()
	    		  .displayName("Signals")
	              .group("io.enmilocalfunciona.native")
	              .pathsToMatch("/services/**")
	              .build();
	  }
	 @Bean
	  public OpenAPI springShopOpenAPI() {
		 Contact contact = new Contact();
		 contact.setEmail("info@onoriel.es");
		 contact.setName("ONORIEL");
		 contact.setUrl("https://www.onoriel.com");
		 
	      return new OpenAPI()
	              .info(new Info().title("Native REST Api Application - Signals")
	              .description("Documentation REST API")
	              .version("Version 1.0.0")
	              .contact(contact)
	              .license(new License().name("License of Api for General use")))
	              .externalDocs(new ExternalDocumentation()
	              .description("Terms of service base into company terms of use"));
	  }
}

Agregar las propiedades relacionadas con Native Swagger UI modificando en el archivo application.yaml de la aplicación y agregando:

springdoc:
  enable-native-support: 'true'
  swagger-ui:
    path: /swagger-ui

Probar cambios

Para la construcción y ejecución del artefacto nativo repetimos el proceso documentado anteriormente.

Una vez iniciada la aplicación bastará con abrir un navegador, navegar hacia el siguiente enlace e interactuar con el API:

http://localhost:7080/swagger-ui

Agregar Persistencia H2

Agregar las dependencias necesarias para incluir la persistencia en memoria H2 al POM de la aplicación. En el tag de agregar las siguientes dependencias:

<!-- H2 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
</dependency>

Agregar las propiedades relacionadas con H2 modificando en el archivo application.yaml de la aplicación y agregando:

spring:
  h2:
    console.enabled: 'true'
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb
    username: sa
    password: 

Crear la clase modelo de la aplicación para transportar la información de las entidades. Para ello, agregar el sub-paquete model al paquete base de la aplicación y crear dentro la clase Signal.

public record Signal (String name){
}

Crear la clase repostorio de la aplicación para gobernar la información de las entidades. Para ello, agregar el sub-paquete repository al paquete base de la aplicación y crear dentro la clase SignalRepository.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import com.atsistemas.model.Signal;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

@Repository
public class SignalRepository {
    @Autowired
    JdbcTemplate jdbcTemplate;

    static class SignalRowMapper implements RowMapper<Signal> {
        @Override
        public Signal mapRow(ResultSet rs, int rowNum) throws SQLException {
            Signal signal = new Signal(rs.getString("name"));
            return signal;
        }
    }
    public List<Signal> findAll() {
        return jdbcTemplate.query("SELECT * FROM SIGNALS", new SignalRowMapper());
    }

}

Modificar el controlador de la aplicación para que pueda utilizar el objeto repository:

// Agregar    
@Autowired
SignalRepository repository;
//modificar
@GetMapping("/signals")
public List<Signal> getSignals() {
    return repository.findAll();

}    

Agregar un nuevo recurso sobre la ruta <src/main/resources> con nombre schema.sql con el siguiente contenido:

CREATE TABLE IF NOT EXISTS SIGNALS (
                      id         INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
                      name VARCHAR(30)
);   

Agregar un nuevo recurso sobre la ruta <src/main/resources> con nombre data.sql con el siguiente contenido:

delete from SIGNALS; 
insert into SIGNALS values(1,'E1234567');
insert into SIGNALS values(2,'A1234568');

Probar cambios

Para la construcción y ejecución del artefacto nativo repetimos el proceso documentado anteriormente.

Una vez iniciada la aplicación bastará con abrir un navegador, navegar hacia el siguiente enlace e interactuar con el API:

http://localhost:7080/swagger-ui

Agregar Persistencia Oracle

Iniciar Servicio Oracle

Bastará con crear un contenedor utilizando una imagen Docker de la versión Oracle XE 11g.

docker run -d -p 8161:1521 -p 8080:8080 oracleinanutshell/oracle-xe-11g

URL [localhost:8161/xe] | Credenciales (system/oracle)

Ahora, conectar con la base de datos (por ejemplo, utilizando Oracle SQL Developer ) y crear la tabla de Signals:

CREATE TABLE SIGNALS 
(
  ID NUMBER 
, NAME VARCHAR2(20 BYTE) 
) ;

Cambios en la aplicación

Eliminar el archivo schema.sql de la ruta <src/main/resources>.


Agregar la propiedad con la versión de las dependencias Oracle al POM de la aplicación. En el tag de agregar uno nuevo con esta propiedad:

<oracle.ojdbc.version>21.7.0.0</oracle.ojdbc.version>

Agregar las dependencias necesarias para incluir la persistencia Oracle al POM de la aplicación. En el tag de agregar las siguientes dependencias:

<!-- JDBC  -->
<dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc11</artifactId>
    <version>${oracle.ojdbc.version}</version>
</dependency>

Agregar/modificar las propiedades relacionadas con Oracle modificando en el archivo application.yaml de la aplicación y agregando:

spring:
  datasource:
    driver-class-name: oracle.jdbc.driver.OracleDriver
    userName: system
    url: jdbc:oracle:thin:@${spring.datasource.plain-url}
    plain-url: localhost:8161/xe
    password: oracle
  sql:
   init:
    mode: always  

Probar cambios

Para la construcción y ejecución del artefacto nativo repetimos el proceso documentado anteriormente.

En este punto encontramos un error al tratar de ejecutar el artefacto nativo, ya sea a través de la imagen Docker generada o del mismo ejecutable creado por GraalVM, donde nos dice que no puede resolver algunas configuraciones native de reflexión:

AOT Hints

Para resolver el anterior error necesitamos indicarle a GraalVM que incluya algunos valores de configuraciones en el momento de construcción del artefacto que son resueltos por reflexión por la aplicación en tiempo de ejecución. De esta forma el artefacto contará con todas las dependencias necesarias para su correcto funcionamiento y que fueron ignoradas por GraalVM ya que no pudo detectar su dependencia por la configuración por defecto y análisis inicial realizado.

Primeramente, se debe crear una clase hints con las configuraciones ad-hoc necesarias para la aplicación. Para ello, agregar el sub-paquete hints al paquete base de la aplicación y crear dentro la clase WorkshopNativeHints.

import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;

import oracle.jdbc.driver.OracleDriver;

public class WorkshopNativeHints implements RuntimeHintsRegistrar {
	    @Override
	    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
 	    	hints.reflection().registerType(OracleDriver.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS);
	    }	
}

Modificar la clase principal de la aplicación Spring Boot (main class) para importar la nueva definición de WorkshopNativeHints.

//Agregar en las anotaciones de la clase
@ImportRuntimeHints(WorkshopNativeHints.class)

Probar cambios

Para la construcción y ejecución del artefacto nativo repetimos el proceso documentado anteriormente.

Una vez iniciada la aplicación bastará con abrir un navegador, navegar hacia el siguiente enlace e interactuar con el API. Esta vez no debería presentarse ningún problema en su ejecución:

http://localhost:7080/swagger-ui

Conclusiones

Con este nuevo artículo se ha podido desarrollar una guía básica de desarrollo de aplicaciones Spring Boot 3.0 con características nativas y utilizando herramientas del framework (Hints) que permiten la configuración de los artefactos para incluir las dependencias necesarias para su construcción y el correcto funcionamiento en modo nativo. Con esto podemos alinear nuestro conocimiento con las nuevas funcionalidades nativas de Spring Boot 3.0 y su liberación oficial de Spring Native y empezar a construir o migrar aplicaciones Java que saquen el mayor rendimiento optimizando los recursos y tiempos de nuestro ecosistema de aplicaciones.

Código fuente Github.

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.