Accueil > Frameworks > Introduction à Spring-Batch

Introduction à Spring-Batch

Contexte

Dans le cadre de la refonte d’une application extranet chez un client, j’ai été plus particulièrement chargé du développement des batchs. Les batchs à développer devaient tous réaliser des tâches similaires :

  • transferts FTP de fichiers (upload et download)
  • traitements sur les fichiers ainsi obtenus (redimensionnement d’images JPEG, parsing de fichiers XML)
  • enregistrement des méta-données sur les fichiers ainsi manipulés en base de données.

Dans l’ancienne version de l’application, les batchs étaient développés grâce à un framework interne (vieillissant). J’ai donc recherché des frameworks de batch permettant de remplacer l’ancienne architecture tout en s’intégrant avec les composants utilisés dans la partie web de l’application (développée en Spring et Struts 1).

Choix du framework

Dans un souci de facilitation de la maintenance, le choix de Spring-batch s’est rapidement imposé. En effet, la plupart des composants (DAOs, services) nécessaires pour réaliser les tâches ci-dessus avaient déjà été développés sous la forme beans Spring utilisés par l’application web ; la principale tâche restant à accomplir étant le câblage de ces différents composants. Il était donc plus efficace et plus maintenable de réutiliser les composants existants et de les injecter dans les batchs via IOC.

Le développement a donc consisté à créer les classes composant la partie « front » des batchs et à gérer leur enchaînement.

Le but de cet article n’est pas de décrire de manière détaillée le fonctionnement de Spring-batch. Il s’agit plutôt de réaliser par étapes un petit tutoriel, la documentation officielle de Spring étant très complète mais peu adaptée à une prise en main rapide : http://static.springsource.org/spring-batch/reference/html-single/index.html

Ce tutoriel traitera surtout du développement de tâches pour lesquels un composant de Spring-batch n’existe pas déjà. En effet, ce framework propose des classes permettant de mettre en place rapidement (et généralement en ne manipulant que les fichiers de configuration Spring) des tâches récurrentes dans le domaine des batchs :

  • parsing de fichiers XML pour insertion dans une base de données ;
  • export de données d’une base vers un fichier plat ;

Des exemples fonctionnels pour ces tâches sont disponibles sur le web, notamment dans la documentation officielle ; en revanche, il n’existe que très peu d’exemples de la mise en place de tâches “spécifiques” : dans le cas qui nous intéresse, ces tâches étaient par exemple l’upload/download de fichiers via FTP et de manipulation sur des fichiers images (en tant que fichiers, et donc sans lire leur contenu comme c’est le cas avec des fichiers CSV ou XML plus fréquemment manipulés par les batchs).

Vocabulaire utilisé

Avant d’identifier les composants à développer pour notre batch, un rappel du vocabulaire utilisé dans le domaine des batchs (et plus particulièrement de Spring-batch) est nécessaire :

Job : Un job désigne un traitement batch.

Step : Phase atomique d’un batch. Un job est composé de steps ; il s’agit d’une étape du batch. Un step encapsule les informations permettant de définir et de contrôler le déroulement du batch.

Tasklet : Tâche exécutée par un step, par exemple “Parser le fichier test.xml”, “Insérer un enregistrement en base de données”… Dans le cadre de Spring-batch, cela consiste en une implémentation de l’interface Tasklet. Un step n’exécute qu’une tasklet ; en revanche une même tasklet peut être exécutée par plusieurs steps.

Spring-batch propose un certain nombre de tasklets pré-définies, orientées par exemple vers la lecture et l’écriture de fichiers. Il est cependant possible de créer ses propres tasklets, c’est ce que nous allons faire au cours de cet article.

Problématique

Supposons que le batch à écrire doive réaliser les opérations suivantes :

  • download de fichiers JPG d’un serveur FTP vers un répertoire temporaire ;
  • redimensionnement des images et transfert dans un répertoire local ;
  • enregistrement de métadonnées concernant ces images dans une base de données.

Implémentation

La première étape consiste à identifier quels sont les concepts utilisés ici, afin de mettre en place notre architecture de batch et de définir les interactions entre ces composants (enchaînements, conditions d’arrêt…)

L’ensemble du processus décrit ci-dessus correspond à un job ; en effet tous ces traitements sont liés et seule l’exécution de tous ces traitements permet de réaliser la tâche attendue.

Ce processus peut facilement être décomposé en plusieurs tâches élémentaires, qui seront représentées sous la forme de steps : chacune des trois opérations décrites sera configurée comme étant un step dans l’application context.

Chaque step sera par la suite implémentée sous la forme d’une tasklet. Toute tasklet utilisée dans un projet Spring-batch est une classe implémentant l’interface org.springframework.batch.core.step.tasklet.Tasklet.

Création de l’applicationContext

