Spring-Batch (Part II): Validation and Skip Policy. Ou comment gérer les données non valides dans un batch?

Après la première partie sur les tests unitaires du reader de Spring-Batch, nous passons à la validation des données input.
Ce second volet exige un minimum de connaissances sur spring & spring-batch.

Nous tentons de relever plusieurs défis majeurs correspondants aux attentes en production:

  • identifier avec précision le numéro de la ligne invalide (non traitée par le batch lancé la veille),
  • générer un rapport exploitable du déroulement du batch (exécutée la nuit),
  • porter en base les erreurs rencontrées en traçant correctement leurs causes exactes,

Nous nous posons la question de la validation des lignes lues par le Reader et aussi comment définir une politique d'éviction (skip) de ces lignes non valides.

En franglais, c'est la question validate & skip policy dans les batchs.

Ce sont deux notions bien distinctes mais fort bien liées.

Bien valider les données en entrée est une question naturelle voire critique dans tous les batchs.

Gérer les données en doublons dans les batchs est également une question centrale.

Eh oui, lorsque nous avons attaqué la validation avec spring-batch, les pièges n'étaient pas si minimes!

Et pourtant spring-batch structure cette partie en recommandant d'utiliser ses API adaptées.

Néanmoins tout n'est pas toujours clairement documenté ou très bien présenté!

De nombreux défis sont à lever surtout quand les batchs durent longtemps et, dans ce dernier cas, la validation devient davantage critique.

En particulier, un premier défi majeur consiste à autoriser le traitement du batch d'aller jusqu'au bout quitte à écarter un certain nombre (seuil à définir) de lignes non conformes. Ces lignes non valides doivent être traitées à part et/ou enregistrées dans une table d'erreurs dans la BDD.

Un autre défi, en découle, celui de maîtriser la variété des params du batch/jobs (ex. skipLimit, skip policy,..) qui pourrait influencer le statut du batch (le faire passer du coup de COMPLETE à FAILED ou inversement) ce qui n'est pas sans impact en intégration continue.

Enfin, un dernier défi, facilement réalisable avec spring-batch, est de fournir un compte rendu précis du déroulement de chacune des étapes du batch (reader, processor, writer, step, chunk, job).

 

Pour illustrer le rapport attendu, partons de ce fichier  src/main/resources/entreprises.csv:

id, rs, dateCreation
1,"rs1",2000-01-1
2,rs2,2017-05-13
3,rs3,2017-05-14
0,rs4,2017-05-14

 

Le fichier csv contient un header qui sera ignoré par le reader.

Chaque ligne du fichier donc, correspond à une entreprise.

Une ligne est valide lorsque l'identifiant (fourni) est non nul.

Note. La ligne 5, de l'extrait précédent, donne un exemple de donnée non valide puisque l'identifiant fourni est égal à zéro.

Ainsi la capture ci-dessous illustre le rapport attendu:

 _/_/_/
 :: Spring Boot ::        (v1.5.3.RELEASE)
