À la fin de ce TP, vous serez capable de :
;
Vous êtes développeur.euse Java au sein du service informatique d’une grande entreprise. Le département mainframe utilise une application COBOL de gestion des ressources humaines. Chaque nuit, votre application Java doit :
Le fichier s’appelle employes.csv et utilise ; comme séparateur.
employes.csv
En-tête (ligne 1) :
matricule;nom;prenom;service;poste;salaire;dateEmbauche;codeAgence;actif
Exemples de lignes :
EMP00001;Dupont;Jean;INFORMATIQUE;Développeur;45000.00;2018-03-15;AG001;O EMP00002;Martin;Marie;COMPTABILITE;Comptable;38500.50;2020-07-01;AG002;O EMP00003;Schmidt;Hans;MARKETING;Chef de projet;52000.00;2015-11-20;AG001;N EMP00004;;Pierre;RH;Chargé RH;31000.00;2022-01-10;AG003;O EMP00005;Leroy;Isabelle;INFORMATIQUE;Architecte;68000.00;2010-05-05;AG001;O
Note : Le fichier de test employes.csv fourni contient 200 lignes dont quelques lignes invalides (nom vide, matricule manquant, salaire non numérique).
Un enregistrement est invalide (à ignorer avec Skip) si :
yyyy-MM-dd
Un enregistrement avec actif = N doit être filtré (retourner null dans le processor) → il n’est pas écrit dans le fichier COBOL mais n’est pas compté comme une erreur.
actif = N
null
Le fichier de sortie s’appelle employes_cobol.dat.
employes_cobol.dat
+----------+---------+----------+---------+--------+---------+--------+-----------+------+ | Champ | Longueur| Type COBOL| Position| Règles | +----------+---------+----------+---------+---------------------------------------------+ | matricule| 8 | PIC X(8) | 1-8 | Compléter d'espaces à droite. Tronquer si > 8 | | nom | 25 | PIC X(25) | 9-33 | MAJUSCULES. Espaces à droite si < 25. | | prenom | 25 | PIC X(25) | 34-58 | 1ère lettre maj, reste minuscule. Espaces à droite. | | service | 20 | PIC X(20) | 59-78 | MAJUSCULES. Espaces à droite. | | poste | 30 | PIC X(30) | 79-108 | Conserver la casse. Espaces à droite. | | salaire | 10 | PIC 9(8)V99|109-118 | Entier sur 8 ch + centimes sur 2 ch, SANS virgule. Ex: 45000.00 → 0004500000 | | dateEmb | 8 | PIC X(8) |119-126 | Format AAAAMMJJ. Ex: 2018-03-15 → 20180315 | | codeAgence| 5 | PIC X(5) |127-131 | Espaces à droite. | +----------+---------+----------+---------+---------------------------------------------+ TOTAL : 131 caractères par enregistrement (pas de retour à la ligne)
Exemple d’enregistrement de sortie :
EMP00001 Dupont Jean INFORMATIQUE Développeur 0004500000201803150AG001
(131 caractères exactement)
Créez un projet Spring Boot avec Spring Initializr (start.spring.io) :
start.spring.io
com.formation
cobol-pipeline
application.properties
# JobRepository en mémoire (H2) spring.datasource.url=jdbc:h2:mem:batchdb;DB_CLOSE_DELAY=-1 spring.datasource.driver-class-name=org.h2.Driver spring.batch.jdbc.initialize-schema=always spring.batch.job.enabled=false # Chemins des fichiers (modifiables via JobParameters) batch.input.file=classpath:input/employes.csv batch.output.file=file:output/employes_cobol.dat batch.chunk.size=500 batch.skip.limit=50 logging.level.com.formation=DEBUG logging.level.org.springframework.batch=INFO
Créez src/main/resources/input/employes.csv avec au moins 200 lignes dont :
src/main/resources/input/employes.csv
ABC
actif=N
EmployeCsv
Classe représentant une ligne du fichier CSV d’entrée. Tous les champs sont des String (le processor se chargera des conversions) sauf que vous pouvez ajouter des getters/setters.
String
public class EmployeCsv { private String matricule; private String nom; private String prenom; private String service; private String poste; private String salaire; // String → sera converti en BigDecimal dans le processor private String dateEmbauche; // String → sera converti en LocalDate private String codeAgence; private String actif; // "O" ou "N" // getters, setters, toString... }
EmployeCobol
Classe représentant un enregistrement COBOL. Tous les champs sont des String avec la longueur exacte.
public class EmployeCobol { private String matricule; // 8 chars private String nom; // 25 chars private String prenom; // 25 chars private String service; // 20 chars private String poste; // 30 chars private String salaire; // 10 chars private String dateEmbauche; // 8 chars private String codeAgence; // 5 chars /** * Construit et retourne la ligne COBOL de 131 caractères. * Lance une IllegalStateException si la longueur n'est pas correcte. */ public String toLigneCobol() { String ligne = matricule + nom + prenom + service + poste + salaire + dateEmbauche + codeAgence; if (ligne.length() != 131) { throw new IllegalStateException( "Longueur incorrecte : " + ligne.length() + " (attendu 131) pour " + matricule ); } return ligne; } }
CobolFormatter
Créez CobolFormatter avec les méthodes statiques suivantes :
public final class CobolFormatter { // PIC X(n) : alphanumérique, complété d'espaces à droite public static String picX(String valeur, int longueur) { ... } // PIC 9(n) : numérique entier, complété de zéros à gauche public static String pic9(long valeur, int longueur) { ... } // PIC 9(n)V9(d) : numérique décimal, sans virgule // Ex: 45000.00, nEntier=8, nDec=2 → "0004500000" public static String pic9V(BigDecimal valeur, int nEntier, int nDecimales) { ... } // Date au format AAAAMMJJ public static String dateAaaaMmJj(LocalDate date) { ... } }
Dans votre classe BatchConfig, créez un bean FlatFileItemReader<EmployeCsv> :
BatchConfig
FlatFileItemReader<EmployeCsv>
@Bean
@StepScope
#{jobParameters['fichierEntree']}
linesToSkip(1)
Indice : Utilisez FlatFileItemReaderBuilder avec .delimited().delimiter(";").
FlatFileItemReaderBuilder
.delimited().delimiter(";")
EmployeValidationProcessor
Ce processor doit :
actif = "N"
matricule
ValidationException
nom
salaire
BigDecimal
dateEmbauche
LocalDate
prenom
service
// Créez votre propre exception de validation public class ValidationException extends RuntimeException { public ValidationException(String message) { super(message); } }
EmployeToCoblProcessor
Ce processor convertit un EmployeCsv (déjà validé et nettoyé) en EmployeCobol :
CobolFormatter.picX()
CobolFormatter.pic9V()
CobolFormatter.dateAaaaMmJj()
toLigneCobol()
Utilisez CompositeItemProcessor pour chaîner :
CompositeItemProcessor
Créez un bean FlatFileItemWriter<EmployeCobol> :
FlatFileItemWriter<EmployeCobol>
#{jobParameters['fichierSortie']}
ISO-8859-1
shouldDeleteIfExists(true)
lineAggregator
employe.toLigneCobol()
Dans BatchConfig, créez deux Steps :
Step 1 : etapeVerificationFichier (Tasklet)
etapeVerificationFichier
Step 2 : etapeTransformation (Chunk)
etapeTransformation
${batch.chunk.size:500}
employeCsvReader
compositeProcessor
employeCobolWriter
ValidationException.class
FlatFileParseException.class
${batch.skip.limit:50}
@Bean public Job cobolPipelineJob(Step etapeVerification, Step etapeTransformation, JobRapportListener rapportListener) { return new JobBuilder("cobolPipelineJob", jobRepository) .listener(rapportListener) .start(etapeVerification) .next(etapeTransformation) .build(); }
Créez EmployeSkipListener qui :
EmployeSkipListener
nbIgnores
JobRapportListener
Créez un JobExecutionListener qui affiche dans les logs à la fin du Job :
JobExecutionListener
╔══════════════════════════════════════════════════════╗ ║ RAPPORT D'EXÉCUTION - CobolPipeline ║ ╠══════════════════════════════════════════════════════╣ ║ Job : cobolPipelineJob ║ ║ Statut : COMPLETED ║ ║ Durée : 12.345 secondes ║ ╠══════════════════════════════════════════════════════╣ ║ STEP : etapeTransformation ║ ║ Lignes lues : 200 ║ ║ Lignes écrites : 183 ║ ║ Lignes filtrées : 7 (actif=N) ║ ║ Lignes ignorées : 10 (données invalides) ║ ╚══════════════════════════════════════════════════════╝
Indice : Dans afterJob(), itérez sur jobExecution.getStepExecutions() pour récupérer readCount, writeCount, skipCount de chaque step.
afterJob()
jobExecution.getStepExecutions()
readCount
writeCount
skipCount
Créez une classe CobolPipelineRunner qui implémente CommandLineRunner et lance le Job :
CobolPipelineRunner
CommandLineRunner
@Component public class CobolPipelineRunner implements CommandLineRunner { @Autowired private JobLauncher jobLauncher; @Autowired private Job cobolPipelineJob; @Value("${batch.input.file}") private String fichierEntree; @Value("${batch.output.file}") private String fichierSortie; @Override public void run(String... args) throws Exception { JobParameters params = new JobParametersBuilder() .addString("fichierEntree", fichierEntree) .addString("fichierSortie", fichierSortie) .addLong("timestamp", System.currentTimeMillis()) .toJobParameters(); JobExecution execution = jobLauncher.run(cobolPipelineJob, params); System.exit(execution.getStatus() == BatchStatus.COMPLETED ? 0 : 1); } }
Lancez l’application et vérifiez :
output/
Commande pour vérifier la longueur des lignes (Linux/Mac) :
awk 'length != 131 {print NR": longueur="length" → "$0}' output/employes_cobol.dat # Ne doit rien afficher si toutes les lignes font 131 chars
Activez le traitement multi-threadé sur le Step etapeTransformation avec 4 threads. N’oubliez pas de rendre le reader thread-safe avec SynchronizedItemStreamReader.
SynchronizedItemStreamReader
Comparez le temps d’exécution avec et sans multi-threading sur le fichier de 200 lignes. (L’effet sera plus visible avec un fichier de 100 000 lignes.)
Modifiez CobolPipelineRunner pour lire le chemin du fichier d’entrée depuis les arguments de la ligne de commande :
java -jar cobol-pipeline.jar --fichier=/data/employes_production.csv
Ajoutez un troisième Step qui lit le fichier COBOL produit et génère un fichier rapport.csv récapitulatif :
rapport.csv
matricule,nom,prenom,service,salaire_calcule EMP00001,DUPONT,Jean,INFORMATIQUE,45000.00 ...
Ajoutez dans le processor une vérification des doublons de matricule (en maintenant un Set<String> en mémoire). Si un matricule apparaît deux fois, ignorer la deuxième occurrence et la logger.
Set<String>
# Compter les lignes du fichier CSV (sans l'en-tête) wc -l output/employes_cobol.dat # Afficher les 5 premières lignes head -5 output/employes_cobol.dat # Afficher la longueur de chaque ligne awk '{print length, $0}' output/employes_cobol.dat | head -10 # Vérifier que toutes les lignes font 131 chars awk 'length != 131 {count++} END {print count+0 " lignes incorrectes"}' output/employes_cobol.dat
spring.batch.job.enabled=false
picX(valeur, n)
Bon courage et bons batchs !