Spring-Batch : par quel bout le prendre ?

SpringSpring-Batch répond à un besoin récurrent : la gestion des programmes batchs écrits en Java. Si le framework semble de plus en plus complet et fonctionnel, celui-ci souffre de sa complexité de configuration et reste un peu difficile d'accès malgré les efforts de l'équipe de développement. Personnellement, j'ai passé quelques heures pour faire fonctionner mon premier batch. Les exemples fournis fonctionnent rapidement, et illustrent très bien les possibilités qu'offre Spring-Batch. Mais, comme ces possibilités sont très riches, les exemples sont nombreux et (nécessairement) complexes. On lit la documentation, on regarde les multiples exemples en détail, et au moment d'implémenter notre premier batch et de plonger pleinement dans le cœur du sujet, on se pose la question "Mais par quel bout je commence ?".

Donc, pour permettre aux gens qui, comme moi, aiment bien créer leur "hello-world" afin de bien comprendre ce qu'ils utilisent, voici un exemple minimal d'un projet Spring-Batch.

Pourquoi Spring-Batch ?

Avant de vous expliquer comment commencer avec Spring-Batch, je vous propose un petit rappel :

Spring-Batch a pour vocation de fournir un framework robuste permettant de développer plus facilement des programmes batch en Java. En effet, jusqu'à très récemment, il n'existait aucune solution open source sur laquelle s'appuyer quand on voulait faire un tel programme. Il fallait donc utiliser diverses API pour réaliser les fonctionnalités de base de son batch - par ex. lire / écrire dans des bases de données, manipuler des fichiers texte etc. Typiquement, on commençait par écrire un bon vieux programme Java avec sa méthode "public static void main(String args){}"... Ce qui d'ailleurs peut être une bonne solution pour des besoins simples et ponctuels.

Alors, quand passer à Spring-Batch ? Par exemple, dès que l'on rencontre l'une des problématiques suivantes :

  • Traitement "par lot" pour éviter par exemple de charger en mémoire l'ensemble des données traitées par le batch. Ce type de fonctionnement est adapté à des traitements sur de gros volumes de données. De plus, Spring-batch fournit des implémentations de classes permettant de lire ou d'écrire par lot sur divers types de supports (SQL, fichier plat ; etc.), ce qui évite de réinventer la roue...

  • Gestion des transactions : Spring-batch s'appuie sur la gestion des transactions fournie par Spring, et permet donc de gérer de façon déclarative les transactions dans vos batchs.

  • Gestion de la reprise sur erreur, encore une fonctionnalité que le framework vous aide fortement à mettre en oeuvre.

  • Utilisation de Spring : le développeur qui a l'habitude de Spring peut réutiliser facilement ses notions ainsi que les composants de ce framework tels que, par exemple, les JdbcTemplates ou encore l'intégration à Hibernate...

  • Cadre de développement : à mon sens, un des apports les plus fondamentaux de Spring-batch est de proposer un cadre de développement autour de notions communes comme par exemple Job, Step, ItemWriter etc, ce qui aide beaucoup à la maintenabilité du code des batchs : un développeur qui doit maintenir différents batchs peut passer de l'un à l'autre, le logiciel est organisé autour des mêmes classes et interfaces.

A noter également que l'architecture d'un batch réalisé avec Spring-batch permet de le rendre facilement testable par un harnais de tests unitaires automatisés. Il est notamment relativement simple, grâce à Spring, de remplacer certaines classes par des bouchons, ou encore de travailler sur une base de données en mémoire HSQLDB au lieu de la "vraie" base.

Il y a bien d'autres raisons d'utiliser Spring-batch et je vous invite à aller lire la documentation officielle ou vous trouverez de nombreuses informations sur les possibilités du framework.

Maintenant, attaquons notre premier batch...

Initialisation du projet

Dépendances Maven

J'utilise Maven 2 pour construire mon projet : l'ajout de Spring-batch à mon projet se résume alors aux dépendances à ajouter dans le fichier pom.xml :