Nous allons commencer par créer un fichier applicationContext.xml basique, contenant le minimum de beans qui nous permettront ensuite au batch de s’exécuter. Trois beans doivent être créés :

  • Un job launcher : Bean permettant l’exécution d’un batch. Dans notre cas, nous allons utiliser son implémentation la plus basique, SimpleJobLauncher.
  • Un transaction manager : Dans notre cas, nous n’utilisons pas de transactions, mais tout job launcher doit posséder un composant permettant de gérer les transactions qui lui sont associées. Nous allons donc utiliser ici un ResourcelessTransactionManager, ce point n’étant pas le thème principal de l’article.
  • Un job repository : Bean permettant de gérer la persistance des méta-données liées au batch.

Le fichier applicationContext.xml minimum sera donc le suivant :

1
2
3
4
5
6
7
8
9
10
11
<beans>
    <bean id="transactionManager" class="org.springframework.batch.support.transaction.ResourcelessTransactionManager" />
    <!-- In memory job repository -->
    <bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
        <property name="transactionManager" ref="transactionManager" />
    </bean>
    <!-- Job launcher -->
    <bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
        <property name="jobRepository" ref="jobRepository" />
    </bean>
</beans>

Tasklet de download FTP : FTPTasklet

Cette tasklet a simplement pour objectif de télécharger via FTP les fichiers se trouvant dans un répertoire distant vers un répertoire local.

Aucune des tasklets pré-définies de Spring-Batch ne permet de mettre en place directement ce traitement, nous allons donc devoir écrire une tasklet spécifique pour cette opération. Nous allons donc implémenter notre propre classe FTPTasklet à partir de l’interface Tasklet proposée par Spring-Batch.

L’interface Tasklet est la plus générique possible pour implémenter une tâche de batch : elle ne possède qu’une seule méthode, execute, destinée à effectuer les traitements attendus :

1
RepeatStatus execute (StepContribution contribution, ChunkContext chunkContext) throws Exception

La classe FTPTasklet est donc construite suivant le modèle ci-dessous :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FTPTasklet implements Tasklet {
    private static Logger log = Logger.getLogger(FTPTasklet.class);

    // FTP Server connection properties
    private String ipAddress;
    private String login;
    private String password;

    private String localRep;

    /**
     * Define getters and setters here
     */

    public RepeatStatus execute(StepContribution contribution,
                                ChunkContext chunkContext) throws IOException {
        // Connect to FTP server
        // Check connection
        // Retrieve files

        return null;
    }
}

Le code complet se trouve dans l’archive du projet liée à cet article. Le traitement de download est implémenté dans la méthode execute, en utilisant les informations de connexions injectées via IOC.

Une fois cette tasklet écrite, nous pouvons modifier le fichier applicationContext.xml pour définir un batch minimal l’utilisant. Cela consiste à modifier le job pour lui ajouter un step exécutant FTPTasklet :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- Job definition -->
<batch:job id="myJob">
    <!-- Step definition -->
    <batch:step id="ftpDownloadStep">
        <batch:tasklet>
            <bean class="com.excilys.tuto.batch.tasklet.FTPTasklet">
                <!-- Properties of FTPTasklet bean -->
                <property name="localRep" value="/dev/workspaces/spring_batch/batch-itemreader/src/main/resources/input/"/>
                <property name="ipAddress" value="127.0.0.1"/>
                <property name="login" value="testuser"/>
                <property name="password" value="testuser"/>
            </bean>
        </batch:tasklet>
    </batch:step>
</batch:job>

Le batch peut alors être exécuté en utilisant la classe main org.springframework.batch.core.launch.support.CommandLineJobRunner, qui prend en paramètre le path du fichier applicationContext et le nom du job à lancer (ici myJob).

Lecture et redimensionnement des images : JPEGFileResizeTasklet

La deuxième tâche à créer pour notre batch est la tâche chargée du redimensionnement des fichiers JPEG précédemment récupérés.

On pourrait créer la tasklet chargée de lire et de redimensionner les images comme la tasklet de download FTP, en écrivant une classe implémentant l’interface Tasklet :

1
2
3
4
5
6
7
8
public class JPEGFileResizeTasklet implements Tasklet {
    public RepeatStatus execute (StepContribution contribution, ChunkContext chunkContext)
            throws Exception {
        // Read local directory
        // Resize images
        // Write images to output directory
    }
}

La configuration de la step correspondante serait alors la suivante :

1
2
3
4
5
6
7
<batch:step id="step1">
    <batch:tasklet>
        <bean class="com.excilys.tuto.batch.tasklet.JPEGFileResizeTasklet">
            <property name="localRep" value="/dev/workspaces/spring_batch/batch-itemreader/src/main/resources/input/"/>
        </bean>
    </batch:tasklet>
</batch:step>

Cependant, Spring-batch propose des classes prédéfinies permettant de réaliser ce traitement de manière beaucoup plus succinte.

En effet, la documentation met l’accent sur les classes permettant de parser des fichiers plats ou de lire des données dans une base afin d’en extraire des “items”. Ces items sont ensuite traités en “chunks” (ensembles d’items).

