Aspectos Clave en el Desarrollo con Spring Boot

Este artículo explora diversas consideraciones y soluciones en el desarrollo de aplicaciones con Spring Boot, abarcando desde la configuración de MyBatis hasta la gestión de concurrencia y la medición del rendimiento.

MyBatis y el uso de @Param()

La anotación @Param en MyBatis es esencial para mapear correctamente los argumentos de un método de interfaz DAO (Mapper) a las sentencias SQL en los archivos XML correspondientes. Su uso es mandatorio en las siguientes situaciones:

  • Múltiples Parámetros: Cuando un método de su interfaz DAO acepta más de un argumento, @Param es necesario para asignar un nombre explícito a cada uno.
  • Alias de Parámetros: Permite asignar un nombre más descriptivo a un parámetro, incluso si es el único.
  • Uso de ${variable} en SQL: Si la sentencia SQL en el XML utiliza la sintaxis ${variable} (para incrustar directamente valores como nombres de columnas o tablas), es imprescindible usar @Param en el método Java. Es vital recordar que esta práctica presenta un riesgo significativo de inyección SQL y debe emplearse con extrema cautela y solo con entradas validadas.
// Interfaz del Repositorio (Mapper)
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;

@Mapper
public interface ProductCatalogMapper {
    /**
     * Recupera una lista de productos ordenados por un campo específico.
     * @param sortField El nombre de la columna por la cual ordenar los resultados.
     * @return Una lista de objetos Product.
     */
    List<Product> retrieveProductsOrderedBy(@Param("sortField") String sortField);
}

<!-- Archivo XML de MyBatis -->
<select id="retrieveProductsOrderedBy" resultType="com.example.app.model.Product">
    SELECT product_id, name, price FROM products ORDER BY ${sortField} ASC
</select>

  • Parámetros en SQL Dinámico: En construcciones SQL dinámicas de MyBatis (como <if>, <where>), si un parámetro se utiliza como variable, @Param es requerido, incluso para un solo argumento.
<!-- Consulta con SQL dinámico para buscar detalles de un elemento -->
<select id="queryItemDetails" resultType="java.util.Map">
    SELECT item_code, item_name, description
    FROM inventory_items
    <where>
        <if test="itemCodeParam != null and itemCodeParam.length() > 0">
            AND item_code = #{itemCodeParam}
        </if>
    </where>
    ORDER BY last_modified DESC
</select>

Para la sentencia SQL dinámica anterior, la interfaz del repositorio debería definir el método de la siguiente manera:

// Interfaz del Repositorio (Mapper)
import org.apache.ibatis.annotations.Param;
import java.util.Map;

public interface InventoryItemMapper {
    Map<String, Object> queryItemDetails(@Param("itemCodeParam") String itemCodeParam);
}

Resolución de Errores Comunes

Uno de los errores más frecuentes en MyBatis es el mensaje: There is no getter for property named ‘id’ in ‘class java.lang.String’. Este problema surge cuando se pasa un tipo primitivo o String a un método MyBatis que lo espera como una propiedad de un objeto (un JavaBean).

Solución: La solución es simple: añada la anotación @Param al argumento del método en la interfaz DAO, proporcionándole un nombre explícito.

// Interfaz DAO corregida
import org.apache.ibatis.annotations.Param;

public interface CustomerRepository {
    Customer findCustomerRecordById(@Param("customerId") String customerId);
}

Otro inconveniente habitual al realizar pruebas unitarias en proyectos Spring Boot es la imposibilidad de ejecutar un método anotado con @Test (a menudo percibido como la ausencia de un método run).

Solución: Asegúrese de que la anotación @Test se importe de la API de JUnit 5 (JUnit Jupiter). La importación correcta es import org.junit.jupiter.api.Test; y no import org.junit.Test;, que corresponde a JUnit 4.

// Importación correcta para JUnit 5
import org.junit.jupiter.api.Test;

public class DataServiceTests {
    @Test // Esta es la anotación correcta para JUnit 5
    void shouldProcessDataSuccessfully() {
        // ... lógica de prueba
    }
}

Clases de Entidad Java: DO, DTO, BO, VO