<dependency>
 <groupId>org.springframework.batch</groupId>
 <artifactId>spring-batch-core</artifactId>
 <version>1.1.0.RELEASE</version>
</dependency>
<dependency>
 <groupId>org.springframework.batch</groupId>
 <artifactId>spring-batch-infrastructure</artifactId>
 <version>1.1.0.RELEASE</version>
</dependency>

Aux dépendances Spring-batch, il faut ajouter, pour cet exemple, quelques dépendances Spring. Pour plus de concision, j'ajoute la dépendance globale vers Spring, mais on pourra être plus rigoureux par la suite et n'ajouter que les modules nécessaires à notre projet.

<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring</artifactId>
 <version>2.5.5</version>
</dependency>

La classe métier Person

Notre exemple est un batch qui va effectuer un traitement sur des personnes. Pour modéliser cette donnée, j'utilise un simple POJO :

public class Person {
  private Long id;
  private String name;
  // ... getters & setters

  @Override
  public String toString() {
    return "Person:id="+id+",name="+name;
  }

  public Person(Long id, String name) {
    super();
    this.id = id;
    this.name = name;
  }
}

Implémentation du batch

L'objectif est d'avoir un batch qui lit un ensemble de personnes dans une source de données et traite ces données. Dans cet exemple, nous allons lire un ensemble de données dans un tableau statique. Le traitement consistera simplement à écrire ces objets dans la sortie standard.

Pour comprendre ce que nous codons, nous avons besoin d'introduire les notions suivantes, qui ne sont qu'une partie des notions de Spring-batch :

Un batch correspond à une classe de type Jobs. Ces Jobs contiennent une séquence de Steps. Un Step peut être de type item-oriented ou tasklet. Dans notre cas, nous souhaitons traiter un ensemble de personnes par lot : le type de step adapté est donc item-oriented. Un step de ce type a besoin de 2 objets : un ItemReader et un ItemWriter.

Ces concepts sont très importants dans Spring-batch. Je ne les détaille pas tous ici, car ce n'est pas l'objet de cet article. Je vous recommande d'aller lire cette page qui les détaille : http://static.springsource.org/spring-batch/1.0.x/spring-batch-docs/reference/html/core.html

Spring-batch fournit une implémentation par défaut de Job et de Step. Les seules choses que nous allons coder ici sont l'ItemReader et l'ItemWriter de notre classe Person : en effet, pour exécuter le step, Spring-batch a besoin de savoir comment lire une personne et comment l'écrire.

A noter que Spring-batch vient avec des ItemReaders et ItemWriters préconçus. Exemple : fichier plat, XML, requête SQL, etc. Voir ici : http://static.springframework.org/spring-batch/1.0.x/spring-batch-docs/reference/html/spring-batch-infrastructure.html

L'ItemReader

Cette classe est donc chargée de récupérer les données. Dans notre cas, on lit simplement une variable statique. La méthode principale est donc la méthode read qui a pour rôle de lire un morceau unitaire de donnée (l'item). Les méthodes mark et reset permettent respectivement de mémoriser une position dans le flux de données (ici : l'ensemble des personnes) et de revenir à cette position.

public class PersonReader implements ItemReader {
  static Person personArray = new Person[100];
  static {
    for (int i = 0; i < 100; i++) {
      personArray[i] = new Person(((Integer) i).longValue(), "name"+i);
    }
  }

  static int readIndex = -1;

  public void mark() throws MarkFailedException {
    readIndex++;
  }

  public Object read() throws UnexpectedInputException, NoWorkFoundException, ParseException {
    if (readIndex>=personArray.length) {
      return null;
    }
    return personArray[readIndex];
  }

  public void reset() throws ResetFailedException {
  }
}

L'ItemWriter

Cette classe définit comment écrire l'objet que l'on nous passe. En clair, on répond ici à la question "Que fait-on avec chaque personne ?", ceci par la méthode write. La méthode clear permet d'effacer le contenu du buffer d'écriture, et flush permet de provoquer immédiatement l'écriture du buffer.

