Programación de Tareas con Quartz: Limpieza de Caché de Imágenes en Qiniu y Redis

Quartz es una librería de código abierto líder en el ámbito de la planificación de tareas (Job Scheduling). Ofrece una potente capacidad para definir y ejecutar trabajos programados, ya sea de forma endependiente o, más comúnmente, integrada con frameworks como Spring. Con Quartz, es posible establecer una o múltiples tareas recurrentes, cada una con su propia configuración de tiempo de ejecución, como ejecutar cada hora, el primer día del mes a las 10 AM, o el último día del mes a las 5 PM.

Pasos para la Configuración de Tareas con Quartz

El proceso para implementar tareas programadas utilizando Quartz, especialmente en conjunto con Spring, generalmente sigue estos pasos:

  1. Creación de un módulo secundario (ej. modulo-planificador).
  2. Modificación del archivo pom.xml para incluir las dependencias necesarias.
  3. Definición de una clase de tarea personalizada (Job), que contendrá la lógica a ejecutar.
  4. Configuración de un archivo XML de Spring (ej. spring-tasks.xml) para registrar el objeto de la tarea, los detalles del trabajo (JobDetail), los disparadores (Triggers) y la fábrica de programadores (SchedulerFactory).
  5. Ajuste del archivo web.xml para asegurar que el contenedor de Spring cargue la configuración de las tareas al inicio.

Ejemplo de Implementación con Spring y Quartz

A continuación, se detalla un caso práctico de integración de Quartz con Spring para la gestión de tareas.

(1) Creación del Módulo Maven y pom.xml

Se inicia creando un subproyecto Maven, por ejemplo, llamado modulo-planificador, con un arquetipo webapp. El archivo pom.xml se configura con las dependencias esenciales para Spring, Quartz, Redis y el SDK de Qiniu Cloud:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>proyecto-principal</artifactId>
        <groupId>com.app.gestion</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>modulo-planificador</artifactId>
    <packaging>war</packaging>

    <name>Módulo de Planificación de Tareas</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.app.gestion</groupId>
            <artifactId>utilidades-comunes</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.20</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.3.20</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.3.20</version>
        </dependency>
        <dependency>
            <groupId>com.qiniu</groupId>
            <artifactId>qiniu-java-sdk</artifactId>
            <version>7.9.0</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <path>/</path>
                    <port>9003</port>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

(2) Definición de una Clase de Tarea (Job)

Se crea una clase Java secnilla que servirá como la tarea que Quartz ejecutará. Por ahora, solo mostrará un mensaje en la consola.

package com.app.gestion.jobs;

/**
 * Clase de tarea programada para demostración.
 * Aquí se definirá la lógica a ejecutar por Quartz.
 */
public class TareaDemostracionLimpieza {
    /**
     * Método que simula una tarea de limpieza de imágenes.
     */
    public void ejecutarLimpiezaTemporal() {
        System.out.println("Ejecutando la tarea de limpieza temporal de imágenes...");
    }
}

(3) Configuración de spring-tasks.xml

Este archivo XML es crucial para la integración de Quartz con Spring. Aquí se registran el bean de la tarea, el JobDetail que lo envuelve, el CronTrigger que define cuándo se ejecutará y el SchedulerFactoryBean que orquesta todo.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- Escaneo automático de componentes para inyección de dependencias (ej. JedisPool) -->
    <context:component-scan base-package="com.app.gestion"/>

    <!-- Declaración del bean de nuestra tarea personalizada -->
    <bean id="tareaLimpiezaImagenes" class="com.app.gestion.jobs.TareaDemostracionLimpieza"/>

    <!-- Configuración del JobDetail: vincula un bean con un método a ejecutar -->
    <bean id="detalleTareaLimpieza" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <property name="targetObject" ref="tareaLimpiezaImagenes"/>
        <property name="targetMethod" value="ejecutarLimpiezaTemporal"/>
        <property name="concurrent" value="false"/> <!-- Evita la ejecución concurrente del mismo trabajo -->
    </bean>

    <!-- Configuración del disparador (Trigger) para la tarea -->
    <bean id="disparadorTarea" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
        <property name="jobDetail" ref="detalleTareaLimpieza"/>
        <property name="cronExpression">
            <!-- Ejemplos de expresiones cron: -->
            <!-- Cada 10 segundos: 0/10 * * * * ? -->
            <!-- Cada 2 minutos: 0 0/2 * * * ? -->
            <!-- Todos los días a las 2 AM: 0 0 2 * * ? -->
            <value>0/10 * * * * ?</value> <!-- Configurado para ejecutarse cada 10 segundos -->
        </property>
    </bean>

    <!-- Configuración de la fábrica del planificador (SchedulerFactory) -->
    <bean id="planificadorGlobal" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="disparadorTarea"/>
            </list>
        </property>
    </bean>
