Tutoriel pour apprendre à automatiser l'exécution de tâches avec Spring Batch

| Matthieu Lefèvre (Soat) | Soat |

Marre de gérer les crontab pour lancer des tâches automatisées récurrentes ? Cette solution alternative à l'aide de Spring Batch devrait vous intéresser.

Spring Batch permet de créer des applications console classiques, en lui configurant un scheduler intégré dans le framework, il est possible de gérer les tâches récurrentes.

Les objectifs de cet article sont de présenter un projet Spring Batch simple, puis de lui configurer un scheduler pour l'automatisation et enfin de préparer le package à l'aide de Maven pour la livraison.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel pour apprendre à automatiser l'exécution de tâches avec Spring Batch

- Auteurs : Matthieu Lefèvre (Soat), Soat

- Parution : 24 août 2015

- Licence : Copyright ® 2015 Soat. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Projet Spring Batch

- I-A. Projet Maven

- I-B. Configurer Spring

- I-C. Configurer Spring Batch

- I-D. Création de la classe principale

- II. Ajout du Scheduler à notre batch

- III. Préparation du package

- IV. Conclusion et remerciement

I - Projet Spring Batch

I-A - Projet Maven

Dans cette partie, nous allons créer un projet batch à l'aide de Spring et de Maven. Pour cela, créez un projet Maven vierge dans votre IDE préféré, pour exemple, mon pom.xml :

1. <project

2. xmlns="http://maven.apache.org/POM/4.0.0"

3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

5. <modelVersion>4.0.0</modelVersion>

7. <groupId>com.soat.batch</groupId>

8. <artifactId>scheduler</artifactId>

9. <version>0.0.1-SNAPSHOT</version>

10. <packaging>jar</packaging>

11. <name>batch-scheduler</name>

13. <properties>

14. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

15. <!-- propriétés utilisées dans la dernière partie de cet article -->

16. <java.version>1.7</java.version>

17. <package.directory>${project.build.directory}/${name}</package.directory>

18. <main.class>com.soat.batch.App</main.class>

20. <!-- version de Spring batch -->

21. <spring-batch.version>3.0.0.RELEASE</spring-batch.version>

22. </properties>

24. <dependencies>

25. <dependency>

26. <groupId>org.springframework.batch</groupId>

27. <artifactId>spring-batch-core</artifactId>

28. <version>${spring-batch.version}</version>

29. </dependency>

30. </dependencies>

31. </project>

La dépendance spring-batch-core est suffisante pour notre projet, puisqu'elle contient les bibliothèques du framework Spring.

I-B - Configurer Spring

Nous allons maintenant démarrer le développement de notre programme, en commençant par la configuration Spring :

1. @Configuration

2. @EnableBatchProcessing

3. @PropertySource("classpath:batch.properties")

4. public class AppConfiguration {

5. @Bean

6. public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {

7. return new PropertySourcesPlaceholderConfigurer();

8. }

9. }

Cette classe est très utile, car l'annotation @EnableBatchProcessing nous crée gentiment les beans nécessaires au fonctionnement d'un batch Spring :

- jobRepository ;

- transactionManager ;

- jobLauncher.

I-C - Configurer Spring Batch

Créons à présent les classes spécifiques à un batch Spring.

1. @Component

2. public class BatchTasklet implements Tasklet {

4. @Value("${tasklet.message:@null}") private String message;

6. @Override

7. public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {

8. System.out.println(message);

9. return RepeatStatus.FINISHED;

10. }

11. }

Pour des raisons évidentes de simplicité, je ne crée qu'une tâche (tasklet), qui affiche un message récupéré de mon fichier batch.properties ; j'assigne ensuite cette tâche à mon job configuré dans le fichier suivant.

1. <?xml version="1.0" encoding="UTF-8"?>

2. <beans xmlns="http://www.springframework.org/schema/beans"

3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

4. xmlns:context="http://www.springframework.org/schema/context"

5. xmlns:batch="http://www.springframework.org/schema/batch"

6. xmlns:task="http://www.springframework.org/schema/task"

7. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

8. http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd

9. http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd

10. http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">

12. <context:component-scan base-package="com.soat.batch" />

14. <batch:job id="scheduledJob">

15. <batch:step id="jobStep">

16. <batch:tasklet ref="batchTasklet" />

17. </batch:step>

18. </batch:job>

20. </beans>

Il n'y a rien de complexe dans ce fichier, c'est la configuration du job qui contient notre tâche. C'est ce job qui va être lancé par notre programme au travers de la classe suivante :

1. @Component

2. public class BatchLauncher {

4. @Autowired

5. @Qualifier("scheduledJob")

6. private Job job;

8. @Autowired

9. private JobLauncher jobLauncher;

11. public void run() {

12. JobParameters parameters = new JobParametersBuilder()

13. .addLong("currentTime", new Long(System.currentTimeMillis()))

14. .toJobParameters();

15. try {

16. jobLauncher.run(job, parameters);

17. } catch (JobExecutionAlreadyRunningException e) {

18. } catch (JobRestartException e) {

19. } catch (JobInstanceAlreadyCompleteException e) {

20. } catch (JobParametersInvalidException e) {

21. }

22. }

23. }