public class PersonConsoleWriter implements ItemWriter {

   private StringBuilder sb = new StringBuilder();

   public void clear() throws ClearFailedException {
     sb = new StringBuilder();
   }

   public void flush() throws FlushFailedException {
     System.out.print(sb);
     sb = new StringBuilder();
   }

   public void write(Object o) throws Exception {
     Person person = (Person) o;
     sb.append(person.toString()+"n");
   }
 }

Nous avons donc nos deux classes composant notre batch, on peut donc s'intéresser à la partie la plus compliquée de Spring-batch : la configuration.

La configuration

La configuration se fait dans un fichier XML Spring classique que nous avons appelé ici : batch-sample-context.xml.

Tout d'abord, il faut configurer nos deux beans correspondant au reader et au writer :

Ensuite, il faut configurer le composant qui permet de lancer un batch, le "jobLauncher". La façon la plus simple d'instancier cet objet est d'utiliser la classe SimpleJobLauncher :

Simple, mais on voit que l'on a besoin d'un "jobRepository" qui permet de suivre et de reprendre l'avancement des taches. L'utilisation de la classe MapJobRepositoryFactoryBean permet encore un fois d'avoir une configuration réduite, mais vous serez amené à utiliser d'autres formes de repository dès lors que vous voudrez utiliser des fonctionnalités de Spring-batch telles que la possibilité de rejouer un batch, ou bien de pouvoir conserver l'historique de façon persistante afin de tracer quel jobs et quels steps se sont exécutés, quand, et avec quel code retour (OK, KO...).

On voit que l'on a besoin d'un transaction manager. Cette propriété est obligatoire, ce qui est à mon sens dommage pour les cas simples comme le nôtre où nous n'utilisons pas les transactions. Heureusement, il existe une classe nous permettant de nous passer d'autres objets : ResourcelessTransactionManager. Il s'agit en quelque sorte d'un transaction manager "bouchon" à utiliser quand on n'a pas besoin de transactions impliquant des ressources transactionnelles telles que bases de données, JMS, XA...

Une fois ces objets techniques configurés, il nous reste le gros du sujet : la configuration de notre batch.

Comme dit précédemment, un batch est formé d'un job qui lui-même est composé d'étapes (steps) :

Et nous avons alors une configuration complète de notre batch, ouf !

Pour voir la configuration complète : batch-sample-context.xml

Lancement du batch

Une fois tout ce travail effectué, nous avons envie de pouvoir lancer notre batch. Spring-batch fournit le nécessaire pour lancer le batch en ligne de commande.

Pour le test, j'utilise Eclipse : aller dans le menu "Run > Run as..." et configurer une nouvelle application Java avec :

  • Classe principale :
org.springframework.batch.core.launch.support.CommandLineJobRunner
  • Arguments :
batch-sample-context.xml minimal

Le premier argument est le nom du fichier Spring à utiliser. Le second est le nom du job à lancer (ID du bean Spring).

Vous trouverez aussi un fichier .launch pour eclipse dans le projet complet (voir plus loin pour y accéder)

Pour tester avec Maven 2, il faut lancer la commande suivante :

mvn compile exec:java -Dexec.mainClass="org.springframework.batch.core.launch.support.CommandLineJobRunner" -Dexec.args="batch-sample-context.xml minimal"

Conclusion

De part ses possibilités, Spring-Batch requiert une configuration complexe qui au premier contact peut être assez repoussante. Ce premier batch est simpliste et très réducteur. Par exemple, l'utilisation des classes ResourcelessTransactionManager et MapJobRepositoryFactoryBean réduit considérablement les possibilités de Spring-batch.

Néanmoins, j'espère que ce premier pas vous sera utile pour ensuite vous plonger dans ce framework en profondeur, et pouvoir ajouter au fur et à mesure les concepts et fonctionnalités que l'on trouvera dans la documentation et les exemples. L'exemple complet est disponible sur la forge publique Octo : minimal-spring-batch-sample

Bon courage à vous !

Article écrit par Benoit Lafontaine et Julien Jakubowski