Automatizando el Registro de Ejecutores y Tareas en XXL-JOB mediante Modificaciones Personalizadas

XXL-JOB es un middleware de programación de tareas ampliamente utilizado por su ligereza, simplicidad y soporte distribuido, que resuelve numerosos problemas de programación en proyectos. Sin embargo, el proceso de configurar manualmente ejecutores y tareas en su centro de administración puede volverse tedioso cuando hay muchas tareas, requiriendo repetir pasos como enlazar jobHandler y definir expresiones cron.

Para optimizar este flujo, exploré un enfoque que permite el registro automático de ejecutores y tareas al iniciar el proyecto, eliminando la necesidad de configuración manual en la interfaz web. La idea principal es registrar proactivamente componentes al centro de programación durante el arranque de la aplicación.

Análisis del Enfoque

El objetivo es registrar automáticamente el ejecutor y cada jobHandler al centro de administración de XXL-JOB. Esto implica invocar API específicas en el módulo xxl-job-admin durante la inicialización. Las API relevantes incluyen endpoints para listar y guardar ejecutores, así como para listar y agregar tareas. Sin embargo, estas API están protegidas por autenticación, lo que requiere manejar el inicio de sesión y el almacenamiento de cookies.

El proceso implica los siguientes pasos clave:

  • Iniciar sesión en el centro de administración para obtener una cookie de autenticación.
  • Verificar y registrar el ejecutor basado en configuraciones como appname y title.
  • Identificar métodos anotados con @XxlJob y un nuevo marcador personalizado para registrar tareas automáticamente.

Implementación del Starter

Se desarrolló un starter de Spring Boot que, al ser añadido como dependencia, habilita el registro automático. Las dependencias esenciales incluyen:


<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.3.0</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

Servicio de Autenticación

Para interactuar con las API de administración, se crea un servicio que gestiona la autenticación. Almacena la cookie obtenida al iniciar sesión y la reutiliza en solicitudes subsecuentes.


// Clase modificada: AutenticacionServicio
private final Map<String, String> credencialesCache = new HashMap<>();

public void iniciarSesion() {
    String endpoint = direccionAdmin + "/login";
    HttpResponse respuesta = HttpRequest.post(endpoint)
            .form("usuario", nombreUsuario)
            .form("clave", contrasena)
            .execute();
    List<HttpCookie> cookies = respuesta.getCookies();
    Optional<HttpCookie> cookieEncontrada = cookies.stream()
            .filter(c -> c.getName().equals("XXL_JOB_LOGIN_IDENTITY"))
            .findFirst();
    if (!cookieEncontrada.isPresent()) {
        throw new RuntimeException("Fallo al obtener la cookie de autenticación");
    }
    String valorCookie = cookieEncontrada.get().getValue();
    credencialesCache.put("XXL_JOB_LOGIN_IDENTITY", valorCookie);
}

public String obtenerCookie() {
    for (int intento = 0; intento < 3; intento++) {
        String valor = credencialesCache.get("XXL_JOB_LOGIN_IDENTITY");
        if (valor != null) {
            return "XXL_JOB_LOGIN_IDENTITY=" + valor;
        }
        iniciarSesion();
    }
    throw new RuntimeException("No se pudo recuperar la cookie de autenticación");
}

Servicio de Ejecutores

Este servicio permite consultar y registrar ejecutores en el centro de administración. Realiza búsquedas precisas para evitar duplicados.


// Clase modificada: EjecutorAdministrador
public List<GrupoTrabajo> listarEjecutores() {
    String url = direccionAdmin + "/jobgroup/pageList";
    HttpResponse respuesta = HttpRequest.post(url)
            .form("nombreAplicacion", nombreApp)
            .form("titulo", tituloEjecutor)
            .cookie(autenticacionServicio.obtenerCookie())
            .execute();
    String cuerpo = respuesta.body();
    JSONArray arreglo = JSONUtil.parse(cuerpo).getByPath("data", JSONArray.class);
    return arreglo.stream()
            .map(obj -> JSONUtil.toBean((JSONObject) obj, GrupoTrabajo.class))
            .collect(Collectors.toList());
}

