Framework Spring-Batch 2.x : Une démo avancée (Part 2/2)

On présente ici, la suite des phases de la démo du billet précédent.
spring-mini-logo.png

On utilise le terme phase pour ne pas le confondre avec la notion étape utilisée dans spring-batch.

Pour rappel, dans la première partie de ce billet on a mis en place l'environnement nécessaire (base de données et projet maven/eclipse).
La phase 1, ci-dessous, va permettre de rentrer dans le vif du sujet.

PHASE 1: Configurer Spring
La configuration de Spring-Batch de la démo est répartie dans deux fichiers.
Leurs contenus seront donnés ci-dessous.
Détaillons cette configuration.

1.1- Déclarer le job nommé jdbc2FileJob,
Voir plus loin les détails en xml.

1.2- Déclarer une étape (step) du job
On déclare une étape (step) concrète, nommée stepJdbc2File, qui hérite de l'étape (step) abstraite, nommée abstractStep.
L'étape abstractStep ne sera jamais exécutée en tant que partie d'un job.
Le stepJdbc2File représente l'étape concrète du job.
Les beans jdbcReader, myProcessor et fileWriter permettent respectivement de lire, traiter et écrire les données.

1.3- Déclarer le bean reader nommé jdbcReader,
Le jdbcReader s'appuie sur la classe MyRowMapper à écrire par le développeur.

1.4- Déclarer le bean processor nommé myProcessor,
La classe MyProcessor est à écrire par le développeur.

1.5- Déclarer le bean writer nommé fileWriter,
Rien de bien compliqué.

1.6- Déclarer les beans dataSource & transactionManager.
Rien que du standard ici.

1.7- Déclarer le bean jobRepository qui utilise les beans dataSource et transactionManager,
Tout est classique.

1.8- Déclarer le bean jobLauncher qui s'appuie sur jobRepository,

Ces trois derniers beans sont regroupés dans le fichier db-jobrepo.xml.

Codes xml des deux fichiers de configuration de spring-batch

Voici le contenu complet du premier fichier de spring nommé springbatch-context.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:batch="http://www.springframework.org/schema/batch"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/batch 	
  http://www.springframework.org/schema/batch/spring-batch-2.1.xsd
  http://www.springframework.org/schema/beans 
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  http://www.springframework.org/schema/context 
  http://www.springframework.org/schema/context/spring-context-3.0.xsd">

 <context:component-scan base-package="fr.ach.tb" />
 <!-- Importer les definitions des jobs  -->
 <import resource="db-jobrepo.xml" />
 <!-- Le job utilise le chunk reader & writer -->
 <!-- ######## abstract step ########### -->
 <batch:step id="abstractStep" abstract="true">
   <batch:tasklet allow-start-if-complete="true" start-limit="7">
    <batch:chunk commit-interval="2" />
    </batch:tasklet>
 </batch:step>
<!-- JOB principal -->
 <batch:job id="jdbc2FileJob" restartable="true">
   <batch:step id="stepJdbc2File" parent="abstractStep">
    <batch:tasklet>
     <batch:chunk reader="jdbcReader" processor="myProcessor" 
                           writer="fileWriter"/>
    </batch:tasklet>
   </batch:step>
  </batch:job>
   <!--    itemProcessor       -->
   <bean id="myProcessor" 
        class="fr.ach.tb.springbatch.avances.MyProcessor"/>
   <!-- JDBC reader le bean nommé jdbcReader.    -->
   <bean id="jdbcReader"
	class="o.s.b.item.database.JdbcPagingItemReader">
     <property name="dataSource" ref="dataSource"/>
     <property name="queryProvider">
      <bean
	class="o.s.b.i.d.support.SqlPagingQueryProviderFactoryBean">
	<property name="dataSource" ref="dataSource" />
	<property name="selectClause" 
                 value="select id,nom,prenom,adresse"/>
	<property name="fromClause" value="from clients"/>
	<property name="sortKey" value="nom"/>
	</bean>
      </property>
      <property name="pageSize" value="10"/>
      <property name="rowMapper">
	<bean class="fr.ach.tb.springbatch.avances.MyRowMapper" />
      </property>
   </bean>
   <!--                   fileWriter                          -->
   <bean id="fileWriter" 
      class="o.s.b.item.file.FlatFileItemWriter">
      <property name="lineAggregator" ref="lineAggregator" />
      <property name="resource" value="file:output.txt" />
   </bean>
   <bean id="lineAggregator"
     class="o.s.b.item.file.transform.DelimitedLineAggregator">
     <property name="delimiter" value="|" />
   </bean>