On récupère le job configuré ainsi que le launcher de Spring Batch pour créer le point d'entrée de notre batch.

On ajoute au lancement du jobLauncher de Spring un timestamp qui va permettre la récurrence puisque les différentes instances d'un même job doivent avoir des paramètres différents dans Spring Batch.

I-D - Création de la classe principale

1. public class App {

3. public static void main(String[] args) {

4. try(ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("job-config.xml")) {

5. BatchLauncher launcher = (BatchLauncher) context.getBean(BatchLauncher.class);

6. launcher.run();

7. }

8. }

9. }

Ceci est notre classe principale, c'est-à-dire le point d'entrée de notre batch. On remarque que c'est la même structure qu'une application console classique ; il suffit ensuite de déclarer le contexte Spring afin de pouvoir récupérer le bean BatchLauncher qui fait tourner notre programme.

Cet article n'a pas pour objectif de montrer les différentes façons de créer un batch avec Spring : c'est pourquoi notre exemple reste très simpliste.

II - Ajout du Scheduler à notre batch

Dans le fichier job-config.xml, configuré dans la section précédente, il suffit d'ajouter quelques lignes de code pour configurer le scheduler. Afin de lancer notre programme de façon récurrente, il faut passer, dans la configuration, la référence du bean qui sert de point d'entrée du batch : BatchLauncher (avec un b minuscule pour les conventions de nommage). Puis, il ne nous reste qu'à choisir le type de récurrence : fixed-delay, fixed-rate, cron.

1. <task:scheduled-tasks>

2. <task:scheduled ref="batchLauncher" method="run" fixed-delay="5000" />

3. </task:scheduled-tasks>

Avec cette configuration, votre tâche va s'exécuter toutes les cinq secondes à partir de la fin de la tâche précédente.

1. <task:scheduled-tasks>

2. <task:scheduled ref="batchLauncher" method="run" fixed-rate="5000" />

3. </task:scheduled-tasks>

Avec cette configuration, votre tâche va s'exécuter toutes les cinq secondes à partir du début de la tâche précédente.

1. <task:scheduled-tasks>

2. <task:scheduled ref="batchLauncher" method="run" cron="5 30 6 * * 1-5" />

3. </task:scheduled-tasks>

Avec cette configuration, votre tâche va s'exécuter tous les jours ouvrés (lundi-vendredi) à 6 heures 30 minutes et 5 secondes du matin.

On remarque ici une extension du cron Unix avec l'ajout des secondes.

C'est donc Spring Batch qui s'occupe de lancer notre tâche au travers du BatchLauncher. Certaines lignes doivent être enlevées de App.java, qui ne nécessite plus l'appel explicite de la méthode run du bean BatchLauncher :

1. public class App {

3. public static void main(String[] args) {

4. new ClassPathXmlApplicationContext("job-config.xml");

5. }

6. }

Pour améliorer un peu ces configurations, je préfère mettre la valeur de la récurrence dans un fichier .properties qui me permettra de la modifier sans avoir besoin de recompiler le batch.

1. batch.message=batch lancé !

2. batch.cron=5 30 6 * * 1-5

C'est notre fichier batch.properties configuré dans AppConfiguration.java.

III - Préparation du package

Afin de pouvoir livrer le batch, il faut préparer le package nécessaire. Pour cela, nous allons utiliser Maven et quelques-uns de ses plugins. Dans le fichier pom.xml ajoutons la section build.

1. <dependencies>

2. ...

3. </dependencies>

4. <build>

5. <plugins>

6. <!-- placer les plugins ici -->

7. </plugins>

8. </build>

Le premier plugin va nous permettre de réinitialiser le package, cela supprime la build précédente pour la remplacer ensuite par le nouveau package que les autres plugins vont créer.

1. <plugin>

2. <groupId>org.apache.maven.plugins</groupId>

3. <artifactId>maven-clean-plugin</artifactId>

4. <version>2.6.1</version>

5. <executions>

6. <execution>

7. <id>clean-package</id>

8. <phase>prepare-package</phase>

9. <goals>

10. <goal>clean</goal>

11. </goals>

12. <configuration>

13. <excludeDefaultDirectories>true</excludeDefaultDirectories>

14. <filesets>

15. <fileset>

16. <directory>${package.directory}</directory>

17. <includes>

18. <include>*</include>

19. <include>**</include>

20. </includes>

21. </fileset>

22. </filesets>

23. </configuration>

24. </execution>

25. </executions>

26. </plugin>

Le prochain plugin sert à la compilation du package pour la version de Java souhaitée.

1. <plugin>

2. <groupId>org.apache.maven.plugins</groupId>

3. <artifactId>maven-compiler-plugin</artifactId>

4. <version>3.3</version>

5. <executions>

6. <execution>

