Planifier et monitorer un batch Spring à l’aide de Quartz Job Scheduler

Dans cet article nous allons voir comment implémenter un Batch simple à l’aide de l’environnement Spring grâce à l’un de ses composants: Spring Batch. Puis nous mettrons en place une planification de ce Batch grâce à Quartz Job Scheduler. Enfin nous verrons comment on peut monitorer et piloter notre batch depuis une interface web de type Angular.

Mise en place du Batch

Spring propose une page dédiée pour la génération de projet de type SpringSpring Initializr. Nous allons réaliser le Batch sous Maven en Java 11. Pensez à bien sélectionner Spring Batch dans les dépendances à ajouter au projet.

Dans les grandes lignes, le projet sera initialisé via le fichier pom.xml avec les dépendances suivantes: 

  • spring-boot-starter-parent: Évite de gérer la version des dépendances en héritant des valeurs par défaut du parent.
  • spring-boot-starter-batch: Importe les dépendances liées à Spring Boot et Spring Batch.
  • spring-boot-starter-test: Importe les dépendances liées aux tests dont notamment JUnit et Mockito.
  • spring-batch-test: Cette bibliothèque contient des classes auxiliaires qui aideront à tester le Job du Batch.

Configuration du Batch

Le Batch sous Spring est une classe héritant de Jobs. Un job (notre batch donc) est définit par une ou plusieurs Step. Dans notre cas on ne va en implémenter qu’une. Une Step peut être de type item-oriented ou tasklet, le premier étant adapté à des traitements de lecture / écriture de données. Dans sa plus simple expression une Step peut ressembler à ceci:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
 
public class CustomStep implements Tasklet, StepExecutionListener {
    private final Logger logger = LoggerFactory.getLogger(CustomStep.class);
 
    @Override
    public void beforeStep(StepExecution stepExecution) {
        logger.debug("Custom step initialized.");
    }
 
    @Override
    public RepeatStatus execute(StepContribution stepContribution,
                                ChunkContext chunkContext) throws Exception {
        try {
            // Add your business logic here.
            logger.info("Custom Step is running ...");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return RepeatStatus.FINISHED;
    }
 
    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        logger.debug("Custom step ended.");
        return ExitStatus.COMPLETED;
    }
}

Maintenant que nous avons notre Step, il faut indiquer à Spring comment l’utiliser. Pour cela nous avons besoin d’une classe annotée de @Configuration indiquant à Spring que celle-ci est une source de définition de @Bean.

import com.ingeniance.batch.job.CustomStep;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import static com.ingeniance.batch.job.BatchConstant.JOB_NAME;
 
@Configuration
public class JobConfig {
    @Bean
    protected Step customStep(StepBuilderFactory stepBuilders) {
        return stepBuilders
                .get("customStep")
                .tasklet(new CustomStep())
                .build();
    }
 
    @Bean
    public Job customJob(JobBuilderFactory jobBuilders, StepBuilderFactory stepBuilders) {
        return jobBuilders
                .get(JOB_NAME)
                .start(customStep(stepBuilders))
                .build();
    }
}

Deux notions importantes ici:

  • La création de notre Step via une StepBuilderFactory.
  • L’injection de cette StepBuilderFactory à notre Job via sa création à l’aide d’une JobBuilderFactory.

Sauf que rien n’indique pour l’instant à Spring comment démarrer notre Job. C’est là qu’entre en jeu l’annotation @EnableBatchProcessing. Vous pourriez la rajouter directement à notre classe JobConfig, mais pour des besoins ultérieurs avec Quartz, nous allons créer une deuxième classe de configuration.

import org.springframework.batch.core.configuration.annotation.DefaultBatchConfigurer;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.context.annotation.Configuration;
 
import javax.sql.DataSource;
 
@Configuration
@EnableBatchProcessing
public class BatchConfig extends DefaultBatchConfigurer {
 
    @Override
    public void setDataSource(DataSource dataSource) {
        // initialize will use a Map based JobRepository (instead of database)
    }
}

C’est cette annotation qui va permettre l’initialisation et l’injection de nombreux beans dans notre application Batch. Ce fichier de configuration va également indiquer à l’auto-configuration d’utiliser une MapJobRepositoryFactoryBean en lieu et place d’une DataSource de type BDD. Cela nous sera utile pour la partie monitoring. Vous devez également ajouter une propriété d’exclusion à l’initialisation de l’application :

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class BatchApplication {
    public static void main(String[] args) {
        SpringApplication.run(BatchApplication.class, args);
    }
}