public boolean verificarExistenciaEjecutor() {
    List<GrupoTrabajo> ejecutores = listarEjecutores();
    return ejecutores.stream()
            .anyMatch(e -> e.getAppname().equals(nombreApp) && e.getTitle().equals(tituloEjecutor));
}

public boolean registrarNuevoEjecutor() {
    String url = direccionAdmin + "/jobgroup/save";
    HttpResponse respuesta = HttpRequest.post(url)
            .form("nombreAplicacion", nombreApp)
            .form("titulo", tituloEjecutor)
            .cookie(autenticacionServicio.obtenerCookie())
            .execute();
    Object codigo = JSONUtil.parse(respuesta.body()).getByPath("code");
    return codigo.equals(200);
}

Servicio de Tareas

Maneja la consulta y creación de tareas, asociándolas a un ejecutor específico. Utiliza búsquedas por manejador de tareas para evitar registros redundantes.


// Clase modificada: TareaOperador
public List<InfoTarea> buscarTareas(Integer idEjecutor, String manejadorTarea) {
    String url = direccionAdmin + "/jobinfo/pageList";
    HttpResponse respuesta = HttpRequest.post(url)
            .form("grupoTrabajo", idEjecutor)
            .form("manejadorEjecutor", manejadorTarea)
            .form("estadoActivacion", -1)
            .cookie(autenticacionServicio.obtenerCookie())
            .execute();
    String cuerpo = respuesta.body();
    JSONArray arreglo = JSONUtil.parse(cuerpo).getByPath("data", JSONArray.class);
    return arreglo.stream()
            .map(obj -> JSONUtil.toBean((JSONObject) obj, InfoTarea.class))
            .collect(Collectors.toList());
}

public Integer crearTarea(InfoTarea tarea) {
    String url = direccionAdmin + "/jobinfo/add";
    Map<String, Object> parametros = BeanUtil.beanToMap(tarea);
    HttpResponse respuesta = HttpRequest.post(url)
            .form(parametros)
            .cookie(autenticacionServicio.obtenerCookie())
            .execute();
    JSON json = JSONUtil.parse(respuesta.body());
    Object codigo = json.getByPath("code");
    if (codigo.equals(200)) {
        return Convert.toInt(json.getByPath("content"));
    }
    throw new RuntimeException("Error al registrar la tarea");
}

Definición de Anotaciones Personalizadas

Se creó una anotación @RegistroTarea para acompañar a @XxlJob, permitiendo especificar parámetros como expresión cron, descripción y estado de activación.


// Anotación modificada: @RegistroTarea
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RegistroTarea {
    String expresionCron();
    String descripcion() default "Tarea por defecto";
    String responsable() default "Responsable no especificado";
    int estadoActivacion() default 0;
}

Lógica de Auto-registro

El componente principal escucha el evento de inicio de la aplicación para ejecutar el registro automático. Escanea beans de Spring, identifica métodos con las anotaciones relevantes y determina si ya existen en el centro de administración antes de crearlos.