En el ámbito del desarrollo Java, especialmente con frameworks como Spring Boot, es fundamental diferenciar entre los diversos tipos de objetos de datos para mantener una arquitectura limpia y modular. La guía de desarrollo de Alibaba para Java establece una clara distinción:

  • DO (Data Object): Un objeto que se corresponde directamente con la estructura de una tabla en la base de datos. Es utilizado por la capa DAO para la persistencia de datos.
  • DTO (Data Transfer Object): Objeto diseñado para la transferencia de datos entre las distintas capas de la aplicación (Service, Manager, Controller). Contiene únicamente los datos necesarios para una operación específica, desacoplando la lógica de negocio de su representación.
  • BO (Business Object): Un objeto que encapsula la lógica de negocio. Puede ser el resultado de operaciones complejas en la capa de servicio y a menudo combina datos de múltiples DOs para representar una entidad de negocio completa.
  • Query (Objeto de Consulta): Objeto dedicado a encapsular parámetros de consulta. Es una buena práctica utilizar un objeto Query cuando una consulta requiere más de dos parámetros, en lugar de pasar un Map.
  • VO (View Object): Objeto de vista, cuyo propósito es presentar datos directamente a la interfaz de usuario (por ejemplo, para renderizar una plantilla HTML en una aplicación web). Contiane los datos ya formateados para su visualización.

Convenciones de Nomenclatura Recomendadas:

  • Objetos de Datos: XxxDO (donde Xxx es el nombre de la tabla).
  • Objetos de Transferencia de Datos: XxxDTO (donde Xxx representa el dominio de negocio).
  • Objetos de Vista: XxxVO (donde Xxx suele indicar el nombre de la página o componente de la interfaz de usuario).
  • Nota Importante: El término POJO es una sigla genérica que abarca todos estos tipos; se debe evitar nombrar clases como XxxPOJO.

Manejo de Información de Autenticación en Hilos Asíncronos

En aplicaciones Spring Boot que utilizan Spring Security con operaciones asíncronas, el contexto de seguridad (donde se almacena la información del usuario autenticado) no se propaga automáticamente a los nuevos hilos. Esto puede llevar a la pérdida de datos de autenticación en operaciones fuera del hilo principal. A continuación, se presentan dos enfoques para gestionar esta situación:

1. Propagación Manual del Contexto del Hilo Principal

Consiste en capturar el contexto de seguridad del hilo principal y establecerlo explícitamente en el hilo secundario antes de que este último ejecute su lógica.

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.concurrent.CompletableFuture;

public class AuthServicePropagation {

    public void executeAsyncOperationWithUserContext() {
        // Capturar el contexto de autenticación del hilo actual (principal)
        Authentication mainThreadAuth = SecurityContextHolder.getContext().getAuthentication();

        CompletableFuture.runAsync(() -> {
            // Establecer el contexto de autenticación en el hilo asíncrono
            SecurityContextHolder.getContext().setAuthentication(mainThreadAuth);
            try {
                String authenticatedUsername = SecurityContextHolder.getContext().getAuthentication().getName();
                System.out.println("Hilo asíncrono: Usuario autenticado = " + authenticatedUsername);
                // Aquí va la lógica de negocio asíncrona que requiere el contexto del usuario
            } finally {
                // Opcionalmente, limpiar el contexto para evitar fugas de información
                // SecurityContextHolder.clearContext();
            }
        });
    }
}

2. Configuración de la Estrategia de Contexto de Seguridad

Spring Security permite configurar una estrategia de almacenamiento del contexto que automáticamente hereda la información de autenticación a los hilos hijos. Esto se logra estableciendo la estrategia MODE_INHERITABLETHREADLOCAL al inicio de la aplicación.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.context.SecurityContextHolder;
import jakarta.annotation.PostConstruct; // Usar javax.annotation.PostConstruct para versiones anteriores de Java/Spring Boot

@Configuration
public class SecurityContextStrategyConfig {

    @PostConstruct
    public void configureSecurityContextPropagation() {
        // Configura Spring Security para que el contexto de autenticación
        // se propague automáticamente a los hilos hijos.
        SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
        System.out.println("Estrategia de propagación del contexto de seguridad configurada: MODE_INHERITABLETHREADLOCAL.");
    }
}

Medición del Tiempo de Ejecución de Código en Java

Evaluar el rendimiento de diferentes secciones del código es fundamental para la optimización. Java ofrece varias herramientas para medir el tiempo de ejecución.

1. Usando System.currentTimeMillis()

Este método devuelve la hora actual en milisegundos desde la época Unix (1 de enero de 1970 UTC). Es adecuado para medir duraciones de tiempo "reales".

public class ExecutionTimeLogger {
    public static void main(String[] args) {
        long startTimeMillis = System.currentTimeMillis();

        // Simulación de una operación de computación
        performComplexCalculation();

        long endTimeMillis = System.currentTimeMillis();
        long totalDuration = endTimeMillis - startTimeMillis;

        System.out.printf("Duración total de ejecución: %d ms.%n", totalDuration);
    }