Notez que Spring se charge d’injecter directement nos beans stepBuilders et jobBuilders via l’annotation @EnableBatchProcessing.

Notre Batch est maintenant prêt à être exécuté.

Planification du Batch avec Quartz

Afin d’orchestrer les job instanciés avec Spring Batch, les équipes de Spring proposent l’utilisation de Spring Cloud Data Flow. Le problème est que Spring Cloud Data Flow s’intègre avec l’orchestrateur Kubernetes, un outil pas évident à prendre en main et pas forcément disponible dans votre stack technique. Pour compenser ce manque, il est possible d’intégrer à notre Batch l’API Quartz, très populaire parmi les développeurs Java, qui est un planificateur permettant l’exécution de tâches par l’intermédiaire de trigger appliqués aux jobs.

Vous devez dans un premier temps importer les bonnes dépendances pour faire fonctionner Quartz dans Spring Batch.

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

Vous devez ensuite ajouter un nouveau fichier de configuration qui permettra à Quartz d’instancier notre Job Spring Batch et de lui associer une planification d’exécution.

import com.ingeniance.batch.quartz.AutowiringSpringBeanJobFactory;
import com.ingeniance.batch.quartz.QuartzJobLauncher;
import org.quartz.spi.JobFactory;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
 
import java.util.HashMap;
import java.util.Map;
 
import static com.ingeniance.batch.job.BatchConstant.JOB_NAME;
 
@Configuration
@ConditionalOnExpression("'${scheduler.enabled}'=='true'")
public class QuartzConfig {
    @Value("${scheduler.trigger.cron-expression}")
    private String cronExpression;
 
    @Autowired
    private ApplicationContext applicationContext;
 
    @Bean
    public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) {
        JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor();
        jobRegistryBeanPostProcessor.setJobRegistry(jobRegistry);
        return jobRegistryBeanPostProcessor;
    }
 
    @Bean
    public JobDetailFactoryBean jobDetailFactoryBean() {
        JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
        jobDetailFactoryBean.setJobClass(QuartzJobLauncher.class);
        Map<String, Object> map = new HashMap<>();
        map.put("jobName", JOB_NAME);
        jobDetailFactoryBean.setJobDataAsMap(map);
        return jobDetailFactoryBean;
    }
 
    @Bean
    public CronTriggerFactoryBean cronTriggerFactoryBean() {
        CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
        cronTriggerFactoryBean.setJobDetail(jobDetailFactoryBean().getObject());
        cronTriggerFactoryBean.setCronExpression(this.cronExpression);
        return cronTriggerFactoryBean;
    }
 
    @Bean
    public JobFactory jobFactory() {
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
    }
 
    @Bean(name = "scheduler")
    public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory) {
        SchedulerFactoryBean scheduler = new SchedulerFactoryBean();
        scheduler.setJobFactory(jobFactory);
        scheduler.setTriggers(cronTriggerFactoryBean().getObject());
        scheduler.setApplicationContext(applicationContext);
        return scheduler;
    }
}

Pour identifier notre Job par Quartz on utilise une SchedulerFactoryBean. Cette factory initialise trois notions important de notre scheduler :

  • Le job à instancier (setJobFactory)
  • Le détail du ou des triggers à appliquer à notre job (setTriggers)
  • Le contexte de l’application Spring Batch (setApplicationContext)

La partie un peu « tricky » du code réside dans l’initialisation du jobFactory avec une AutowiringSpringBeanJobFactory.

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
 
public final class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory
        implements ApplicationContextAware {
 
    private transient AutowireCapableBeanFactory beanFactory;
 
    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle)
            throws Exception {
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);
        return job;
    }
 
    @Override
    public void setApplicationContext(final ApplicationContext context) {
        beanFactory = context.getAutowireCapableBeanFactory();
    }
}

Cette classe permet d’injecter automatiquement tous les beans de Quartz dans Spring. Ainsi, une fois le job instancié par Quartz, celui-ci sera accessible par injection depuis Spring via l’annotation @Autowired. Plutôt pratique dans le cas du monitoring que l’on souhaite mettre en place.

J’ai également ajouté un fichier application.properties qui prends trois paramètres. Libre à vous d’utiliser un fichier application.yml à la place :