// Clase modificada: AutoRegistradorComponente
@Component
public class AutoRegistradorComponente implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
    private ApplicationContext contextoAplicacion;
    @Autowired
    private EjecutorAdministrador ejecutorAdministrador;
    @Autowired
    private TareaOperador tareaOperador;

    @Override
    public void setApplicationContext(ApplicationContext contexto) throws BeansException {
        this.contextoAplicacion = contexto;
    }

    @Override
    public void onApplicationEvent(ApplicationReadyEvent evento) {
        registrarEjecutor();
        registrarTareas();
    }

    private void registrarEjecutor() {
        if (!ejecutorAdministrador.verificarExistenciaEjecutor()) {
            if (ejecutorAdministrador.registrarNuevoEjecutor()) {
                System.out.println("Ejecutor registrado exitosamente");
            }
        }
    }

    private void registrarTareas() {
        List<GrupoTrabajo> grupos = ejecutorAdministrador.listarEjecutores();
        if (grupos.isEmpty()) return;
        GrupoTrabajo grupoActual = grupos.get(0);

        String[] nombresBeans = contextoAplicacion.getBeanNamesForType(Object.class, false, true);
        for (String nombreBean : nombresBeans) {
            Object bean = contextoAplicacion.getBean(nombreBean);
            Map<Method, XxlJob> metodosAnotados = MethodIntrospector.selectMethods(bean.getClass(),
                    method -> AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class));

            for (Map.Entry<Method, XxlJob> entrada : metodosAnotados.entrySet()) {
                Method metodo = entrada.getKey();
                XxlJob anotacionJob = entrada.getValue();
                if (metodo.isAnnotationPresent(RegistroTarea.class)) {
                    RegistroTarea registro = metodo.getAnnotation(RegistroTarea.class);
                    List<InfoTarea> tareasExistentes = tareaOperador.buscarTareas(grupoActual.getId(), anotacionJob.value());
                    boolean yaRegistrada = tareasExistentes.stream()
                            .anyMatch(t -> t.getExecutorHandler().equals(anotacionJob.value()));
                    if (!yaRegistrada) {
                        InfoTarea nuevaTarea = new InfoTarea();
                        nuevaTarea.setJobGroup(grupoActual.getId());
                        nuevaTarea.setExecutorHandler(anotacionJob.value());
                        nuevaTarea.setJobCron(registro.expresionCron());
                        nuevaTarea.setJobDesc(registro.descripcion());
                        nuevaTarea.setAuthor(registro.responsable());
                        nuevaTada.setTriggerStatus(registro.estadoActivacion());
                        tareaOperador.crearTarea(nuevaTarea);
                    }
                }
            }
        }
    }
}

Configuración y Autoensamblaje

Se definieron clases de configuración y archivos de metadatos para la auto-configuración en Spring Boot.


// Clase de configuración: ConfiguracionAutoRegistro
@Configuration
@ComponentScan(basePackages = "com.ejemplo.autoregistro")
public class ConfiguracionAutoRegistro {
}

Archivo META-INF/spring.factories:


org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.ejemplo.autoregistro.config.ConfiguracionAutoRegistro

Prueba de Integración

Para validar el enfoque, se creó un proyecto Spring Boot que incluye el starter. Las configuraciones en application.properties abarcan tanto parámetros nativos de XXL-JOB como los nuevos campos requeridos para el auto-registro.


# Propiedades de configuración modificadas
xxl.job.admin.direccion=http://127.0.0.1:8080/xxl-job-admin
xxl.job.acceso.token=token_predeterminado
xxl.job.app.executor.nombre=executor-prueba
xxl.job.app.executor.direccion=
xxl.job.app.executor.ip=127.0.0.1
xxl.job.app.executor.puerto=9999
xxl.job.app.executor.rutaLogs=/data/logs/xxl-job
xxl.job.app.executor.diasRetencion=30
# Nuevos campos para auto-registro
xxl.job.admin.usuario=admin
xxl.job.admin.contrasena=123456
xxl.job.executor.titulo=titulo-prueba

Se definieron métodos de tarea con las anotaciones correspondientes:


// Métodos de prueba modificados
@XxlJob(value = "tareaDiaria")
@RegistroTarea(expresionCron = "0 0 0 * * ?",
        responsable = "equipo-desarrollo",
        descripcion = "Tarea de ejecución diaria")
public void tareaDiaria() {
    System.out.println("Ejecutando tarea diaria");
}

@XxlJob(value = "tareaPeriodica")
@RegistroTarea(expresionCron = "0 0 * * * ?",
        estadoActivacion = 1)
public void tareaPeriodica() {
    System.out.println("Ejecutando tarea periódica");
}

Al iniciar la aplicación, se observa que el ejecutor y las tareas se registran automáticamente en el centro de administración de XXL-JOB, verificable en la interfaz web. Las pruebas de ejecución manual desde la interfaz confirman el correcto funcionamiento.

Etiquetas: xxl-job spring-boot autoconfiguration tareas-programadas java

Publicado el 6-6 17:50