</beans>

Et voici aussi l'extrait xml (entête omis) du second fichier db-jobrepo.xml

  <bean id="dataSource" 
    class="org.apache.commons.dbcp.BasicDataSource"
          destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/test"/>
    <property name="username" value="USER" /> 
    <property name="password" value="XXXX" />
  </bean>
  <bean id="transactionManager"
    class="o.s.jdbc.datasource.DataSourceTransactionManager"
             lazy-init="true">
   <property name="dataSource" ref="dataSource" />
  </bean>
  <bean id="jobRepository"
    class="o.s.b.core.repository.support.JobRepositoryFactoryBean">
   <property name="dataSource" ref="dataSource" />
   <property name="transactionManager" ref="transactionManager"/>
   <property name="databaseType" value="mysql" />
   <property name="tablePrefix" value="batch_" />
   </bean>
   <bean id="jobLauncher"
     class="o.s.b.core.launch.support.SimpleJobLauncher">
     <property name="jobRepository" ref="jobRepository"/>
   </bean>

Penser à compléter les packages annotés en abrégé. Par exemple,
le package o.s.b.i.d désigne org.springframework.batch.item.database
Et penser à remplacer les propriétés url, username et password de la dataSource.

Explications:

Le fichier de spring springbatch-context.xml commence par préciser le package à scanner pour des beans à auto-injecter.
Puis, il importe les beans déclarés dans db-jobrepo.xml.
Ensuite, Le step (étape) abstrait(e) du job ne sert qu'à mutualiser les déclarations communes.

Arrive ensuite la déclaration principale du job de cette démo qui est jdbc2FileJob.
Ce job est défini comme suit:

<batch:job id="jdbc2FileJob" restartable="true">
 <batch:step id="stepJdbc2File" parent="abstractStep">
  <batch:tasklet>
    <batch:chunk reader="jdbcReader" processor="myProcessor"
                          writer="fileWriter"/>
   </batch:tasklet>
 </batch:step>
</batch:job>

Ainsi, comme vous le voyez, la seule étape (step) concrète de ce job est nommée stepJdbc2File et a pour parent abstractStep.
L'étape comporte un itemReader, un itemProcessor et un itemWriter.
Le premier est un JdbcReader et sert à extraire les données de la table "CLIENTS".
L'itemProcessor permet de transformer ces données (métier). MyProcessor est la première classe à écrire.
Enfin, l'itemWriter envoie ces données transformées et formatées dans un fichier output.txt.

La déclaration xml du JdbcReader est :

<bean id="jdbcReader" 
    class="o.s.b.i.database.JdbcPagingItemReader">
   <property name="dataSource" ref="dataSource" />
   <property name="queryProvider">
     <bean 
        class="o.s.b.i.d.SqlPagingQueryProviderFactoryBean">
	<property name="dataSource" ref="dataSource" />
	<property name="selectClause" 
                      value="select id,nom,prenom,adresse"/>
	<property name="fromClause" value="from clients" />
	<property name="sortKey" value="nom" />
     </bean>
     </property>
     <property name="rowMapper">
	<bean class="fr.ach.tb.springbatch.avances.MyRowMapper" />
     </property>
     <property name="pageSize" value="10" />
</bean>

L'implémentation JdbcPagingItemReader d'itemReader est utilisée. Celle-ci exige la présence de SqlPagingQueryProviderFactoryBean.
Cette factory nécessite de définir la propriété "queryProvider" qui doit renseigner les selectClause et fromClause pour permettre d'extraire les données.
On peut rajouter au besoin ''whereClause" paramétré.
Enfin, la propriété "rowMapper" permet de mapper chaque enregistrement extrait en un objet java de type Personne.
La classe de mapping MyRowMapper est la seconde (et la dernière) classe de la démo à écrire par le développeur.

Le second fichier db-jobrepo.xm déclare une dataSource, transactionManager, jobRepository et jobLauncher.
Sachez que toutes ces déclarations sont utiles bien que le principe "convention over configuration" permet de réduire la verbosité xml.

Or ce qui important dans cette configuration est ces deux points:
- La présence du <tasklet:chunk permet le traitement par lot.
Justement, la propriété commit-interval définit la taille du lot traité par le jdbcReader.

- L'autre point important est la classe SqlPagingQueryProviderFactoryBean qui est une implémentation de ItemReader.
Celle-ci, via la propriété pageSize, donne la taille de l'extraction des données depuis la base source.