INFO  Job: [FlowJob: [name=myjob]] launched with parameters:.. 
INFO =>Found 0 nb entreprises in BD before job.
INFO => Found 0 nb erreurs in DB.
INFO Executing step: [mystep]
INFO item valide: EntrepriseDto(numeroLigne=2, idFourni=1,..)
INFO item valide: EntrepriseDto(numeroLigne=3, idFourni=2,..)
966  INFO item valide: EntrepriseDto(numeroLigne=4, idFourni=3,..)
ERROR =>Validation Exception in input at line 5. 
 [Validation failed for
 EntrepriseDto(numeroLigne=5, idFourni=0, rs=rs4..): 
 Field error on field 'idFourni': rejected value [0]; 
message [
 Erreur, l'idFourni '0' doit etre un entier superieur ou egal a 1]]
INFO Writing err : Erreur(numLine=5,
 typeException=o.s.b.i.v.ValidationException, ..)
INFO item valide:
 EntrepriseDto(numeroLigne=2,idFourni=1,..)
INFO item valide:
 EntrepriseDto(numeroLigne=3, idFourni=2,..)
INFO item valide:
EntrepriseDto(numeroLigne=4, idFourni=3,..)
INFO Writing : Entreprise(idFourni=1, rs=rs1, ..)
INFO  f.n.a.w.MyCompositeJpaWriter: Writing : 
Entreprise(idFourni=2, rs=rs2, ..)
INFO  f.n.a.w.MyCompositeJpaWriter: Writing : 
Entreprise(idFourni=3, rs=rs3,..)
INFO f.n.a.l.JobCompleteListener: =>3 nb entreprises en BD.
INFO f.n.a.l.JobCompleteListener: =>Liste entreprises portees en BD:
[
	Entreprise(idFourni=1, rs=rs1,..), 
	Entreprise(idFourni=2, rs=rs2, ..), 
	Entreprise(idFourni=3, rs=rs3, ..)
] 
INFO f.n.a.l.JobCompleteListener: =>0 nb erreurs found
INFO o.s.b.c.l.s.SimpleJobLauncher: Job: [name=myjob] completed..

 

Revoyons ensemble le contenu de cette capture où quelques défis sont déjà réglés.

Par exemple, la ligne 10 illustre le cas de donnée invalide 'catchée' par ValidationException.

Plus intéressant, la ligne 30 donne un compte-rendu (sommaire pour ce blog) à la fin du batch.
Mieux encore l'affichage du numéro de la ligne défaillante (tel qu'il est dans le fichier input).

Ce résultat est obtenu sans trop écrire du code. Nous donnons toutes les précisions utiles plus loin.

Ce billet est très dense, prenez un bon café car de multiples notions sont évoquées:

  • chunk,
  • listener du job (pour le compte-rendu sur le déroulement du batch et sur l'état de la BD),
  • reader pour le fichier csv,
  • processor qui intègre la validation (et les pièges à éviter),
  • listener (du processor) afin de gérer les lignes non valides,
  • listener (du chunk),
  • writer JpaWriter,
  • faultTolerant,
  • skip (les exceptions de validation),
  • skipLimit.

 

La démo repose sur java 8, spring-boot 1.5, spring-batch 3 et hibernate-validator 5.

Rassurez vous, toute la configuration de spring est réalisée avec exclusivement du java (zéro xml).

Le scénario de la démo (basique mais couvrant bien le besoin réel) consiste à charger en BD les lignes, uniquement celles valides, d'un fichier input de format csv.

Nous gérons aussi la présence des doublons en base selon des critères métiers (mais le focus n'est pas là-dessus).

Donc les grandes étapes de la démo:

  • en entrée un fichier csv à lire via le reader FlatFileItemReader de spring-batch (avec ses composants Tokenizer, Mapper,..)

.

  • en sortie du reader, les lignes lues sont transformées en une liste d'objets java de type EntrepriseDto,
  • cette liste d'objets est transmise au processor qui se charge de les valider et/ou de skipper celles non valides,
  • un listener (avec sa méthode onErrorProcess) se charge des lignes non valides (porter en BD),
  • un writer JPA pour porter les objets valides en bd,
  • enfin un listener pour établir un compte rendu sur le déroulement du batch.

 

Nous traçons l'état du batch, les lignes rejetées, les lignes portées en BD ainsi que les traitements effectués.

 

PRATIQUONS UN PEU

 

L'arborescence du projet démo sera à la fin comme suit:

L'entité du modèle nommée Entreprise.java contient ce code:

package fr.netapsys.abdou.model;
import javax.persistence.*;
import lombok.*;
import javax.validation.constraints.NotNull;

@Getter @Setter
@ToString(exclude={"id"})
@EqualsAndHashCode(exclude={"id"})
@Entity
public class Entreprise {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
	private Long id;
	
	private Long idFourni;
	@NotNull 
	private String rs;
	private String dateCreation;
}

Simple entité hibernate, non?

Afin d'identifier le numéro de la ligne invalide dans le compte rendu à la fin du batch, nous introduisons ce nouveau DTO en rajoutant le contrat org.springframework.batch.item.ItemCountAware ayant une seule méthode void setItemCount(int numLine).

Ainsi, la classe EntrepriseDto est constituée de ces lignes de code:

package fr.netapsys.abdou.model;

import javax.validation.constraints.
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.batch.item.ItemCountAware;
import lombok.Data;
@Data
public class EntrepriseDto implements ItemCountAware {	
	private  int numeroLigne;
	@NotNull(message="Erreur, l'idFourni ne doit etre null!") 
	@NotBlank(message="Erreur, l'idFourni '${validatedValue}' ne doit etre vide!")
	@Min(
	  value=1, 
	  message="Erreur, l'idFourni '${validatedValue}' doit etre un entier superieur ou egal a {value}"
	 )
	private String idFourni;

	private String rs;
	private String dateCreation;

	@Override 
	public void setItemCount(int numLine) {
		this.numeroLigne = numLine+1;		
	}
}
DTO & contrat ItemCountAware

 

Ce qui a changé:

  • l'apparition d'un attribut nommé numeroLigne,
  • l'implémentation de la méthode setItemCount (notez dans le setter, num +1 pour que le comptage démarre à 1),
  • retenez que en sortie du reader c'est EntrepriseDto,
  • ainsi l'input du processor est EntrepriseDto et sa sortie est de type Entreprise.

 

SCHEMA DE LA BASE

Le fichier schema-all.sql est utilisé par spring batch pour charger (en mémoire) en BD H2:

DROP TABLE entreprise IF EXISTS;

CREATE TABLE entreprise  (
    id bigint serial NOT NULL PRIMARY KEY,
    idFourni int,
    rs VARCHAR(500),
    dateCreation VARCHAR(40)
);

DROP TABLE err IF EXISTS;
CREATE TABLE err  (
    id bigint serial NOT NULL PRIMARY KEY,
    numLine integer,
    contenu VARCHAR(2000),
    typeException VARCHAR(500),
    stackTrace VARCHAR(2000),
    dateCrea VARCHAR(40)
);
schema-all.sql

 

CONFIGURER BATCH

 

Afin d'éviter de nombreux pièges, prenez soin dès la configuration de spring-batch de choisir les bons packages de validation, lisez les imports dans les entêtes de java.

Nous décrivons en premier le fichier AppConfigBatch.java qui centralise la configuration de spring et de notre principal job (nommé myjob). Ce fichier vous sera fourni à la fin du billet.

Nous vous recommandons de suivre son contenu pas à pas.

Nous le présentons par blocs en commençant par l'unique job de notre batch démo.

 

PART 1. CONFIGURER JOB

 

Ce bloc code peut être considéré comme le point d'entrée dans l'unique batch de la démo:

@Bean
public Job loadEntrepJob(
 final JobCompleteListener completeJobListener,
 final ChunkListener chunkListener, 
 final ItemProcessListener<EntrepriseDto,Entreprise> itemProcListener, 
 final ItemReadListener<EntrepriseDto> readItemListener
 ){
  return jobBuilderFactory.get("myjob")
      .incrementer(new RunIdIncrementer())
      .listener(completeJobListener)
      .flow(mystep(chunkListener,itemProcListener, readItemListener))
      .end()
      .build();     
 }

 

Zoomons un peu sur ce bout de code. Quelques indications intéressantes à voir:

  • Lignes 2 à 5: le job configuré avec trois listeners : JobCompleteListener, ChunkListener & ItemProcessListener.
    Le premier est pour le job (ligne 6).
    Et les autres listeners sont de scope step d'où les arguments du bean mystep de la ligne 11 (mystep sera configuré juste après),

 

Note. le code du listener (classe JobCompleteListener.java) sera fourni plus loin avec les deux autres listeners.

A ce propos, il est prudent de noter que JobListener n'est pas une interface de Spring-Batch, c'est JobExceutionListener qui en est une dans spring-batch.

Par conséquent, nous avons utilisé l'implémentation JobExecutionListenerSupport pour faciliter l'écriture de notre classe JobCompleteListener.

 

PART 2. CONFIGURER CHUNK & STEP

 

chunk in step

Chunk

 

Et passons au bean mystep dont voici le code:

@Bean 
 public Step mystep(
  final ChunkListener chunkListener, 
  final ItemProcessListener<EntrepriseDto,Entreprise> procListener,
  final ItemReadListener<EntrepriseDto> readItemListener
  ){
   return stepBuilderFactory.get("mystep")
    .<EntrepriseDto, Entreprise> chunk(4)
    .faultTolerant()
    .skip(ValidationException.class)
    .skip(FlatFileParseException.class)
   .skip(DuplicateKeyException.class)
   .skipLimit(9)
   .listener(chunkListener)
   .listener(procListener)
   .listener(readItemListener)
   .reader(myReader())
   .processor(validItemProcessor())
   .writer(writer())
   .build();
}

 

Ko debout! C la Kata! C (C)atastrophe comme code!

Mais non! Voyons ensemble les explications en détails:

  • La ligne 2 à 5: initialise le constructeur mystep ayant comme arguments les listeners définis dans le job ci-dessus,
  • Ligne 8: définit un chunk de taille 4 (c'est le pas de commit pour le writer ),
  • Ligne 9: active la tolérance (j'adore la tolérance 🙂 lorsque des exceptions se produisent! Cette ligne est indispensable afin de définir la politique d'éviction (skip policy) après (Ouf! j'espère que c'est clair!),
  • Lignes 10 à 13: permettent de skipper certains types d'erreurs  (ValidationException pour les erreurs de validation, Parse exception du reader, DuplicateException) et jusqu'à un certain seuil ou limite (ici fixé à 9 voir la ligne 13) ,
  • Lignes 14 à 16: activent les listeners passés en arguments,
  • Lignes restantes: définissent un reader , processor, un writer (du standard ou presque).

Notes.

  • commit-interval ou pas de commit est fixé à 4 (je sais c'est en dur et c'est mal!),
  • skipLimit est fixé à titre d'exemple ici à 9 (encore en dur dans le code!)

Mais avouons-le, il manque pas mal de code, en particulier, les beans xxxxListener, myReader, validItemProcessor, writer.

Alors remédions à cela, voici les éléments du code manquant (toujours extrait de la classe AppConfigBatch.java).

 

PART 3. CONFIGURER READER

 

Pour le reader le code est peut-être un peu spécial:

@Bean  @StepScope
public MyFlatFileItemReader<EntrepriseDto> myReader() {
		MyFlatFileItemReader<EntrepriseDto> myFlatFileItemReader = new MyFlatFileItemReader<>();

myFlatFileItemReader.setDelegate(reader());
 
return myFlatFileItemReader;
 }

public FlatFileItemReader<EntrepriseDto> reader() {
  return 
ReaderUtils.
configureFtaFileReaderByDelimiterAndQuoteAndSuffixPolicy(

  pathToFile,
  ReaderUtils.NAMES_COLUMNS,
  ReaderUtils. DELIMITER_SEPARATEUR_CSV,
  '"',null, 
   maxCount2Read.intValue());
}

Il suffit de voir que ReaderUtils contient une seule méthode statique configureFtaFileReaderByDelimiterAndQuoteAndSuffixPolicy permettant de configurer un reader pour le fichier csv (voir le code reader).

Ensuite, MyFlatFileItemReader est une spécialisation du reader. Voir mon billet sur le reader et le concept delegate pour plus d'explications. voir le code de MyFlatFileItemReader ici

 

Chère lectrice, cher lecteur si vous êtes encore là alors bravo! Mais je crois que votre café s'est déjà refroidi 🙂

 

Habituez vous au principe de separation of concerns afin de produire du code maintenable et lisible.

Il est donc plus pratique de vous fournir les trois listeners du batch dans des classes distinctes (voici le code listeners) regroupés dans un même package.

Non, rassurez-vous, nous n'avons pas oublié d'évoquer les classes essentielles de ce billet, les validateurs.

Et comme c'est un peu plein de pièges, fonçons tout doux dans les détails des validateurs.

 

Mais avant cela, parlons un peu des listeners.

 

PART 4. CODER LES LISTENERS

 

L'ensemble des listeners sont consignés dans ce listeners.

 

Passons maintenant au processor.

 

PART 5. CONFIGURER PROCESSOR

Le bean processor dans le fichier de config de spring batch est constitué de ces lignes:


//processor
@Bean @StepScope
public ItemProcessor<EntrepriseDto, Entreprise> validItemProcessor(){
	return new 
           MyValidatingItemProcessor(); 
 }

La classe MyValidatingItemProcessor (peut être mal nommée?) est comme suit:

package fr.netapsys.abdou.processor;

import java.util.List;

import o.s.batch.core.configuration.annotation.StepScope;
import o.s.batch.item.ItemProcessor;
import o.s.batch.item.validator.SpringValidator;
import o.s.batch.item.validator.ValidationException;
import o.s.beans.factory.annotation.Autowired;
import o.s.dao.DuplicateKeyException;
import o.s.stereotype.Component;

import fr.netapsys.abdou.model.Entreprise;
import fr.netapsys.abdou.model.EntrepriseDto;
import fr.netapsys.abdou.repositories.EntrepriseRepository;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component @StepScope
public class MyValidatingItemProcessor implements 
                 ItemProcessor <EntrepriseDto,Entreprise>{
@Autowired EntrepriseRepository entRepo;
	@Autowired  
	SpringValidator<EntrepriseDto> springValidator;

	@Override
	public Entreprise process(EntrepriseDto item) throws 
	                        ValidationException {
		springValidator.validate(item);
		Entreprise e = convertToEntreprise(item);
		isDuplicateInDbByCleFonctionnel(e);
		log.info("item valide: "+item);
		return e;
	}

	private void isDuplicateInDbByCleFonctionnel(Entreprise entrep) {
		List<Entreprise> list = entRepo.findAll();//TODO tempor
		for(Entreprise ent : list){
			if( ent.equals(entrep) ){
				throw new 
				  DuplicateKeyException("Error! Duplicate item:"+entrep+" in db!");
			}
		}
	}

	private Entreprise convertToEntreprise(EntrepriseDto item) {
		Entreprise entrep= new Entreprise();
		entrep.setIdFourni(Long.valueOf(item.getIdFourni()));
		entrep.setRs(item.getRs());
		entrep.setDateCreation(item.getDateCreation());
		return entrep;
	}
}

 

 

Le focus de ce blog est la partie suivante qui est la validation.

 

PART 6. CONFIGURER VALIDATOR

 

Pour cette section, nous mentionnons, de manière verbeuse, les noms des packages afin de lever toute ambiguïté.

Insistons un peu, nous utilisons exclusivement SpringValidator du package org.springframework.batch.item.validator.

Je crois que ce choix est mieux que de perdre des heures sur le net... mais vous êtes libre de chercher votre propre chemin 🙂

SpringValidator implémente le contrat (interface) org.springframework.batch.item.validator.Validator afin de l'adapter au contrat org.springframework.validation.Validator.

D'où SpringValidator utilise un delegate de type org.springframework.validation.Validator.

C'est important de voir ce que la javadoc de spring précise:

SpringValidator (spring-batch-infrastructure 3.0.7.RELEASE API)
Class SpringValidator<T>: org.springframework.batch.item.validator.SpringValidator<T>

 

 

Au préalable, nous configurons org.springframework.validation.beanvalidation.LocalValidatorFactoryBean qui produit une instance de type org.springframework.validation.Validator (le delegate dans SpringValidator).

Notes.

  • Le bean LocalValidatorFactoryBean est une factory qui trouve automatiquement l'implémentation hibernate-validator (si le jar soit présent dans le classpath),
  • C'est le processor qui utilise SpringValidator pour appeler sa méthode void validate(item) throws ValidationException.

 

Revenons à la configuration, voici le code pour les deux beans de validation (localValidatorFactoryBean & SpringValidator):

@Bean //needed 
public org.springframework.validation.Validator localValidatorFactoryBean(){
	return new LocalValidatorFactoryBean(); 
} 

@Bean 
public <T> SpringValidator<T> springValidatorItem(){

 SpringValidator<T> springValidator = 
    new SpringValidator<>();
  springValidator.setValidator(
          localValidatorFactoryBean()); 
  
  return springValidator;
}

 

Le commentaire dans le code mentionne la nécessité de définir les beans.

 

 

PART 7. CONFIGURER WRITER

Là c'est presque du standard sauf que c'est un compositeWriter qui permet de sauvegarder l'entité Entreprise ou l'entité Erreur selon qu'une exception est levée ou non.

Nous utilisons spring data pour la couche DAO ce qui revient à déclarer des interfaces uniquement!

 

D'abord dans le fichier de config AppBatchConfig.java nous définissions le bean myWriter:

@Bean @StepScope
public <T> ItemWriter<T> writer() {
	return 
      new MyCompositeJpaWriter<T>();
  }

Voici le code de la classe MyCompositeJpaWriter:

package fr.netapsys.abdou.writers;
import java.util.List;
import o.s.batch.item.ItemWriter;
import o.s.beans.factory.annotation.Autowired;
import o.s.stereotype.Component;
import fr.netapsys.abdou.model.Entreprise;
import fr.netapsys.abdou.model.Erreur;
import fr.netapsys.abdou.repositories.EntrepriseRepository;
import fr.netapsys.abdou.repositories.ErreurRepository;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class MyCompositeJpaWriter<T> implements ItemWriter<T>{

 @Autowired EntrepriseRepository entrepRepo; 
 @Autowired ErreurRepository errRepo; 
 @Override   
 public void write(final List<? extends T> list) {  
    for (T t : list) {  
	 log.info("Writing : {}",t.toString());  
	 if ( t instanceof Entreprise ){
		 entrepRepo.save((Entreprise)t);  
	 }else if(t instanceof Erreur){
		 errRepo.save((Erreur)t);
	 }else{
	   throw new IllegalArgumentException(
					"instance  not yet implemented!");
	   }
	 }  
  }    
}

 

 

Bon nombre de briques liées au JPA sont définies ici et une classe entité hibernate Erreur. Voici donc ces classes:

 

Entité Erreur:

package fr.netapsys.abdou.model;
import javax.persistence.*;
import lombok.*;
@Data
@Entity
@ToString(exclude={"id","contenu"})
public class Erreur {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
    private String numLine;
    private Long id;
    private String contenu;
    private String typeException;
    private String stackTrace;   
    private String dateCrea;
}

 

Entreprise Repository:

package fr.netapsys.abdou.repositories;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import fr.netapsys.abdou.model.Entreprise;

@Repository
public interface EntrepriseRepository extends JpaRepository<Entreprise, Long> ,
 CrudRepository<Entreprise, Long> {
}

Erreur Repository

package fr.netapsys.abdou.repositories;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import fr.netapsys.abdou.model.Erreur;

@Repository
public interface ErreurRepository extends JpaRepository<Erreur, Long> , 
CrudRepository<Erreur, Long>  {
}

 

Controller & Run

Ouf! Enfin, nous arrivons à la partie exécution du batch.

Pour cela nous avons un moyen simple et rapide de lancer le batch c'est celui d'activer l'annotation @EnableBatchProcessing.

Mais nous préférons plutôt écrire un controller spring comme suit:

@RestController
@Slf4j
public class JobLauncherController {
 
    @Autowired JobLauncher jobLauncher;
 
    @Autowired
    Job job;
 
    @RequestMapping(name="/lancejob")
    public  Rapport handle() throws Exception {
    	JobParameters jobParameters=null;
    	JobExecution jobExec=null;
        try {
            jobParameters = new JobParametersBuilder()
            		.addLong("time", System.currentTimeMillis())
            		//.addLong("maxCount2Read", 2L)
            		//.addString("pathToFile", "entreps30.csv")
                    .toJobParameters();
            jobExec = jobLauncher.run(job, jobParameters);
        } catch (Exception e) {
            log.error(e.getMessage(),e);
        }
       
        Rapport rapport= new Rapport();        rapport.setRapportExec(
 jobExec.getExitStatus().toString()+ "\n\t"+
jobExec.getStepExecutions().iterator().
next().toString()); 
        return rapport;
     }
}

 

Et les imports nécessaires sont:

package fr.netapsys.abdou.controller;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import fr.netapsys.abdou.model.Rapport;
import lombok.extern.slf4j.Slf4j;

 

Le controller utilise un POJO nommé Rapport, ayant un seul attribut. Rapport est constitué de ces lignes:

package fr.netapsys.abdou.model;

import lombok.Data;

@Data
public class Rapport {

	private String rapportExec;
}

Il ne vous reste qu'à exécuter la commande maven:

    mvn spring-boot:run

Puis dans le navigateur saisir l'url:

    localhost:8080/lancejob

Pour voir s'afficher ce rapport (la capture ci-dessous avec postman ):

 

ENFIN, CONCLURE

C'est très dense comme blog et c'est donc naturellement difficile de conclure!

La démo présente un moyen efficace de réaliser ce qui est attendu d'un batch critique qui peut durer des heures la veille.

Un premier défi est relevé, celui de produire un rapport détaillé sur le déroulement du batch à tous les niveaux.

Un autre de décrire avec précision les erreurs produites par chaque étape du batch et de les enregistrer en base.

Enfin, les listeners permettent de produire un code maintenable facilement avec le principe 'speration of cocncerns'.

 

N'hésitez pas à laisser des commentaires en particulier si des cookies sont glissés dans le contenu.

 

NOTES.

 

@Suivre... d'autres aspects du puissant framework spring-batch qui est l'une des implémentations de référence de la JSR 352.

 

4 commentaires

  1. Pour être complet, dans le listener MyListenerItemProcessor cette classe doit être annotée ainsi:
    @Transactional(propagation=Propagation.REQUIRES_NEW)

    C’est un sujet à part sur lequel je reviendrai procainement.

  2. Merci pour ce super article, est il possible de mettre a dispoisition le code complet de cette demo

    Merci d’avance

  3. Merci @Ethanol pour votre commentaire
    Le zip du projet sera ajouté (inséré dans le billet) prochainement.

Laisser un commentaire

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

Captcha *