Comment gérer des batchs en Java avec Apache TomEE et Jakarta?

Introduction

Gérer des batchs en Java, soit pour accéder à des volumes de données importants, effectuer des calculs longs ou simplement gérer une file d’exécution, peut vite devenir une galère. Regardons ce que propose le projet Apache Jakarta pour une telle gestion.

Contexte

Ici je vais présenter un exemple complet prévu pour fonctionner avec Java 17 (minimum), Apache TomEE et une application REST. Le combo est complété avec Maven pour obtenir un projet classique.

Mise en place

Configuration TomEE

Il faut passer deux paramètres à TomEE:

  • tomee.mp.scan=all
  • openejb.cxf.CxfContainerClassLoader=false

Dans mon cas, je les passe à travers la ligne de commande avec le flag “-D”: -Dtomee.mp.scan=all -Dopenejb.cxf.CxfContainerClassLoader=false.

Configuration microprofile

Il faut ajouter un fichier vide nommé microprofile-config.properties dans le dossier resources/META-INF/.

Configuration des batchs

Pour configurer les batchs, il faut impérativement ajouter un fichier batchee.properties dans le dossier resources/. Ce fichier contient les lignes suivantes:

    
1org.apache.batchee.jmx = false
2BatchThreadPoolService = ch.gobothegeek.pro.jeebatch.GtgBatchThreadService
  • La première ligne indique qu’il ne faut pas connecter BatchEE avec JMX.
  • La seconde ligne indique quelle classe de gestion de threads utiliser.

Configuration du batch de test

Dans le dossier resources/, il faut ajouter un fichier job-test.xml contenant le code suivant:

    
1<?xml version="1.0" encoding="UTF-8"?>
2<job id="job-test" xmlns="https://jakarta.ee/xml/ns/jakartaee" version="2.0">
3    <step id="step1">
4        <properties>
5            <property name="param1" value="TEST" />
6        </properties>
7        <batchlet ref="ch.gobothegeek.pro.jeebatch.JobTest" />
8    </step>
9</job>

A noter que l’id du job doit être identique au nom de fichier.

Code

Classe GtgApplication

Cette classe sert de point d’entrée à notre application REST.

    
1package ch.gobothegeek.pro.jeebatch;
2
3import jakarta.enterprise.context.ApplicationScoped;
4import jakarta.ws.rs.ApplicationPath;
5
6@ApplicationPath("/home")
7@ApplicationScoped
8public class GtgApplication extends jakarta.ws.rs.core.Application { }

Classe GtgBatchManager

C’est cette classe qui va être chargée de piloter le démarrage des batchs. Il y a un point à noter ici: le nommage des batchs: il faut indiquer l’id du job tel qu’indiqué dans le fichier XML du batch.

    
 1package ch.gobothegeek.pro.jeebatch;
 2
 3import jakarta.annotation.PostConstruct;
 4import jakarta.batch.operations.JobOperator;
 5import jakarta.batch.runtime.BatchRuntime;
 6import jakarta.ejb.Singleton;
 7import jakarta.ejb.Startup;
 8import java.util.Properties;
 9
10@Startup
11@Singleton
12public class BatchManager {
13    private JobOperator jobMgr;
14
15    @PostConstruct
16    private void init() {
17        this.jobMgr = BatchRuntime.getJobOperator();
18    }
19
20    public long runJob(String job, Properties params) throws InterruptedException {
21        long batchID = this.jobMgr.start(job, params);
22        return batchID;
23    }
24}

Classe GtgBatchThreadService

Ici on crée un ThreadExecutor qui est autorisé à exécuter un thread à la fois. L’objectif est de créer une file d’attente limitant l’accès à une ressource unique.

    
 1package ch.gobothegeek.pro.jeebatch;
 2
 3import org.apache.batchee.container.services.executor.BatcheeThreadFactory;
 4import org.apache.batchee.container.services.executor.BoundedThreadPoolService;
 5import java.util.Properties;
 6import java.util.concurrent.ExecutorService;
 7import java.util.concurrent.LinkedBlockingQueue;
 8import java.util.concurrent.ThreadPoolExecutor;
 9import java.util.concurrent.TimeUnit;
10
11public class GtgBatchThreadService extends BoundedThreadPoolService {
12    @Override
13    protected ExecutorService newExecutorService(Properties batchConfig) {
14        return new ThreadPoolExecutor(1, 1, 600, TimeUnit.SECONDS,
15                new LinkedBlockingQueue<Runnable>(10),
16                BatcheeThreadFactory.INSTANCE);
17    }
18}

Classe JobTest

Finalement, le code du batch a exécuter. On simule un traitement long avec Thread.wait (parfaitement optionnel dans la vraie vie). Le batch retourne imméditement “COMPLETED” mais se place en file d’attente dû au fait que le ThreadExecutor n’autorise pas plusieurs traitements en parallèle.

    
 1package ch.gobothegeek.pro.jeebatch;
 2
 3import jakarta.batch.api.Batchlet;
 4import jakarta.batch.runtime.BatchRuntime;
 5import jakarta.batch.runtime.context.JobContext;
 6import jakarta.batch.runtime.context.StepContext;
 7import jakarta.inject.Inject;
 8import jakarta.inject.Named;
 9
10@Named
11public class JobTest implements Batchlet {
12    @Inject JobContext jobContext;
13    @Inject StepContext stepContext;
14
15    @Override
16    public String process() throws Exception {
17        long wait;
18        System.out.println("Param statique = " + this.stepContext.getProperties().getProperty("param1"));
19        System.out.println("Param dynamique = " + BatchRuntime.getJobOperator().getJobExecution(this.jobContext.getExecutionId()).getJobParameters().getProperty("param"));
20        wait = Long.parseLong(BatchRuntime.getJobOperator().getJobExecution(this.jobContext.getExecutionId()).getJobParameters().getProperty("timeout"));
21        Thread.sleep(wait);
22        return "COMPLETED";
23    }
24}

Conclusion

En quelques lignes de code, on peut mettre en oeuvre la gestion de batchs via la JSR-352. Bien sûr, cet article ne couvre pas l’intégralité du pilotage. Il manque:

  • la gestion des statuts
  • La gestion des résultats
  • Eventuellement, l’arrêt des batchs