7. <id>compile-package</id>

8. <phase>compile</phase>

9. <goals>

10. <goal>compile</goal>

11. </goals>

12. <configuration>

13. <source>${java.version}</source>

14. <target>${java.version}</target>

15. </configuration>

16. </execution>

17. </executions>

18. </plugin>

On va maintenant packager le projet en .jar en excluant le fichier batch.properties qui pourra alors être modifié sans recompilation du batch. Il faudra néanmoins relancer le programme pour qu'il prenne en compte les modifications. La configuration du manifeste est très importante pour que votre application fonctionne, car c'est dans ce fichier que se trouve la déclaration de la classe principale ainsi que le classpath pour accéder aux dépendances.

1. <plugin>

2. <groupId>org.apache.maven.plugins</groupId>

3. <artifactId>maven-jar-plugin</artifactId>

4. <version>2.6</version>

5. <executions>

6. <execution>

7. <id>jar-package</id>

8. <phase>package</phase>

9. <goals>

10. <goal>jar</goal>

11. </goals>

12. <configuration>

13. <finalName>${artifactId}-${version}</finalName>

14. <outputDirectory>${package.directory}/</outputDirectory>

15. <excludes>

16. <!-- les fichiers .properties ne seront pas dans le jar -->

17. <exclude>*.properties</exclude>

18. </excludes>

19. <archive>

20. <manifest>

21. <addClasspath>true</addClasspath>

22. <classpathPrefix>dependencies/</classpathPrefix>

23. <mainClass>${main.class}</mainClass>

24. </manifest>

25. <manifestEntries>

26. <Class-Path>.</Class-Path>

27. </manifestEntries>

28. </archive>

29. </configuration>

30. </execution>

31. </executions>

32. </plugin>

Je préfère sortir toutes les bibliothèques externes (dépendances Maven) en dehors du jar dans un dossier que vous pouvez appeler dependencies, ou bien libs, comme bon vous semble : cela permet de créer un jar qui ne contient que les classes de votre programme.

1. <plugin>

2. <groupId>org.apache.maven.plugins</groupId>

3. <artifactId>maven-dependency-plugin</artifactId>

4. <version>2.10</version>

5. <executions>

6. <execution>

7. <id>copy-dependencies</id>

8. <phase>package</phase>

9. <goals>

10. <goal>copy-dependencies</goal>

11. </goals>

12. <configuration>

13. <outputDirectory>${package.directory}/dependencies/</outputDirectory>

14. </configuration>

15. </execution>

16. </executions>

17. </plugin>

Nous avons exclu les fichiers .properties (ressources) du jar, donc il nous faut les intégrer dans le package grâce à ce plugin. Nous voulons que les fichiers xml de configuration Spring restent dans le jar donc, à l'inverse de ce que nous avions fait pour le plugin jar, nous devons exclure les .xml des ressources à sortir.

1. <plugin>

2. <groupId>org.apache.maven.plugins</groupId>

3. <artifactId>maven-resources-plugin</artifactId>

4. <version>2.7</version>

5. <executions>

6. <execution>

7. <id>copy-resources</id>

8. <phase>package</phase>

9. <goals>

10. <goal>copy-resources</goal>

11. </goals>

12. <configuration>

13. <outputDirectory>${package.directory}</outputDirectory>

14. <resources>

15. <resource>

16. <directory>src/main/resources/</directory>

17. <excludes>

18. <!-- le fichier job-config.xml reste dans le jar -->

19. <exclude>*.xml</exclude>

20. </excludes>

21. </resource>

22. </resources>

23. </configuration>

24. </execution>

25. </executions>

26. </plugin>

Il suffit de lancer un maven install pour packager le projet dans le dossier target/batch-scheduler.

Ensuite, dans votre invite de commandes préférée, jouer le batch d'un simple.

java -jar scheduler-0.0.1-SNAPSHOT.jar

IV - Conclusion et remerciement

Un batch récurrent est une fonctionnalité très utile dans un projet, mais sa création peut s'avérer assez complexe. Cet article expose une façon de faire particulièrement simple avec le framework Spring ; il ne tient qu'à vous maintenant d'y intégrer la logique propre à votre projet et je vous conseille de bien étudier le framework batch avant (si cela n'est pas déjà fait).

Si vous avez différents batchs à créer qui doivent travailler à des moments différents, il est possible de les séparer dans le même projet en ajoutant une ligne task :scheduled avec son propre timer réduisant ainsi le nombre de projets à maintenir.

La préparation du package de livraison est une manière que j'ai trouvée élégante pour procéder, mais cela peut paraître assez complexe à mettre en place en sachant qu'un EDI vous permet de tout compiler dans un jar : c'est un petit plus sur l'utilisation de Maven, mais pas une nécessité pour créer un batch.

Cet article a été publié avec l'aimable autorisation de SOAT, société d'expertise et de conseil en informatique.

Nous tenons à remercier ClaudeLeloup pour la relecture orthographique et Mickaël Baron pour la mise au gabarit.