</beans>

(4) Configuración de web.xml

Para asegurar que Spring cargue la configuración de Quartz al iniciar la aplicación web, se añade un ContextLoaderListener en web.xml que apunte a los archivos de configuración de Spring.

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <display-name>Aplicación Web de Tareas Programadas</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:spring-*.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

Al desplegar y ejecutar la aplicación, se observará que el mensaje "Ejecutando la tarea de limpieza temporal de imágenes..." aparece en la consola cada 10 segundos, de acuerdo con la expresión cron configurada.

Expresiones Cron

La expresión 0/10 * * * * ? es un ejemplo de expresión cron, un formato estándar que permite definir patrones de tiempo altamente flexibles para la ejecución de tareas. Consiste en seis o siete campos que representan, de izquierda a derecha: segundos, minutos, horas, día del mes, mes, día de la semana y, opcionalmente, año. Existen diversas herramientas online que facilitan la generación y validación de estas expresiones.

Limpieza Programada de Imágenes Residuales

El proceso de limpieza periódica de imágenes no utilizadas se basa en la gestión de dos conjuntos (sets) en Redis. Cuando una imagen se carga, su nombre se guarda en un primer conjunto (ej. SETMEAL_PIC_RESOURCES). Al asociar una imagen con un registro en la base de datos (ej. un "paquete" o "comida"), su nombre se añade a un segundo conjunto (ej. SETMEAL_PIC_DB_RESOURCES). Las imágenes "basura" o no utilizadas son aquellas presentes en el primer conjunto pero ausentes en el segundo. La tarea programada de Quartz calculará esta diferencia y procederá a eliminar las imágenes correspondientes tanto de Qiniu Cloud como de Redis.

Los pasos para implementar esta limpieza son:

  1. Configurar spring-redis.xml para la conexión con Redis.
  2. Actualizar spring-tasks.xml para modificar la frecuencia de ejecución de la tarea.
  3. Implementar una utilidad para la eliminación de archivos en Qiniu Cloud.
  4. Revisar la clase de tarea programada para incluir la lógica de cálculo de diferencias en Redis y eliminación de imágenes.

(1) Configuración de spring-redis.xml

Se define la configuración del pool de conexiones Jedis para Redis:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Configuración del pool de conexiones Jedis -->
    <bean id="configuracionPoolJedis" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxTotal" value="200"/>
        <property name="minIdle" value="50"/>
        <property name="testOnBorrow" value="true"/>
        <property name="testOnReturn" value="true"/>
    </bean>

    <!-- Definición del JedisPool para la conexión a Redis -->
    <bean id="poolConexionesRedis" class="redis.clients.jedis.JedisPool">
        <constructor-arg name="poolConfig" ref="configuracionPoolJedis"/>
        <constructor-arg name="host" value="localhost"/>
        <constructor-arg name="port" value="6379"/>
    </bean>
</beans>

(2) Ajuste del Disparador en spring-tasks.xml

Para la limpieza real de imágenes, es común que la tarea se ejecute una vez al día, por ejemplo, en las primeras horas de la mañana. Se ajusta la expresión cron en la configuración del disparador (disparadorTarea) dentro de spring-tasks.xml a 0 0 2 * * ?, lo que significa "todos los días a las 2:00 AM".

    <!-- Configuración del disparador (Trigger) para la tarea -->
    <bean id="disparadorTarea" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
        <property name="jobDetail" ref="detalleTareaLimpieza"/>
        <property name="cronExpression">
            <!-- Tarea programada para ejecutarse diariamente a las 2 AM -->
            <value>0 0 2 * * ?</value>
        </property>
    </bean>

(3) Implementación de la Utilidad de Qiniu Cloud

Se crea una clase de utilidad para interactuar con la API de Qiniu Cloud y permitir la elimianción de archivos.

package com.app.gestion.util;

import com.qiniu.common.QiniuException;
import com.qiniu.common.Zone;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.Configuration;
import com.qiniu.util.Auth;

/**
 * Clase de utilidad para la interacción con Qiniu Cloud.
 * Gestiona la eliminación de archivos del almacenamiento de objetos.
 */
public class UtilidadesQiniu {

    // Claves de acceso y nombre del bucket para Qiniu Cloud.
    // En un entorno de producción, estas deberían cargarse de forma segura
    // (ej. variables de entorno, HashiCorp Vault, Spring Cloud Config).
    private static final String CLAVE_ACCESO = "TU_CLAVE_DE_ACCESO_AQUI"; // Sustituir por la clave real
    private static final String CLAVE_SECRETA = "TU_CLAVE_SECRETA_AQUI"; // Sustituir por la clave real
    private static final String NOMBRE_BUCKET = "mi-bucket-imagenes"; // Sustituir por el nombre real del bucket