# Custom Quartz Scheduler properties
scheduler.enabled=true
scheduler.trigger.cron-expression=0 0 3 * * ?
 
# Spring Batch properties
spring.batch.job.enabled=false

C’est ici que l’on définit notre Crontab (scheduler.trigger.cron-expression). Ici elle représente une expression répondant à tous les jours à 3 heure du matin.

Il existe de nombreux sites en lignes vous permettant de générer très facilement des Crontab à l’aide de formulaires: https://www.freeformatter.com/cron-expression-generator-quartz.html

spring.batch.job.enabled permet d’indiquer à Spring si notre Batch doit s’exécuter au lancement.

Le dernier paramètre, scheduler.enabled, nous permet simplement d’appliquer ou non notre planificateur à notre Batch via une annotation de condition dans le code :

@ConditionalOnExpression("'${scheduler.enabled}'=='true'")

Vous l’aurez sans doute remarqué mais il nous reste encore à implémenter la classe en charge d’exécuter le job :

import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.*;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;
 
import java.util.Map;
 
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class QuartzJobLauncher extends QuartzJobBean {
    private final Logger log = LoggerFactory.getLogger(this.getClass());
 
    private JobLauncher jobLauncher;
    private JobLocator jobLocator;
 
    @Autowired
    private ApplicationContext applicationContext;
 
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        try {
            jobLocator = applicationContext.getBean(JobLocator.class);
            jobLauncher = applicationContext.getBean(JobLauncher.class);
 
            Map<String, Object> jobDataMap = context.getMergedJobDataMap();
            String jobName = (String) jobDataMap.get("jobName");
            JobParameters params = new JobParametersBuilder()
                    .addString("JobID", String.valueOf(System.currentTimeMillis()))
                    .toJobParameters();
            Job job = jobLocator.getJob(jobName);
            JobExecution jobExecution = jobLauncher.run(job, params);
 
            log.info("########### Status: " + jobExecution.getStatus());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

L’annotation @PersistJobDataAfterExecution permet de conserver les données liées aux précédentes exécutions du Batch. @DisallowConcurrentExecution évite l’exécution simultanée du Batch dans le cas d’une planification trop courte.

Monitoring du Batch via une application Angular

Contrôleurs REST

Maintenant que nous avons un Batch « schedulé », l’idéale serait de pouvoir le piloter depuis une interface Web. Nous devons dans un premier temps exposer plusieurs contrôleurs REST donnant accès à différentes actions sur notre Batch. Je ne vais pas m’attarder sur la partie sécurité qui pourrait faire l’objet d’un article à part entière. Nos contrôleurs seront donc exposés de manière publique.

import com.ingeniance.batch.models.Config;
import com.ingeniance.batch.quartz.SchedulerStates;
import com.ingeniance.batch.quartz.TriggerMonitor;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
 
import javax.annotation.Resource;
import java.util.Collections;
import java.util.Map;
 
@RestController
@RequestMapping("/scheduler")
@ConditionalOnExpression("'${scheduler.enabled}'=='true'")
public class SchedulerController {
    private final Logger log = LoggerFactory.getLogger(this.getClass());
 
    @Autowired
    private TriggerMonitor triggerMonitor;
 
    @Resource
    private Scheduler scheduler;
 
    @GetMapping(produces = "application/json")
    public Map<String, String> getStatus() throws SchedulerException {
        log.trace("SCHEDULER -> GET STATUS");
        String schedulerState = "";
        if (scheduler.isShutdown() || !scheduler.isStarted())
            schedulerState = SchedulerStates.STOPPED.toString();
        else if (scheduler.isStarted() && scheduler.isInStandbyMode())
            schedulerState = SchedulerStates.PAUSED.toString();
        else
            schedulerState = SchedulerStates.RUNNING.toString();
        return Collections.singletonMap("data", schedulerState.toLowerCase());
    }
 
    @GetMapping("/pause")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void pause() throws SchedulerException {
        log.info("SCHEDULER -> PAUSE COMMAND");
        scheduler.standby();
    }
 
    @GetMapping("/resume")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void resume() throws SchedulerException {
        log.info("SCHEDULER -> RESUME COMMAND");
        scheduler.start();
    }
 
    @GetMapping("/run")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void run() throws SchedulerException {
        log.info("SCHEDULER -> START COMMAND");
        scheduler.start();
    }
 
    @GetMapping("/stop")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void stop() throws SchedulerException {
        log.info("SCHEDULER -> STOP COMMAND");
        scheduler.shutdown(true);
    }
 
    @GetMapping("/config")
    public ResponseEntity getConfig() {
        log.debug("SCHEDULER -> GET CRON EXPRESSION");
        CronTrigger trigger = (CronTrigger) triggerMonitor.getTrigger();
        return new ResponseEntity<>(Collections.singletonMap("data", trigger.getCronExpression()), HttpStatus.OK);
    }
 
    @PostMapping("/config")
    public ResponseEntity postConfig(@RequestBody Config config) throws SchedulerException {
        log.info("SCHEDULER -> NEW CRON EXPRESSION: {}", config.getCronExpression());
        CronTrigger trigger = (CronTrigger) triggerMonitor.getTrigger();
 
        TriggerBuilder<CronTrigger> triggerBuilder = trigger.getTriggerBuilder();
        CronTrigger newTrigger = triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(config.getCronExpression())).build();
 
        scheduler.rescheduleJob(triggerMonitor.getTrigger().getKey(), newTrigger);
        triggerMonitor.setTrigger(newTrigger);
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

Nous avons :

  • 1 contrôleur par défaut qui affiche le statut du Batch: getStatus()
  • 4 contrôleurs pour gérer le pilotage de notre Batch: pause()resume()run() et stop()
  • 2 contrôleurs pour récupérer et mettre à jour notre CrontabgetConfig() et postConfig()

A noter également la présence de deux nouvelles classe:

  • TriggerMonitor: permettant de conserver une instance de la planification appliquée au Batch.
import org.quartz.Trigger;
 
public class TriggerMonitor {
    private Trigger trigger;
 
    public void setTrigger(Trigger trigger) {
        this.trigger = trigger;
    }
 
    public Trigger getTrigger() {
        return trigger;
    }
}
  • SchedulerStates: une énumération définissant les différents états du Batch.
public enum SchedulerStates {
    RUNNING, STOPPED, PAUSED
}

TriggerMonitor est injecté à l’aide de l’annotation @Autowired et demande donc la présence d’un @Bean dans le fichier de configuration QuartzConfig.java :

@Bean
public TriggerMonitor triggerMonitor() {
    TriggerMonitor triggerMonitor = new TriggerMonitor();
    triggerMonitor.setTrigger(cronTriggerFactoryBean().getObject());
    return triggerMonitor;
}

Si tout s’est bien passé et si vous compilez votre Batch, vous devriez pouvoir accéder aux informations de celui-ci via les différents endpoints de l’API REST -> Ex: http://localhost:8080/scheduler

Le port 8080 est le port par défaut fournit par le serveur Tomcat de Spring Boot. Il peut être modifié depuis les propriétés d’application.

Il nous manque éventuellement un contrôleur qui permettrait de visualiser la progression du Batch en temps réel, mais cela fera l’objet d’un article complémentaire sur les WebSocket. Attardons nous maintenant sur notre application Web.

Initialisation du projet client avec npm

Angular dispose d’un CLI assez poussé qui permet la génération rapide d’un projet simple. Mais avant d’aller plus loin, vous devez tout d’abord installer npmnpm est un outil de partage de module JavaScript. Il est fourni avec l’installation de Node.js.

Je vous invite également à installer en parallèle l’outil de commande Git Bash. Via l’intégration Windows, vous aurez ainsi la possibilité d’exécuter des commandes npm depuis vos projets plus facilement.

Une fois tous ces outils installés, depuis le dossier devant contenir votre projet, lancez un bash git (bouton droit > Git Bash Here), puis tapez les commandes suivantes:

npm install -g @angular/cli # Installe le CLI d'Angular au niveau global
ng new frontend             # Génère le projet sous le dossier frontend
cd frontend                 # Vous positionne dans le nouveau workspace

Pour voir le résultat vous pouvez éventuellement lancer un serveur web avec la commande suivante et visualiser la page depuis l’adresse par défaut: http://localhost:4200

ng serve

Nous allons rajouter un dossier services à notre projet web. Afin d’être dans les règles de l’art nous allons fournir un service (à ajouter dans le dossier services) capable de communiquer avec notre API.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
 
@Injectable()
export class SchedulerService {
  constructor(private http: HttpClient) {}
 
  startScheduler = () => {
    return this.http.get('/batch-spring/run');
  }
 
  stopScheduler = () => {
    return this.http.get('/batch-spring/stop');
  }
 
  pauseScheduler = () => {
    return this.http.get('/batch-spring/pause');
  }
 
  resumeScheduler = () => {
    return this.http.get('/batch-spring/resume');
  }
 
  getStatus = () => {
    return this.http.get('/batch-spring');
  }
 
  getConfig = () => {
    return this.http.get('/batch-spring/config');
  }
 
  updateConfig = (cronExpression: string) => {
    return this.http.post('/batch-spring/config', {cronExpression});
  }
}

Les endpoints d’accès à notre API sont un peu particuliers pour la simple raison que nous allons passer par un proxy pour configurer nos appels vers notre API Java. Pour cela il suffit d’ajouter un fichier proxy.conf.json à la racine de notre projet Angular:

{
  "/batch-spring/*": {
    "target": "http://localhost:8080",
    "secure": false,
    "logLevel": "debug",
    "changeOrigin": true,
    "pathRewrite": {
      "^/batch-spring": "/scheduler"
    }
  }
}

Nous allons également modifier le fichier package.json pour scripter l’exécution de notre client Web avec l’aide du dit proxy. Modifiez le fichier de la manière suivante:

"scripts": {
    "ng": "ng",
    "start": "ng serve --proxy-config proxy.conf.json", // Ligne à modifier
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },

Il faut maintenant consommer notre service depuis notre composant Angular. Modifier le fichier app.component.ts comme suit:

import { Component, OnInit } from '@angular/core';
import { SchedulerService } from './services/scheduler.service';
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less']
})
export class AppComponent implements OnInit {
  constructor(private schedulerService: SchedulerService) {}
 
  private configBackup: string;
 
  public schedulerState: string;
  public cronExpression: string;
  public onError = false;
  public configMessage: string = null;
 
  ngOnInit() {
    // Get the current status.
    this.getScheduler();
    // Get current Cron expression.
    this.retrieveConfig();
  }
 
  /**
   * Get the current scheduler status.
   */
  public getScheduler = () => {
    this.schedulerService.getStatus().subscribe(
      (response: any) => {
        this.schedulerState = response.data;
      },
      error => {
        console.error(error);
      }
    );
  }
 
  /**
   * Start scheduler.
   */
  public startScheduler = () => {
    this.schedulerService.startScheduler().subscribe(
      () => {
        this.schedulerState = 'running';
      },
      error => {
        console.error(JSON.stringify(error));
      }
    );
  }
 
  /**
   * Stop scheduler.
   */
  public stopScheduler = () => {
    this.schedulerService.stopScheduler().subscribe(
      () => {
        this.schedulerState = 'stopped';
      },
      error => {
        console.error(JSON.stringify(error));
      }
    );
  }
 
  /**
   * Pause scheduler.
   */
  public pauseScheduler = () => {
    this.schedulerService.pauseScheduler().subscribe(
      () => {
        this.schedulerState = 'paused';
      },
      error => {
        console.error(JSON.stringify(error));
      }
    );
  }
 
  /**
   * Resume scheduler.
   */
  public resumeScheduler = () => {
    this.schedulerService.resumeScheduler().subscribe(
      () => {
        this.schedulerState = 'running';
      },
      error => {
        console.error(JSON.stringify(error));
      }
    );
  }
 
  /**
   * Start or pause scheduler.
   */
  public startOrPause = () => {
    switch (this.schedulerState) {
      case 'running':
        this.pauseScheduler();
        break;
      case 'paused':
        this.resumeScheduler();
        break;
      default:
        this.startScheduler();
        break;
    }
  }
 
  /**
   * Get current Cron Expression from Batch-SX API REST.
   */
  public retrieveConfig = () => {
    this.schedulerService.getConfig()
      .subscribe((response: any) => {
        this.cronExpression = response.data;
        this.configBackup = response.data;
      });
  }
 
  /**
   * Define a new Cron expression to manage batch-sx execution process.
   */
  public submitConfig = () => {
    this.schedulerService.updateConfig(this.cronExpression)
      .subscribe(() => {
        this.onError = false;
        this.configBackup = this.cronExpression;
        this.configMessage = 'Update of the new schedule was successfully completed.';
      }, error => {
        this.onError = true;
        this.configMessage = error;
        this.cronExpression = this.configBackup;
      });
  }
}

On couvre à peu près toutes les fonctions de pilotage de notre Batch: le statut, l’arrêt et le redémarrage, la modification de la planification. Il nous faut maintenant une vue pour afficher tout ça. Remplacez le contenu de app.component.html par celui ci-après:

<div class="spring-batch d-flex flex-column w-100 h-75 pt-5">
  <h2 class="ml-4">Monitoring Spring Batch with Quartz Scheduler</h2>
  <div class="d-flex flex-row">
    <!-- Status card -->
    <div class="card ml-4">
      <h5 class="card-header">Status</h5>
      <div class="card-body">
        <h5 class="card-title">Current status : <span [ngClass]="{
          'badge': true,
          'badge-danger': schedulerState === 'stopped' || schedulerState === 'paused',
          'badge-success': schedulerState === 'running'
        }">{{ schedulerState }}</span></h5>
        <p class="card-text">You can pause or restart the batch via the button below.</p>
        <button type="button" id="schedulerControllerBtn" [ngClass]="{
            'btn': true,
            'btn-success': schedulerState === 'stopped' || schedulerState === 'paused',
            'btn-danger': schedulerState === 'running'
          }" (click)="startOrPause()">
          <span *ngIf="schedulerState === 'running'">
            <i class="fa fa-fw fa-pause"></i>
          </span>
          <span *ngIf="schedulerState === 'stopped' || schedulerState === 'paused'">
            <i class="fa fa-fw fa-play"></i>
          </span>
        </button>
      </div>
    </div>
    <!-- Config card -->
    <div class="card ml-4">
      <h5 class="card-header">Configuration</h5>
      <div class="card-body">
        <h5 class="card-title">
            Cron expression currently applied :
            <input placeholder="Cron expression" [(ngModel)]="cronExpression" name="cronExpression" type="text">
            <button type="button" class="btn btn-primary ml-3"
                (click)="submitConfig()">
                Save
            </button>
        </h5>
        <p class="card-text">Modify the field above to apply a new schedule to the batch.</p>
        <div *ngIf="configMessage !== null" [ngClass]="{'alert': true, 'alert-success': !onError, 'alert-danger': onError}">{{ configMessage }}</div>
      </div>
    </div>
  </div>