Les traitements effectués sur des chunks de données sont réalisés en implémentant trois interfaces fournies par Spring-batch :

  • ItemReader : interface prenant en charge la lecture des chunks d’items à traiter ;
  • ItemProcessor : traitement des items ;
  • ItemWriter : écriture des items.

Les implémentations de ces trois interfaces sont ensuite enchaînées pour réaliser le traitement à effectuer sur les items (lecture – traitement – écriture).

Pour mettre en place notre traitement, il serait intéressant d’implémenter ces interfaces : de la même manière que ces classes sont par exemple implémentées par des classes du framework permettant de parser un fichier XML et d’en extraire des objets, nous pouvons les utiliser pour lire le contenu du répertoire d’entrée et d’en extraire des objets Resource.

Une implémentation d’ItemReader pour lire les fichiers d’entrée

Pour mettre en place notre tasklet traitant des chunks de fichiers JPEG, le premier composant à écrire est donc la classe de lecture des fichiers JPEG, implémentant l’interface ItemReader.

Spring-batch propose une implémentation particulière de l’interface ItemReader, ResourcesItemReader, permettant de lire des objets Resource dans un répertoire à partir d’un pattern de nom de fichier. Il n’est donc pas nécessaire de créer notre propre implémentation d’ItemReader ; il suffit de déclarer dans l’applicationContext de Spring un bean de classe ResourcesItemReader, qui lira les fichiers jpg placés dans un répertoire d’entrée. Dans notre cas, ce bean sera donc configuré de la façon suivante :

1
2
3
<bean id="itemReader" class="org.springframework.batch.item.file.ResourcesItemReader">
    <property name="resources" value="/dev/workspaces/spring_batch/batch-itemreader/src/main/resources/input/*.jpg" />
</bean>

Et c’est tout ! Il nous reste à câbler cet item reader avec un itemProcessor qui traitera les items (ici le redimensionnement des images), puis avec un itemWriter pour insérer en base de données les informations concernant les images importées.

Une implémentation d’ItemProcessor pour redimensionner les images

Notre ItemProcessor va prendre en entrée les fichiers lus par l’itemReader, redimensionner les images et créer les objets métier Image correspondants.

Le fonctionnement d’un ItemProcessor étant spécifique au domaine du batch (ici le redimensionnement d’images JPEG), nous devons l’implémenter nous-mêmes. Dans notre cas, la méthode process de l’ItemProcessor prendra en entrée un objet de classe Resource (correspondant à un fichier lu par l’itemReader) pour créer une instance d’une classe de notre package model, représentant les données concernant l’image que nous enregistrerons ensuite dans la base de données :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ImageProcessor implements ItemProcessor {

    private String outputFolder;

    public void setOutputFolder(String outputFolder) {
        this.outputFolder = outputFolder;
    }

    public Image process(Resource item) throws Exception {
        BufferedImage src = ImageIO.read(item.getURL());
        // Resize image
        // Write file
        // Create an Image object and return it
        ImageDTO image = new ImageDTO();
        // ...
        return image;
    }
}

Nos images JPEG sont maintenant redimensionnées. Il ne nous reste plus qu’à insérer les données les concernant dans la base de données en utilisant une implémentation d’ItemWriter.

Une implémentation d’ItemWriter pour insérer les images en base de données

L’interface ItemWriter de Spring ne contient qu’une méthode, write, prenant en entrée une liste d’objets.

Notre itemWriter est très simple : il se contente de récuperer les instances de ImageDTO créées par le processor et d’insérer les enregistrements correspondants en base de données.

Ce batch étant lié à un application web utilisant Spring IOC, il suffit d’injecter dans l’itemWriter le service déjà créé pour l’application web et de faire appel à sa méthode d’insertion afin d’alimenter la base de données.

1
2
3
4
5
6
7
8
9
10
public class ImageWriter implements ItemWriter {
    private ImageService imageService;

    public void write(List<? extends Image> items) throws Exception {
        for(Image item : items) {
            imageService.insertImage(item);
        }
    }
    ...
}

Notre batch est à présent terminé. Seuls les traitements spécifiques à ce batch ont fait l’objet d’une implémentation spécifique : les services et DAOs précédemment développés dans le cadre de l’application web ont pu être réutilisés.

Conclusion

Cet article ne présente que quelques possibilités offertes par Spring-Batch que j’ai été amené à utiliser dans le cadre du projet sur lequel j’ai travaillé. Cependant, ce framework est très riche ; je n’ai par exemple pas évoqué les possibilités qu’il offre dans la gestion des transactions, le traitement par lots, un contrôle plus fin de l’enchaînement des tâches… Pour ces aspects, la documentation officielle est simple à prendre en main et des exemples plus détaillés sont disponibles dans le package de Spring-batch, je vous invite donc à aller y jeter un œil si le sujet vous intéresse.

Le projet Eclipse contenant le code utilisé dans cet article se trouve ici : spring_batch_tuto

Share
Categories: Frameworks Tags: , ,
  1. Pas encore de commentaire
  1. 29/06/2015 à 23:52 | #1