    /**
     * Elimina un archivo específico del bucket de Qiniu Cloud.
     *
     * @param nombreArchivoEnServidor Nombre del archivo tal como está almacenado en Qiniu.
     */
    public static void eliminarArchivoQiniu(String nombreArchivoEnServidor) {
        // Configura la región del servidor de Qiniu.
        // Zone.zone0() es común para la región de China Este. Ajustar según la configuración del bucket.
        Configuration config = new Configuration(Zone.zone0());
        
        // Autenticación con las credenciales de Qiniu.
        Auth autenticador = Auth.create(CLAVE_ACCESO, CLAVE_SECRETA);
        // Gestor de bucket para realizar operaciones como eliminar archivos.
        BucketManager gestorBucket = new BucketManager(autenticador, config);

        try {
            gestorBucket.delete(NOMBRE_BUCKET, nombreArchivoEnServidor);
            System.out.println("Archivo eliminado de Qiniu: " + nombreArchivoEnServidor);
        } catch (QiniuException excepcionQiniu) {
            System.err.println("Error al intentar eliminar archivo de Qiniu: " + nombreArchivoEnServidor);
            System.err.println("Código de error: " + excepcionQiniu.code());
            if (excepcionQiniu.response != null) {
                System.err.println("Detalles de la respuesta: " + excepcionQiniu.response.toString());
            }
        }
    }
}

(4) Revisión de la Clase de Tarea Programada

La clase de tarea se modifica para incluir la lógica de limpieza. Utilizará JedisPool para conectarse a Redis, calculará la diferencia entre los conjuntos de imágenes y luego, por cada imagen "basura", la eliminará de Qiniu Cloud y de Redis.

package com.app.gestion.jobs;

import com.app.gestion.util.UtilidadesQiniu; // Asegúrate de que el paquete de UtilidadesQiniu sea correcto
import com.app.gestion.constant.RedisConst; // Asume una clase de constantes para las claves de Redis
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.JedisPool;

import java.util.Set;

/**
 * Tarea programada para la limpieza de imágenes residuales en Qiniu y Redis.
 * Esta clase se encarga de identificar y eliminar imágenes que fueron subidas
 * pero que no están asociadas a ningún registro en la base de datos.
 */
public class TareaLimpiezaImagenesProgramada {

    // Inyección de dependencia para el pool de conexiones Jedis (Redis).
    // Asegurarse de que JedisPool esté configurado en un archivo XML de Spring y escaneado por component-scan.
    @Autowired
    private JedisPool poolConexionesJedis;

    /**
     * Método principal de la tarea que ejecuta el proceso de limpieza.
     * Calcula la diferencia entre el conjunto de todas las imágenes subidas y
     * el conjunto de imágenes referenciadas en la base de datos para identificar
     * las imágenes "basura".
     */
    public void ejecutarLimpiezaCompletaDeImagenes() {
        System.out.println("Iniciando la tarea programada de limpieza de imágenes...");
        try (redis.clients.jedis.Jedis jedis = poolConexionesJedis.getResource()) {
            // Calcular la diferencia entre los dos conjuntos de Redis para obtener las imágenes no utilizadas.
            // RedisConst.SETMEAL_PIC_RESOURCES: Todas las imágenes que se han subido.
            // RedisConst.SETMEAL_PIC_DB_RESOURCES: Imágenes que están vinculadas a registros en la base de datos.
            Set<String> imagenesParaEliminar = jedis.sdiff(
                RedisConst.SETMEAL_PIC_RESOURCES, // Nombres de constantes de Redis (SETMEAL_PIC_RESOURCES, SETMEAL_PIC_DB_RESOURCES)
                RedisConst.SETMEAL_PIC_DB_RESOURCES
            );

            if (imagenesParaEliminar == null || imagenesParaEliminar.isEmpty()) {
                System.out.println("No se encontraron imágenes basura para eliminar.");
                return;
            }

            System.out.println("Se encontraron " + imagenesParaEliminar.size() + " imágenes para eliminar.");

            // Iterar sobre las imágenes identificadas como "basura" y proceder a eliminarlas.
            for (String nombreImagen : imagenesParaEliminar) {
                // 1. Eliminar la imagen del servicio de almacenamiento en la nube (Qiniu).
                UtilidadesQiniu.eliminarArchivoQiniu(nombreImagen);
                
                // 2. Eliminar la referencia a la imagen del conjunto de imágenes subidas en Redis.
                jedis.srem(RedisConst.SETMEAL_PIC_RESOURCES, nombreImagen);
                System.out.println("Eliminada referencia en Redis: " + nombreImagen);
            }
            System.out.println("Tarea de limpieza de imágenes completada exitosamente.");
        } catch (Exception e) {
            System.err.println("Error durante la ejecución de la tarea de limpieza de imágenes: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Etiquetas: Quartz Spring Redis Qiniu Cloud java

Publicado el 11-5 14:08