Spring Batch est un framework pour développer des Batch en JAVA. Un Batch peut être défini comme un traitement par lot d’une grande quantité de données.
Les concepts :
Job : Il s’agit ici du traitement proprement dit du Batch. Batch et Job sont deux concepts identiques. On peut citer un exemple (donneesQuotidiennes) : un batch quotidien permettant de lire, modifier et écrire des données provenant d’un fichier plat vers une base de données). Ce type de Batch peut être lancé de façon manuelle ou par de programme comme cron ou Quartz.
Step (Étape) : Un Job (Batch) est constitué d’un ensemble d’étapes (Step). Dans l’exemple du Batch donneesQuotidiennes, une étape peut être la lecture de données, une deuxième étape la transformation de données et enfin une dernière étape l’écriture de données.
JobInstance : Pour gérer l’historique d’un Job, on parle de JobInstance. Nous pouvons ainsi parler du Job du 15 Septembre pour le Batch donneesQuotidiennes, ou du 16 Octobre, …Cela permet d’identifier un Job et de garder son historique afin de pouvoir revenir en arrière en cas de problème. L’identifiant d’un Job peut être passé comme paramètre appelé JobParameters.
JobExecution : Lorsqu’on lance un Batch, des erreurs peuvent survenir. Dans ce cas, il faut relancer le Batch. Ce processus est à répéter jusqu’à ce que le Batch s’exécute correctement. Chaque instance d’exécution (lancement) d’un Batch est appelée JobExecution. Un Batch peut donc avoir plusieurs JobExecution (autant de fois que l’exécution du Batch échoue) ou une seule (si l’exécution du Batch s’effectue correctement à la première fois).
Pour résumer :
- Job (Batch) = ensemble de Step (étape) : lire, transformer, écrire.
- JobInstance = Job + identifiant (peut être une date, l’heure, tout ce qui permet d’identifier de façon unique un Job) : Job du 15 Septembre, du 12 Octobre, etc.
- L’identifiant d’un Job peut être construit à partir des informations fournies comme paramètre lors de son lancement. Ces paramètres sont appelés JobParameters.
- L’instance d’exécution d’un Job est appelée JobExecution.
Configuration d’un Batch
La configuration d’un Batch (Job) se fait dans un fichier XML Spring. Un Job est un simple Bean Spring avec des propriétés spécifiques. L’exemple suivant définit un un Job avec une suite d’étapes : lecture de données d’un fichier, modification et écriture dans une base de données.
<job id="jobReadProcessorWriterPeronn" >
<step id="idStep">
<tasklet>
<chunk
reader="personFileItemReader"
processor="personProcessor"
writer="personWriterDB"
commit-interval="5"/>
</tasklet>
</step>
</job>
Le Reader lit une source de données :
Le Reader est appelé successivement au sein d’une étape et retourne des objets du type pour lequel il est défini (Person dans notre cas) :
<beans:bean id="personFileItemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
.....
</beans:bean>
Le Processor modifie les données lues par le Reader :
Contrairement à un Reader, les implémentations d’un Processor sont plutôt écrites par le développeur de batch, car elles
correspondent à des besoins fonctionnels.
<beans:bean id="personProcessor">
....
</beans:bean>
Le Writer écrit les données provenant du processor (ou directement lues par le Reader) :
<beans:bean id="personWriterDB" class="com.oxiane.sb.service.PersonWriter">
.....
</beans:bean>
Sans vouloir rentrer dans les détails, l’exemple suivant montre la simplicité qu’offre Spring Batch pour effectuer la lecture de données d’un fichier plat. Pour cet exemple, nous disposons d’un fichier contenant un ensemble de personnes. Chaque ligne identifie une personne : son nom, son prénom et sa date de naissance.
larbo,Fird,2010-11-29
cissé,Mayé,1901-01-15
dupon,Jean,1903-10-20
Julle,Jodat,1909-12-19
R,JL,1921-08-21
Maiga,Zali,2009-11-29
Jadi,Jade,1910-11-29
Nous utilisons une implémentation de l’interface ItemReader pour lire le fichier. Différentes implémentations sont fournies permettant de gérer facilement les accès fichiers, JDBC, Hibernate, JPA…
Notre tâche va être principalement de la configuration et le développement d’une seule classe applicative dédiée. Nous utilisons la classe FlatFileItemReader (une implémentation ItemReader) pour d’effectuer la lecture du fichier indiqué par la propriété resource. Nous nous débarrassons ainsi de la gestion des opérations d’entrées/sorties qui sont particulièrement pénibles et propice à des erreurs. La classe FlatFileItemReader fournit, pour chaque ligne du fichier d’entrée, une représentation objet (Person dans notre cas) correspondante qui pourra par exemple être sauvegardé en base.
<beans:bean id="personFileItemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
<beans:property name="resource" value="classpath:persons.csv" />
<beans:property name="lineMapper">
<beans:bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<beans:property name="lineTokenizer">
<beans:bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
<beans:property name="names" value="nom,prenom,dateNaissance" />
</beans:bean>
</beans:property>
<beans:property name="fieldSetMapper">
<beans:bean class="com.oxiane.sb.service.PersonFieldSetMapperService" />
</beans:property>
</beans:bean>
</beans:property>
</beans:bean>
La partie configuration va fournir des informations suivantes : le fichier à lire, la classe utilisée pour lire le fichier, le format de données dans le fichier et enfin une classe qui implémente la logique de transformation de chaque ligne (l’instanciation d’une entité Person à partir d’une ligne du fichier).
package com.oxiane.sb.data;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
public class Person {
/**
* identifiant
*/
private Long id;
/**
* le nom de la personne
*/
private String nom;
/**
* prenom de la personne
*/
private String prenom;
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public String getPrenom() {
return prenom;
}
public void setPrenom(String prenom) {
this.prenom = prenom;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
FlatFileItemReader utilise la classe DefaultLineMapper qui utilise à son tour la classe DelimitedLineTokenizer. Le rôle de DelimitedLineTokenizer est de décomposer chaque ligne dans un objet FieldSet. La propriété names donne le format de l’entête du fichier et permet d’identifier les données de chaque ligne. Cette propriété names est utilisée par la classe d’implantation de transformation de données en objet métier à travers l’objet FieldSet. Il s’agit de la classe indiquée par la propriété fieldSetMapper ( PersonFieldSetMapperService).
package com.oxiane.sb.service;
import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.transform.FieldSet;
import com.oxiane.sb.data.Person;
public class PersonFieldSetMapperService implements FieldSetMapper{
public Person mapFieldSet(FieldSet fs) {
// TODO Auto-generated method stub
Person person = new Person();
person.setNom(fs.readString("nom"));
person.setPrenom(fs.readString("prenom"));
person.setDateNaissance(fs.readDate("dateNaissance"));
return person;
}
}
Les méthodes de l’objet FieldSet : readString(« .. »), readDate(« … »), readBooelan(« … »), prennent comme paramètre, les éléments de la propriété names (on peut par exemple utiliser la méthode readDate(« dateNaissance ») pour lire la date d’une ligne du fichier). Il faut remarquer ici qu’on a plus besoin de conversion de type.
Utilisation de JDBC, Hibernate
Pour finir, je vous propose d’utiliser JDBC et Hibernate pour lire les données depuis une base de données ….
Comme pour la lecture d’un fichier, tout passe par une configuration xml :
JDBC :
<beans:bean id="personJdbcItemReader" class="org.springframework.batch.item.database.JdbcCursorItemReader">
<beans:property name="dataSource" ref="dataSource"/>
<beans:property name="sql" value="select * from person"/>
<beans:property name="rowMapper" ref="personRowMapperService"/>
</beans:bean>
<beans:bean id="personRowMapperService" class="com.oxiane.sb.service.PersonRowMapperService"/>
La classe permettant d’instancier le modèle fonctionnel :
package com.oxiane.sb.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.RowMapper;
import com.oxiane.sb.data.Person;
public class PersonRowMapperService implements RowMapper{
public Person mapRow(ResultSet resultSet, int rowNumber) throws
SQLException {
Person person = new Person();
person.setNom(resultSet.readString("nom"));
person.setPrenom(resultSet.readString("prenom"));
person.setDateNaissance(resultSet.readDate("dateNaissance"));
return person;
}
}
Hibernate
<beans:bean id="personHibernateItemReader" class="org.springframework.batch.item.database.HibernateCursorItemReader">
<beans:property name="sessionFactory" ref="sessionFactory"/>
<beans:property name="queryString" value="from person"/>
</beans:bean>
On peut noter ici la propriété queryString du Reader HibernateCursorItemReader qui indique la requête à exécuter from person