Spring Batch est un framework de traitement par lots léger et complet, faisant partie de l'écosystème Spring. Il est apprécié pour sa flexibilité, sa facilité d'utilisation et sa robustesse en production. Il simplifie grandement la gestion de tâches telles que le traitement efficace de grands volumes de données ou l'exécution planifiée de traitements massifs. Son efficacité est décuplée lorsqu'il est combiné à un framework d'ordonnancement.
Architecture de Spring Batch
L'architecture de Spring Batch est divisée en trois couches principales :
- Couche Application : Contient les jobs et le code personnalisé développé par l'utilisateur, définissant la logique métier et les flux de traitement spécifiques au projet.
- Couche Cœur (Batch Core) : Fournit l'environnement d'exécution nécessaire au lancement et à la gestion des jobs, incluant des composants comme
JobLauncher. - Couche Infrastructure (Batch Enfrastructure) : Sert de fondation aux deux autres couches. Elle comprend les composants essentiels tels que les lecteurs (Reader), les écrivains (Writer) et le framework de reprise sur erreur (Retry).
Concepts Clés
La compréhension des concepts suivants est essentielle pour le développement et le débogage avec Spring Batch :
JobRepository
Ce composant est responsable de l'interaction avec la base de données pour enregistrer l'état des exécutions de jobs (création, mise à jour, statut). Spring Batch nécessite donc une base de données pour gérer son cycle de vie.
JobLauncher
Le JobLauncher est l'interface permettant de démarrer l'exécution d'un Job.
Job
Un Job encapsule l'intégralité d'un processus de traitement par lots. Lancer un traitement, c'est lancer un Job.
Job: Définit la structure et la logique d'un traitement.JobInstance: Une instance spécifique d'exécution d'unJob. Différentes instances d'un mêmeJobpeuvent être lancées avec des paramètres distincts.JobParameters: Les paramètres associés à uneJobInstance.JobExecution: Représente une exécution concrète d'unJob, qui peut réussir ou échouer.
Le rôle du développeur est donc de définir ces Jobs.
Step
Un Step représente une unité de travail au sein d'un Job. Un Job peut être composé d'un ou plusieurs Steps exécutés séquentiellement selon une logique définie. La définition de Steps permet de construire des logiques métier complexes de manière modulaire.
Modèle Lecture-Traitement-Écriture (Item Reader, Item Processor, Item Writer)
La méthode la plus courante pour définir un Step repose sur le modèle : lire des données (ItemReader), les traiter (ItemProcessor), puis les écrire (ItemWriter). Par exemple, un ItemReader peut lire des données depuis un fichier, un ItemProcessor peut effectuer des transformations métier, et un ItemWriter peut persister les données dans une base de données. Spring Batch propose de nombreux readers et writers prêts à l'emploi.
Exemple de Code : Traitement de Fichiers CSV
Illustrons ces concepts avec un exemple concret : lire des données depuis plusieurs fichiers CSV, les traiter, puis les écrire dans un fichier CSV de sortie.
Configuration de Base
Ajoutez les dépendances nécessaires dans votre fichier pom.xml :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Utiliser H2 comme base de données en mémoire est pratique pour le développement. Pour la production, une base de données externe comme Oracle ou PostgreSQL est recommandée.
La classe principale de l'application Spring Boot nécessite l'annotation @EnableBatchProcessing :
@SpringBootApplication
@EnableBatchProcessing
public class PkslowBatchJobMain {
public static void main(String[] args) {
SpringApplication.run(PkslowBatchJobMain.class, args);
}
}
Définissons une classe d'entité simple pour représenter les données :
package com.pkslow.batch.entity;
public class Employee {
private String id;
private String firstName;
private String lastName;
// Getters and setters are assumed to be present
}
Voici un exemple de contenu pour un fichier CSV d'entrée :
id,firstName,lastName
1,Lokesh,Gupta
2,Amit,Mishra
3,Pankaj,Kumar
4,David,Miller
Implémentation du Modèle Lecture-Traitement-Écriture
ItemReader
Pour lire à partir de plusieurs fichiers CSV, nous utilisons MultiResourceItemReader :
@Value("input/inputData*.csv") // Charge les ressources correspondant au pattern
private Resource[] inputResources;
@Bean
public MultiResourceItemReader<Employee> multiResourceItemReader() {
MultiResourceItemReader<Employee> resourceItemReader = new MultiResourceItemReader<Employee>();
resourceItemReader.setResources(inputResources);
resourceItemReader.setDelegate(flatFileItemReader()); // Définit le lecteur délégué pour chaque ressource
return resourceItemReader;
}
@Bean
public FlatFileItemReader<Employee> flatFileItemReader() {
FlatFileItemReader<Employee> reader = new FlatFileItemReader<Employee>();
reader.setLinesToSkip(1); // Ignore la ligne d'en-tête
// Mapper pour analyser chaque ligne CSV
reader.setLineMapper(new DefaultLineMapper<Employee>() {
{
setLineTokenizer(new DelimitedLineTokenizer() {
{
setNames(new String[] { "id", "firstName", "lastName" }); // Noms des colonnes
}
});
setFieldSetMapper(new BeanWrapperFieldSetMapper<Employee>() {
{
setTargetType(Employee.class); // Classe cible pour le mapping
}
});
}
});
return reader;
}
ItemProcessor
Pour cet exemple simple, le processeur convertit la colonne lastName en majuscules :
@Bean
public ItemProcessor<Employee, Employee> employeeItemProcessor() {
return employee -> {
employee.setLastName(employee.getLastName().toUpperCase());
return employee;
};
}
ItemWriter
L'écrivain enregistre les données traitées dans un fichier CSV de sortie :
@Value("output/outputData.csv")
private Resource outputResource;
@Bean
public FlatFileItemWriter<Employee> csvItemWriter() {
FlatFileItemWriter<Employee> writer = new FlatFileItemWriter<Employee>();
writer.setResource(outputResource);
writer.setAppendAllowed(true); // Permet d'ajouter au fichier existant
// Agrégateur pour formater chaque ligne écrite
writer.setLineAggregator(new DelimitedLineAggregator<Employee>() {
{
setDelimiter(","); // Séparateur de champs
setFieldExtractor(new BeanWrapperFieldExtractor<Employee>() {
{
setNames(new String[] { "id", "firstName", "lastName" }); // Champs à extraire
}
});
}
});
return writer;
}
Définition du Step
Une fois le lecteur, le processeur et l'écrivain définis, on peut créer le Step :
@Autowired
private StepBuilderFactory stepBuilderFactory; // Injection du StepBuilderFactory
@Bean
public Step processCsvFileStep() {
return stepBuilderFactory.get("processCsvFileStep")
.<Employee, Employee>chunk(5) // Traite par lots de 5 enregistrements
.reader(multiResourceItemReader())
.processor(employeeItemProcessor())
.writer(csvItemWriter())
.build();
}
L'option chunk(5) indique que les opérations d'écriture seront effectuées après le traitement de 5 enregistrements.
Définition du Job
L'assemblage des Steps forme le Job :
@Autowired
private JobBuilderFactory jobBuilderFactory; // Injection du JobBuilderFactory
@Bean
public Job pkslowCsvProcessingJob() {
return jobBuilderFactory.get("pkslowCsvProcessingJob")
.incrementer(new RunIdIncrementer()) // Ajoute un incrément pour chaque exécution
.start(processCsvFileStep())
.build();
}
Exécution
Après avoir exécuté l'application, les données seront lues depuis les fichiers d'entrée, le lastName sera converti en majuscules, et le résultat sera écrit dans outputData.csv.
Listeners
Les Listeners permettent de réagir à des événements spécifiques du cycle de vie des opérations de lecture, traitement et écriture. Par exemple, on peut enregistrer les erreurs ou notifier d'autres systèmes après une écriture réussie.
Les interfaces correspondantes sont ItemReadListener, ItemProcessListener et ItemWriteListener. Voici un exemple d'implémentation pour ItemWriteListener :
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.ItemWriteListener;
import java.util.List;
public class PkslowWriteListener implements ItemWriteListener<Employee> {
private static final Log logger = LogFactory.getLog(PkslowWriteListener.class);
@Override
public void beforeWrite(List<? extends Employee> list) {
logger.info("Avant écriture : " + list);
}
@Override
public void afterWrite(List<? extends Employee> list) {
logger.info("Après écriture : " + list);
}
@Override
public void onWriteError(Exception e, List<? extends Employee> list) {
logger.error("Erreur lors de l'écriture : " + list, e);
}
}
Pour intégrer un listener au Step :
@Bean
public Step processCsvFileStep() {
return stepBuilderFactory.get("processCsvFileStep")
.<Employee, Employee>chunk(5)
.reader(multiResourceItemReader())
.listener(new PkslowReadListener()) // Exemple d'ajout d'un listener de lecture
.processor(employeeItemProcessor())
.listener(new PkslowProcessListener()) // Exemple d'ajout d'un listener de traitement
.writer(csvItemWriter())
.listener(new PkslowWriteListener()) // Ajout du listener d'écriture
.build();
}
L'exécution du job affichera les logs des listeners, démontrant le traitement par lots et l'efficacité du mécanisme de chunking pour réduire la pression sur les opérations d'I/O.