</div>

Pour améliorer le rendu de notre pas web, j’ai utilisé deux composants: bootstrap et font-awesome. Vous pouvez les installer au projet de la manière suivante depuis votre terminal Bash ou Powershell:

npm i bootstrap font-awesome --save

N’oubliez pas également des les appliquer à votre page en important les distribuables depuis le fichier Less associé à votre composant:

@import "~bootstrap/dist/css/bootstrap.css";
@import "~font-awesome/css/font-awesome.css";
 
.spring-batch {
    .card-title {
        span {
            font-style: italic;
 
            &::first-letter {
                text-transform: uppercase;
            }
        }
    }
}

J’ai ajouté quelques éléments d’embellissements appliqués à la classe CSS spring-batch. N’hésitez pas à vous documenter sur le langage Less ou Sass qui apportent un vrai plus dans l’utilisation des feuilles de styles CSS.

Reste plus qu’à déclarer au niveau du module Angular l’ensemble des dépendances utilisées dans notre page. Modifiez le fichier app.module.ts comme suit:

import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { SchedulerService } from './services/scheduler.service';
 
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    HttpClientModule,
    BrowserModule,
    FormsModule
  ],
  providers: [SchedulerService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Cela permet de déclarer globalement à notre application l’ensemble des modules utilisables par notre application ainsi que l’injection de notre service de scheduling. Il ne nous reste plus qu’à compiler et exécuter notre serveur Web pour afficher notre page. N’oubliez pas d’avoir une instance de votre Batch Spring de lancée.

npm start

Conclusion

Dans cet article nous avons vu:

  • Comment déployer un Batch simple à l’aide de l’environnement Spring via son composant Spring Batch.
  • Comment planifier ce Batch à l’aide de l’API Quartz Scheduler.
  • Comment piloter et monitorer les informations de notre Batch depuis une application Web de type Angular.

L’ensemble des sources sont visibles depuis une répo Git: https://github.com/GutsFun/Monitoring-Spring-Batch

Vous avez ainsi les éléments pour démarrer et mettre en place rapidement un Batch dans un environnement Java le tout schedulé par Quartz et piloté via une page Web.

Références

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Planifier et monitorer un batch Spring à l’aide de Quartz Job S…

par Cyrille Perrot Temps de lecture: 15 min
0