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
appnameytitle. - Identificar métodos anotados con
@XxlJoby 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.