A noter que les deux points ne font pas la même chose!
Bien que tous les deux participent à obtenir des performances optimales.

PHASE 2: Ecrire les deux classes métier: MyRowMapper puis MyProcessor

La classe MyRowMapper

public class MyRowMapper implements RowMapper<Personne>{
 public Personne mapRow(ResultSet rs, int nb) 
        throws SQLException {		
 	Personne client=new Personne();
	client.setId(rs.getInt("id"));
	client.setNom(rs.getString("nom"));
	client.setPrenom(rs.getString("prenom"));
	client.setAdresse(rs.getString("adresse"));
	return client;
  }
}

MyRowMapper implémente le (mono) interface RowMapper de Spring ayant une seule méthode "mapRow".
La méthode mapRow convertit chaque enregistrement en un objet Personne.
Le POJO Personne a les attributs id (int), nom, prénom, adresse (String).

La classe MyProcessor

public class MyProcessor implements ItemProcessor<Personne, String> {
  public String process(Personne pers) throws Exception {
   return pers.getId()+","+pers.getPrenom()+","+pers.getNom();
  }
}

Cette classe implémente le mono interface ItemProcessor ayant une seule méthode "process".
La méthode process renvoie dans notre démo une chaîne simulant la transformation métier utile.
Rappelons que c'est le code de cette méthode qui est exécuté après le reade et avant le writer.

PHASE3 : Test unitaire JUnit

Voici le code du test unitaire qui lance notre job:

import static org.junit.Assert.assertNotNull;
import java.util.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.*;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@ContextConfiguration(locations={"/springbatch-context.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class TestJobConfigurationTests {
  @Autowired	private JobLauncher jobLauncher;
  @Autowired	private Job jdbc2FileJob;	
  @Test
  public void testLaunchJob() throws Exception {
    JobParameter jp=new JobParameter(new Date());
    Map<String,JobParameter> map=new HashMap<String, JobParameter>();
    map.put("param1", jp);
    JobParameters jobparams=new JobParameters(map);
    jobLauncher.run(jdbc2FileJob, jobparams);
  }	
}

Pour exécuter le test, lancer la commande dos:

               mvn test

Explications:
Les beans jobLauncher et jdbc2FileJob sont auto-injectés par spring.
Une Map est créée pour ajouter un paramètre pour le job qui indique la date du jour.
Enfin, la ligne jobLauncher.run(jdbc2FileJob, jobparams) lance effectivement le job.

L'exécution du test JUnit produit la sortie output.txt contenant les enregistrements de la table Clients'.
On peut trouver aussi dans les tables préfixées par batch_ les informations (rapports) sur ce job.
Par exemple, dans la table batch_job_execution les rapports sur toutes les exécutions de ce même job.
Dans la colonne EXIT_CODE vous avez soit COMPLETED lorsque le job est lancé avec succès, soit FAILED sinon.
Pour des besoins spécifiques, d'autres valeurs de EXIT_CODE peuvent être ajoutées.

CONCLUSION:

Essentiellement, en trois étapes, avec peu de code, nous avons réalisé un job qui lit les données depuis une base puis les transforme et enfin les stocke dans un fichier.
Le code écrit est du pure métier pour réaliser les transformations nécessaires.
Et tout le reste c'est du code infrastructure ou spring batch core fournis par spring.
On pourrait définir une dataSource pour la persistance des jobs et une autre pour les données à extraire.
Pour cela il suffit déclarer deux dataSource et de faire pointer correctement le jobRepository.

Pour conclure, vous constatez que Spring-Batch offre un cadre et un vocabulaire (Domain Language) afin de:

  • Réaliser le principe de separation of concerns,
  • Réaliser une architecture en couches (tiers) bien clairement délimitées en fournissant les interfaces utiles,
  • Offrir de simples (ou par défaut) implémentations pour simplifier la prise en main du framework,
  • Faciliter l'extensibilité, la testabilité et la maintenance,

Spring-Batch renforce donc la productivité en réduisant la quantité de code à produire (et les bugs liés).
Et les tests de chaque tiers sont facilités avec un gain manifeste en visibilité et en maintenabilité.
Les performances peuvent être optimisées.
La démo l'a illustré avec JdbcPagingItemReader et pageSize ainsi que le chunk processing et commit-interval.
En fonction du contexte, on peut utiliser d'autres techniques afin d'obtenir de meilleures performances (ex. utilisation des curseurs de la base).

Un commentaire

Laisser un commentaire

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

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.