    private static void performComplexCalculation() {
        long partialSum = 0;
        for (int i = 0; i < 8_000_000; i++) {
            partialSum += (i * 2 - i / 3); // Simula carga de trabajo
        }
        // Para asegurar que la operación no sea optimizada por el compilador si el resultado no se usa
        System.out.println("Resultado intermedio: " + partialSum);
    }
}

2. Usando System.nanoTime()

Proporciona una fuente de tiempo de alta resolución para medir duraciones. No está sincronizado con la hora del sistema, sino que es un contador arbitrario que debe usarse exclusivamente para medir intervalos de tiempo, no para la hora del día.

public class NanoTimerExample {
    public static void main(String[] args) {
        long startNanos = System.nanoTime();

        // Simulación de una operación
        processDataBlocks();

        long endNanos = System.nanoTime();
        long durationNanos = endNanos - startNanos;

        System.out.printf("Duración de la operación: %d ns.%n", durationNanos);
    }

    private static void processDataBlocks() {
        double accumulatedValue = 0;
        for (int i = 0; i < 8_000_000; i++) {
            accumulatedValue += Math.log(i + 1); // Simula cálculos flotantes
        }
        System.out.println("Valor acumulado: " + accumulatedValue);
    }
}

3. Utilizando StopWatch de Spring Framework

La clase StopWatch de Spring (parte de spring-core) es una herramienta conveniente para medir el tiempo de ejecución, especialmente útil cuando se necesita cronometrar múltiples lapsos o tareas dentro de una única operación.

import org.springframework.util.StopWatch;

public class SpringStopwatchUsage {
    public static void main(String[] args) {
        StopWatch operationTimer = new StopWatch("Batch Processing Monitor");

        operationTimer.start("Data Initialization");
        initializeSystemData();
        operationTimer.stop();

        operationTimer.start("Core Processing Logic");
        executeCoreProcessing();
        operationTimer.stop();

        operationTimer.start("Result Finalization");
        finalizeResults();
        operationTimer.stop();

        System.out.println(operationTimer.prettyPrint()); // Muestra un resumen formateado
        System.out.printf("Tiempo total de ejecución del batch: %.2f segundos.%n", operationTimer.getTotalTimeSeconds());
    }

    private static void initializeSystemData() {
        // Simula la inicialización de datos
        try { Thread.sleep(250); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }

    private static void executeCoreProcessing() {
        // Simula la lógica de procesamiento principal
        long sumOfSquares = 0;
        for (int i = 0; i < 1_000_000; i++) {
            sumOfSquares += (long)i * i;
        }
        System.out.println("Suma de cuadrados: " + sumOfSquares);
    }

    private static void finalizeResults() {
        // Simula la finalización de resultados
        try { Thread.sleep(150); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

4. Utilizando StopWatch de Apache Commons Lang

Similar a la versión de Spring, la clase StopWatch de Apache Commons Lang 3 (necesita la dependencia commons-lang3) proporciona una funcionalidad robusta para la medición de tiempo.

import org.apache.commons.lang3.time.StopWatch;
import java.util.concurrent.TimeUnit; // Importar para especificar unidades de tiempo

public class ApacheCommonsStopwatchExample {
    public static void main(String[] args) {
        StopWatch timingTool = new StopWatch();

        timingTool.start();
        performSimpleFileRead(); // Simula la lectura de un archivo
        timingTool.stop();

        System.out.printf("Lectura de archivo completada en: %d milisegundos.%n", timingTool.getTime(TimeUnit.MILLISECONDS));
        System.out.printf("Lectura de archivo completada en: %d segundos.%n", timingTool.getTime(TimeUnit.SECONDS));

        timingTool.reset(); // Reinicia el cronómetro para una nueva medición
        timingTool.start();
        executeDatabaseQuery(); // Simula una consulta a base de datos
        timingTool.stop();

        System.out.printf("Consulta a base de datos completada en: %d nanosegundos.%n", timingTool.getNanoTime());
    }

    private static void performSimpleFileRead() {
        // Simula la lectura de un archivo grande
        StringBuilder buffer = new StringBuilder();
        for (int i = 0; i < 500_000; i++) {
            buffer.append("line content "); // Operación de cadena para simular tiempo
        }
    }

    private static void executeDatabaseQuery() {
        // Simula una operación de base de datos
        try { Thread.sleep(75); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

Etiquetas: SpringBoot MyBatis junit5 SpringSecurity DTO

Publicado el 6-22 03:23