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 Spring: Spring 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’uneJobBuilderFactory
.
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
etjobBuilders
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()
etstop()
- 2 contrôleurs pour récupérer et mettre à jour notre Crontab:
getConfig()
etpostConfig()
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 npm. npm 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
- Spring Batch: https://spring.io/projects/spring-batch
- Angular: https://angular.io/
- Quartz Job Scheduler: http://www.quartz-scheduler.org/
- Langage Less: http://lesscss.org/
- Langage Sass: https://sass-lang.com/