I. Note du gabarisateur

Cet article a été mis au gabarit de developpez.com. Dans la mesure du possible, l'esprit d'origine de l'article a été conservé. Cependant, certaines adaptations ont été nécessaires (par exemple pour des liens ayant changé). Voici le lien vers le PDF d'origine : javasched.pdf

II. Introduction

Beaucoup d'applications d'entreprise nécessitent des traitements souvent longs et coûteux en ressources système. Lorsque ces traitements ne nécessitent pas d'interaction avec l'utilisateur, leur déclenchement peut être différé sur une période de charge faible, afin de ne pas détériorer les temps de réponse. Depuis la version 1.4, Java permet d'effectuer des planifications de tâches simples. Cependant les applications d'entreprise exigent des possibilités de reprise après panne, haute disponibilité, etc. C'est ce que propose Quartz, une bibliothèque open source.

III. Planification en Java standard

Ce tutoriel s'est basé sur la version 1.4.2 du JDK.

Depuis la version 1.3java.util.Timer, l'API standard de Java propose un système de planification de tâches basique au travers des classes et java.util.TimerTask. La classe Timer représente le scheduler et TimerTaskTimerTask une tâche à exécuter. On définit une tâche avec une classe qui dérive de et implémente la méthode run(). Une instance de la tâche est ensuite passée au Timer en spécifiant des paramètres de planification. Le timer exécute ensuite la méthode run() à la date programmée.

L'exemple suivant permettra de se familiariser avec l'API. On définit une tâche MyTask qui attend aléatoirement entre 1000 et 2500 ms. Cette tâche a été planifiée pour être exécutée toutes les deux secondes, trois secondes après le démarrage du programme.

 
Sélectionnez
class MyTask extends TimerTask { 
    Random r=new Random(); 
    public void run() { 
        try { 
            System.out.println("DEBUT"); 
            int t = 1000 + r.nextInt(1500); 
            Thread.sleep(t); 
            System.out.println("FIN : " + t); 
        } catch (InterruptedException e) { 
            e.printStackTrace(); 
        } 
    }  
} 

public class Test { 
    public static void main(String[] args) { 
        Timer t = new Timer(); 
        GregorianCalendar gc = new GregorianCalendar(); 
        gc.add(Calendar.SECOND, 3); 
        t.scheduleAtFixedRate(new MyTask(), gc.getTime(),2000); 
    } 
}

La classe Timer possède deux constructeurs qui permettent de créer un timer en mode bloquant ou en mode démon. Pour rappel, un programme java se termine une fois que tous les threads non démons sont terminés.

  • Le constructeur par défaut crée un timer bloquant. C'est le cas de notre programme d'exemple qui attend donc l'arrêt de la JVM pour se terminer (CTRL-C).
  • Le constructeur avec argument crée un timer démon. Celui-ci se termine en même temps que le programme principal. Pour l'utiliser dans notre exemple, il faut mettre en attente le thread principal en ajoutant par exemple Thread.sleep(…) à la fin du bloc main, afin que le timer puisse lancer ses tâches. Le lancement d'un autre thread bloquant comme une frame swing est une autre possibilité.

Une fois le timer lancé, on crée une planification en spécifiant une date de début et la période de répétitions.

Attention : certaines planifications comme « tous les mois » ne seront pas possibles, car la période entre deux mois n'est jamais la même…

Il existe deux types de planification :

  • Timer.schedule(…) respecte les périodes de répétitions. La date de prochain traitement est calculée en ajoutant la période à la date du dernier traitement. Si cette date n'est pas respectée (par ex. : le garbage-collector ou l'accès à une ressource a ralenti la tâche), un retard apparaît et s'accumule au précédent ;
  • Timer.scheduleAtFixedRate(…) essaye de respecter les dates d'exécution. La date de prochain traitement est calculée par rapport à la date du premier traitement. Ainsi si une tâche a pris du retard, les tâches suivantes peuvent rattraper ce retard en s'exécutant immédiatement après leur précédente.

Le Timer, comme tous les scheduler ne peut pas empêcher l'apparition des retards. Mais généralement les schedulers proposent des solutions pour les minimiser ou les contourner en amortissant le retard d'une tâche sur les suivantes, quitte à ne pas respecter un intervalle défini.

De plus la probabilité et le temps moyen des retards augmentent avec le nombre de tâches, surtout si plusieurs tâches sont programmées en même temps. Et là, le TimerTimerTimer n'est pas adapté. La raison réside dans son implémentation. Le dispose d'une file d'attente de tâches ordonnée par dates croissantes de prochains démarrages et d'un thread de traitement. L'algorithme implémenté est simple : le thread se met en attente de la première tâche de la file. Lorsque la date d'exécution est atteinte, le thread appelle la méthode run(), met à jour la prochaine date d'exécution de la tâche et se remet en attente. La présence d'un seul thread de traitement interdit d'optimiser les exécutions concurrentes de tâches. Malheureusement ce mode d'utilisation est fréquent sur les applications d'entreprise, généralement distribuées. Dans une application distribuée, plusieurs tâches peuvent facilement s'exécuter en parallèle pour optimiser l'utilisation des ressources. Une tâche peut effectuer un calcul tandis qu'une autre effectue des requêtes sur une base de données, et une autre effectue un transfert de fichier… Le n'est donc pas optimal dans ce genre d'application, mais tel n'est pas son but. D'autant qu'il existe une spécification pour un Timer « j2ee » géré par un conteneur de serveur d'application.

D'autres points font du Timer un composant pour des applications standards et non « entreprise » :

  • pas de planification sophistiquée ;
  • pas de persistance des tâches ;
  • pas de système de gestion des tâches évolué.

Cependant les cas d'utilisation du timer sont multiples et ce dernier reste intéressant pour sa grande simplicité et son intégration dans l'API standard. (Des exemples ? La reconnexion à intervalles réguliers d'un client FTP ou l'observation de la modification d'un fichier de paramétrage.)

Comme d'habitude, tout dépend du besoin de votre application.

IV. Planification avec Quartz

Ce tutoriel s'est basé sur la version 1.4.5 de Quartz.

Il existe plusieurs schedulers disponibles pour la plateforme Java, mais les plus intéressants sont Quartz et FluxFlux. est un scheduler commercial très complet, livré avec des outils de conception et de monitoring, qui permet même de gérer des processus workflow. QuartzQuartzQuartz ne va pas jusque-là, mais dispose de nombreuses fonctionnalités qui conviendront à la plupart des applications métier. fait partie du projet OpenSymphony qui propose des composants orientés entreprise pour la plateforme J2EE. La licence de dérive et est entièrement compatible avec Apache Software License définie par Apache Software Foundation. Il est open source, peut être redistribué avec modification des sources, et libre d'utilisation dans des projets commerciaux. C'est pour cette raison que nous allons le découvrir.

IV-A. Premiers Pas

Ce tutoriel permet de découvrir les composants de base de Quartz.

IV-A-1. Configuration

Dans un premier temps, je vous laisse télécharger la dernière version de QuartzQuartz ! est composé d'une bibliothèque principale et de bibliothèques annexes issues d'autres projets open source comme Apache Commons-xxx. Ces dernières ne sont pas toutes utiles pour l'instant :

Bibliothèques de base nécessaires :

  • lib/quartz.jar ;
  • lib/commons-logging.jar.

Ces bibliothèques sont à inclure dans le classpath de l'application.

IV-A-2. Démarrage

Il existe principalement trois types d'objets : le Schedulers, les Jobs et les Triggers. Un Job (travail) représente une tâche à exécuter, un Trigger (déclencheur) représente le mécanisme de planification d'une tâche. Concernant les relations, un job peut être utilisé par plusieurs trigger, mais un trigger n'est associé qu'à un seul job.

En démarrant le scheduler, celui-ci se mettra immédiatement en attente des jobs. Pour cela, on fait d'abord appel à la factory qui permet d'instancier le scheduler, puis on invoque la méthode start() sur l'instance obtenue. Le type de scheduler est déterminé par la factory en fonction des services techniques décrits dans un fichier de propriétés, cependant ce fichier n'est pas nécessaire si on souhaite travailler avec le scheduler par défaut. Dans notre cas, les services comme le fail-over et le clustering seront désactivés (voir §3.3.4Clustering).

 
Sélectionnez
SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
Scheduler sched = schedFact.getScheduler();
sched.start();

IV-A-3. Définition d'un Job

D'abord on crée une classe implémentant l'interface Job :

 
Sélectionnez
public class RapportsVentesJob implements Job {
    public void execute(JobExecutionContext arg0) throws JobExecutionException {
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            throw new JobExecutionException(e);
        }
    }
}

Les instances des jobs sont gérées par Quartz, il n'est pas possible de les instancier directement. À la place, on instancie un JobDetail qui décrit un Job.

 
Sélectionnez
JobDetail jobDetail = new JobDetail("myJob", Scheduler.DEFAULT_GROUP, RapportsVentesJob.class);

Les paramètres minimums du JobDetail sont :

  • définition du nom du job (qui permet d'être identifié par le scheduler) ;
  • définition du groupe d'appartenance (pour les opérations de maintenance groupées comme la suppression ou la mise en pause d'un ensemble de tâches) ;
  • la classe du job à exécuter.

Il est possible de passer des données à un job en utilisant un JobDataMap. Cet objet est un tableau associatif qui sera disponible dans le contexte d'exécution du Job (méthode execute).

 
Sélectionnez
JobDataMap map = new JobDataMap();
map.put("userId","155-123587");
jobDetail.setJobDataMap(map);
 
Sélectionnez
public void execute(JobExecutionContext context) throws JobExecutionException {
    JobDataMap map = context.getJobDetail().getJobDataMap();
    map.getString("userId");
}

IV-A-4. Définition d'un Job avec conservation d'état

L'état du contexte d'exécution d'un job est conservé pour l'exécution suivante au sein d'une même planification. Autrement dit, les données de JobDataMap sont conservées entre chaque top du Trigger. Côté implémentation, il suffit que le Job implémente l'interface StatefulJob.

Remarque : les exécutions concurrentes de StatefulJob ne sont pas possibles et sont lancées en séquence si plusieurs Trigger se déclenchent en même temps.

IV-A-5. Définition d'un Trigger

Il existe actuellement deux types de Trigger.

IV-A-5-a. SimpleTrigger

SimpleTrigger : ce trigger simple permet des planifications d'exécution « immédiate et unique » ou récurrente avec période fixe, avec nombre de répétitions limité ou non. C'est à peu près l'équivalent du Timer de Java.

Les paramètres minimums sont :

  • définition du nom du trigger (qui permet d'être identifié par le scheduler) ;
  • définition du groupe d'appartenance (pour les opérations de maintenance groupées).

Ensuite différents constructeurs permettent de définir :

  • la date de début ;
  • le nombre de répétitions ;
  • la période.
 
Sélectionnez
SimpleTrigger trigger = new SimpleTrigger("myTrigger", Scheduler.DEFAULT_GROUP, 5, 2000);

IV-A-5-b. CronTrigger

CronTrigger : ce trigger permet des planifications basées sur les jours du calendrier. Il utilise la syntaxe des expressions cron d'Unix.

Une expression cron est un ensemble de six champs obligatoires plus un facultatif, séparés par des espaces, et pouvant être associés à des caractères spéciaux :

Champs Valeurs Caractères spéciaux
Seconde [0-59] , - / *
Minute [0-59] , - / *
Heure [0-23] , - / *
Jour du mois [1-31] , - / * ? L W C
Mois [1-12] ou {JAN,MAY,.} , - / *
Jour de la semaine [1-7] ou {MON,WED,.} , - / * ? L # C
année vide ou [1970-2099] , - / *

Signification des caractères spéciaux :

, séparateur de valeurs par ex. : 10, 11,12 ou JAN, MAR
- intervalle entre deux valeurs par ex. : 10-14 signifie 10, 11, 12, 13,14
/ incrément d'une valeur de départ par ex. : 0/15 correspond aux valeurs 0, 15, 30, 45.
* toutes les valeurs
? permet de distinguer l'utilisation du jour du mois du jour de la semaine (c'est soit l'un, soit l'autre)
L signifie le dernier du mois ou de la semaine, par ex. : 6L
W signifie le plus proche jour de la semaine hors week-end, par ex. : 10W signifie que si le 10 est un dimanche, alors l'exécution se fera le lundi
# permet de spécifier le énième jour de la semaine dans le mois, par ex. : LUN#1 signifie le premier lundi du mois
C utilise le calendrier (Calendar, voir plus loin) pour définir les jours à exclure

Exemples :

  • « Tous les jours du lundi au vendredi à 08h00 » se traduit par « 0 0 8 ? * MON-FRI » ;
  • « Tous les derniers vendredis du mois à 10h15 » se traduit par « 0 15 10 ? * 6L ».
 
Sélectionnez
CronTrigger trigger = new CronTrigger("myTrigger", Scheduler.DEFAULT_GROUP, "0/3 * * * * ?");

IV-A-5-c. Exclusion de jours de la planification

CalendarCalendar : interface permettant de spécifier les jours à exclure d'une planification. On utilise si la planification est basée sur des règles métier qui ne peuvent pas être exprimées entièrement par une expression cron, par exemple les jours fériés ou les jours de repos. Il existe des implémentations telles que HolidayCalendar qui permet d'exclure des jours fériés ou WeeklyCalendar qui permet d'exclure certains jours de la semaine.

 
Sélectionnez
WeeklyCalendar c = new WeeklyCalendar();
c.setDayExcluded(java.util.Calendar.MONDAY, true);
trigger.setCalendarName("cal1");
sched.addCalendar("cal1", c, true, true);

IV-A-5-d. Enregistrement de la planification

Enfin, n'oublions pas l'enregistrement de la planification de notre job !

 
Sélectionnez
sched.scheduleJob(jobDetail, trigger);

IV-A-6. Échec d'activation d'un trigger (misfire)

Bien que limités par l'utilisation du pool de thread, les retards peuvent toujours apparaître. On s'en serait douté, Quartz gère les retards de manière plus sophistiquée que le Timer.

Lorsqu'un trigger ne peut pas traiter un job à l'heure prévue, le trigger passe dans l'état « misfired ». Ce cas se produit lorsqu'aucun thread du pool n'est disponible (jobs trop longs par exemple), ou que le scheduler, lancé en tant que serveur distant, est injoignable. Lorsqu'un Trigger passe dans l'état « misfired », le scheduler peut tenter d'effectuer une action telle que relancer le job immédiatement. C'est semble-t-il la politique par défaut, qui correspond au comportement du Timer du JDK. Il existe cependant d'autres actions possibles et ce, pour chaque type de trigger. On spécifie l'action via la méthode Trigger.setMisfireInstruction(int).

Actions d'un SimpleTrigger :

  • MISFIRE_INSTRUCTION_FIRE_NOW ;
  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT ;
  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT ;
  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT ;
  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT.

Actions d'un CronTrigger :

  • MISFIRE_INSTRUCTION_FIRE_ONCE_NOW ;
  • MISFIRE_INSTRUCTION_DO_NOTHING.

Les noms d'action renseignent déjà d'eux-mêmes, mais pour plus de détails, cf. la javadoc !

On peut également permettre un délai de retard au-delà duquel le Trigger passe dans l'état « misfired ». Le délai de retard par défaut est de 60 secondes et peut se configurer (voir §3.3Services techniques).

IV-A-7. Les listeners

Quartz permet au programme client d'être notifié des actions importantes effectuées par les différents composants du scheduler au travers des Listeners. Pour cela le client doit enregistrer auprès du composant à écouter une instance du type de listener qui lui est associé.

Il existe trois types de Listener :

  • JobListener : permet d'être notifié du début et de la fin d'une exécution, et si celle-ci s'est terminée avec ou sans erreur. À chaque fois, le contexte d'exécution est renvoyé ;
  • TriggerListener : permet d'être notifié de l'envoi d'un ordre d'exécution ou de l'impossibilité de l'envoyer (misfire) ;
  • SchedulerListener : permet d'être notifié entre autres de la création, suppression ou mise en attente de la planification d'une tâche.

Les listeners ne servent pas à créer des logs (il existe un plugin qui fait cela simplement), mais ils peuvent servir à mettre à jour l'application cliente, à notifier par mail ou par SMS un utilisateur, une équipe de maintenance… L'application cliente peut définir des tables de BDD où le listener ira mettre à jour l'état d'exécution des jobs, consultables depuis l'application, avec la possibilité en cas d'erreur de les relancer. Cela évite à l'application d'être trop dépendante du scheduler.

Exemple :

 
Sélectionnez
class RapportsVentesJobListener implements JobListener {
    public String getName() {
        return "rapportsVentesListener";
    }

    public void jobExecutionVetoed(JobExecutionContext context) {
    }

    public void jobToBeExecuted(JobExecutionContext context) {
        System.out.println("mise à jour du SI client : le job va être exécuté");
    }

    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        System.out.println("mise à jour du SI client : le job s'est exécuté");
        if(jobException==null) {
            System.out.println("ENVOI mail succès");
        } else {
            System.out.println("ENVOI mail échec");
            System.out.println(jobException);
        }
    }
}
 
Sélectionnez
sched.addJobListener(new RapportsVentesJobListener());
jobDetail.addJobListener("rapportsVentesListener");

IV-B. Gestion des Jobs et Triggers

Supprimer un job et les planifications (triggers) associés :

 
Sélectionnez
sched.deleteJob("myJob", Scheduler.DEFAULT_GROUP);

Supprimer une planification (trigger) :

 
Sélectionnez
sched.unscheduleJob("myTrigger", Scheduler.DEFAULT_GROUP);

Replanifier un job (remplace l'ancienne planification par une nouvelle) :

 
Sélectionnez
sched.rescheduleJob("myTrigger", Scheduler.DEFAULT_GROUP, trigger);

Mettre en attente/Reprendre des jobs et des triggers :

 
Sélectionnez
sched.pauseJobGroup(Scheduler.DEFAULT_GROUP);
Thread.sleep(20000); pause de vingt secondes.
sched.resumeJobGroup(Scheduler.DEFAULT_GROUP);

Dans cet exemple, il est probable que le trigger soit passé dans l'état « misfired » du fait du temps d'attente. Par défaut tous les jobs qui auraient dû s'exécuter dans l'intervalle de pause vont s'exécuter immédiatement (§3.1.6Échec d'activation d'un trigger (misfire)).

IV-C. Services techniques

Quartz propose un ensemble de services techniques comme la gestion des threads, la persistance ou le clustering. Ces services sont paramétrables dans un fichier de propriétés Java.

Exemple de fichier quartz.properties

 
Sélectionnez
#========================================================================== 
# Configure Main Scheduler Properties 
#========================================================================== 
org.quartz.scheduler.instanceName = SchedulerDeTest
org.quartz.scheduler.instanceId = scheduler1 
#========================================================================== 
# Configure ThreadPool 
#========================================================================== 
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool 
org.quartz.threadPool.threadCount = -1 
org.quartz.threadPool.threadPriority = 5 
#========================================================================== 
# Configure JobStore 
#========================================================================== 
org.quartz.jobStore.misfireThreshold = 60000 
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

Pour informer quartz qu'il doit utiliser un fichier et non le paramétrage par défaut, il suffit de renseigner son emplacement dans une variable système de la JVM au démarrage :

 
Sélectionnez
java -Dorg.quartz.properties=conf/quartz.properties…

IV-C-1. Gestion des exécutions concurrentes

Quartz implémente un pool de threads pour gérer les ressources en thread lors des exécutions concurrentes. Lorsqu'un trigger lance l'exécution d'un jobjobjobsjobs, celui-ci prend un thread dans le pool. Lorsqu'aucun thread n'est disponible, le est mis en attente. Le nombre de threads dépend du besoin de l'application, des ressources système disponibles et se détermine généralement expérimentalement. Par exemple si plusieurs sont lancés par minute, un pool de vingt threads ou plus pourrait être nécesaire. À l'inverse si quelques sont lancés de manière espacée dans une journée, un seul thread suffira. De plus, il faut savoir que dans un serveur d'application, le pool de thread n'est pas géré par le conteneur. Il doit être cependant possible de programmer un pool de Thread réalisant cela…

 
Sélectionnez
org.quartz.threadPool.threadCount = 20

Remarque : pour ne pas utiliser de pool, il suffit de donner la valeur -1.

IV-C-2. Persistance

La gestion du cycle de vie des jobs et des triggers est réalisée par le JobStore. Par défaut, ou si on le paramètre ainsi, Quartz utilise un JobStore non persistant avec la classe org.quartz.simpl.RAMJobStore. Par conséquent si l'application se plante, les jobs sont perdus. La solution est d'utiliser un JobStore persistant dans un SGBD. Quartz supporte la plupart des SGBD. Cela permet de garantir la reprise après panne (fail-over), en contrepartie les performances du scheduler sont diminuées du fait des requêtes SGBD, mais cela est généralement négligeable, car il existe souvent d'autres ralentissements dans une application.

Exemple d'un JobStore persistant dans une base MySQL :

 
Sélectionnez
#========================================================================== 
# Configure JobStore 
#========================================================================== 
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX 
org.quartz.jobStore.driverDelegateClass = 
org.quartz.impl.jdbcjobstore.StdJDBCDelegate 
org.quartz.jobStore.useProperties = false 
org.quartz.jobStore.dataSource = QRTZ_DS 
org.quartz.jobStore.tablePrefix = QRTZ_ 
org.quartz.jobStore.misfireThreshold = 60000 
org.quartz.jobStore.isClustered = false
  • driverDelegateClass définit la classe de mapping entre le JobStore et SGBD. Ici j'ai choisi le mapping standard car j'utilise MySQL. Les autres SGBD possèdent leur propre mapping. Ils sont référencés dans la documentation de Quartz ;
  • tablePrefix permet de distinguer les tables Quartz des autres, si vous décidez d'installer les tables dans une base existante ;
  • dataSource référence la description de la datasource (voir ci-dessous) ;
  • misfireThreshold définit le délai de dépassement par rapport à la date planifiée au-delà duquel un trigger passe dans l'état misfired ;
  • isClustered active ou non le mode cluster (voir plus loin).

Remarque : les scripts de création des tables Quartz se trouvent dans le répertoire docs/dbTables.

Définition de la datasource associée :

 
Sélectionnez
#========================================================================== 
# Configure Datasources 
#========================================================================== 
org.quartz.dataSource.myDS.driver = org.gjt.mm.mysql.Driver 
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost/quartz 
org.quartz.dataSource.myDS.user = root 
org.quartz.dataSource.myDS.password = 
org.quartz.dataSource.myDS.maxConnections = 5 
org.quartz.dataSource.myDS.validationQuery = selectlock_name from 
qrtz_locks where lock_name = 'TRIGGER_ACCESS';

bibliothèques supplémentaires nécessaires :

  • lib/commons-collections.jar ;
  • lib/commons-dbcp-1.1.jar ;
  • lib/commons-pool-1.1.jar ;
  • mm.mysql-2.0.2-bin.jar.

IV-C-3. Séparation Client/Serveur avec RMI

Jusqu'à maintenant notre scheduler fonctionnait dans la même JVM que celui du client. Mais Quartz est aussi prévu pour faire fonctionner le schedulerscheduler en tant que serveur distant. Ce mode est utile si on souhaite partager un entre plusieurs applications clientes ou alléger la charge d'un client.

Exemple de serveur :

 
Sélectionnez
public class TestServer { 
    public static void main(String[] args) { 
        if(System.getSecurityManager() != null) {   
            System.setSecurityManager(new java.rmi.RMISecurityManager());   
        } 
        try {
            SchedulerFactory schedFact = new StdSchedulerFactory(); 
            Scheduler sched = schedFact.getScheduler();
            sched.start();
        } catch (SchedulerException se) {
            se.printStackTrace();
        }
    } 
}

Configuration quartz du serveur : il faut ajouter ces paramètres en plus du pool de thread et du jobStore.

 
Sélectionnez
org.quartz.scheduler.instanceName = Sched1 
org.quartz.scheduler.rmi.export = true 
org.quartz.scheduler.rmi.registryHost = localhost 
org.quartz.scheduler.rmi.registryPort = 1099 
org.quartz.scheduler.rmi.createRegistry = true

Rmi.policy : dans notre exemple ce script accorde toutes les permissions (tous les clients sont acceptés et font ce qu'ils veulent…)

 
Sélectionnez
grant { 
permission java.security.AllPermission; 
};

Script de lancement du serveur :

 
Sélectionnez
@SET QRTZ=d:\program\quartz-1.4.5 
@SET QRTZ_CP=.;%QRTZ%\lib\commons-logging.jar;%QRTZ%\lib\commonscollections.jar;%QRTZ%\lib\commons-dbcp-1.1.jar;%QRTZ%\lib\commons-pool-1.1.jar;%QRTZ%\lib\log4j.jar;%QRTZ%\lib\jdbc2_0-stdext.jar;%QRTZ%\lib\quartz.jar 
@SET RMI_CODEBASE=file:/d:/program/quartz-1.4.5/lib/quartz.jar 
java -cp %QRTZ_CP% -Djava.rmi.server.codebase=%RMI_CODEBASE% -Djava.security.policy=java.policy -Dorg.quartz.properties=quartzServer.properties com.developpez.scheduletest.quartz.TestServer

Exemple de client :

 
Sélectionnez
public class TestClient { 
    public static void main(String[] args) { 
        try {
            SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
            Scheduler sched = schedFact.getScheduler();
            JobDetail jobDetail = new JobDetail("myJob", Scheduler.DEFAULT_GROUP, RapportsVentesJob.class); 
            CronTrigger trigger = new CronTrigger("myTrigger", Scheduler.DEFAULT_GROUP);
            trigger.setCronExpression("0/3 * * * * ?");
            sched.scheduleJob(jobDetail, trigger);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Configuration quartz du client : seuls ces paramètres sont utiles.

 
Sélectionnez
org.quartz.scheduler.instanceName = Sched1 
org.quartz.scheduler.rmi.proxy = true 
org.quartz.scheduler.rmi.registryHost = localhost 
org.quartz.scheduler.rmi.registryPort = 1099

Script de lancement du client :

 
Sélectionnez
@SET QRTZ=d:\program\quartz-1.4.5 
@SET QRTZ_CP=.;%QRTZ%\lib\commons-logging.jar;%QRTZ%\lib\commonscollections.jar;%QRTZ%\lib\commons-dbcp-1.1.jar;%QRTZ%\lib\commons-pool-1.1.jar;%QRTZ%\lib\log4j.jar;%QRTZ%\lib\jdbc2_0-stdext.jar;%QRTZ%\lib\quartz.jar 
java -cp %QRTZ_CP% -Dorg.quartz.properties=remoteClient.properties com.developpez.scheduletest.quartz.TestClient

Remarque : en client/serveur, le job est exécuté dans la JVM du serveur, il faudra donc veiller à ce que le scheduler dispose des classes nécessaires à l'exécution…

IV-C-4. Clustering

Le clustering permet de créer un super-scheduler réparti sur des machines différentes. Le traitement d'un jobjob s'effectuera sur la première instance de scheduler disponible dont au moins un thread est disponible. De même si un s'exécute sur une instance et que celle-ci plante, il est possible de paramétrer la récupération du job sur une instance disponible (voir l'exemple ClusterTest de Quartz). Cela permet donc de garantir une haute disponibilité de traitement des tâches, une reprise après panne améliorée de même qu'un équilibrage de charge, lorsque les ressources d'une machine seule ne suffisent plus (trop de threads ou trop de mémoire utilisée).

Prérequis : il faut utiliser un JobStore persistant, pointant sur une datasource partagée par chaque instance. Les machines du cluster doivent être synchrones (utiliser un serveur de temps)

Configuration : les identifiants d'instance des schedulersscheduler doivent être uniques dans tout le cluster. Pour simplifier la configuration, on peut utiliser AUTO ainsi les fichiers quartz.properties sont identiques sur chaque instance. En fait chaque utilisant la datasource sera considéré comme faisant partie du cluster. Les instances communiquent par l'intermédiaire de la datasource. Ainsi aucune autre configuration n'est nécessaire (par exemple au niveau du réseau). Le paramètre clusterCheckinInterval permet de vérifier l'état du cluster à chaque instant et observer les instances disponibles.

 
Sélectionnez
org.quartz.scheduler.instanceId = AUTO 
#========================================================================== 
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000

Récupération d'un job par le scheduler :

 
Sélectionnez
job.setRequestsRecovery(true);

IV-C-5. Plugins

Si certaines fonctionnalités de Quartz manquent ou ne sont pas adaptées à l'application métier, il est possible de les développer en tant que plugins

 
Sélectionnez
#========================================================================== 
# Configure Plugins 
#========================================================================== 
org.quartz.plugin.jobHistory.class = 
org.quartz.plugins.history.LoggingJobHistoryPlugin

Quartz fournit également quelques plugins utiles :

  • JobInitializationPlugin : lit le paramétrage des Jobs et Trigger dans un fichier XML ;
  • LoggingJobHistoryPlugin, LoggingTriggerHistoryPlugin : trace tous les évènements des Jobs et Triggers en s'appuyant sur la bibliothèque Apache-Common-Logging.
  • ShutdownHookPlugin : le scheduler est notifié de tout arrêt de la JVM (par CTRL-C ou par crash) et libère ses ressources proprement.

IV-D. Quelques Jobs utiles fournis avec Quartz

SendMailJob - Envoi de Mail : ce type de Job permet d'envoyer un mail selon les paramètres fournis par le client (le serveur SMTP, l'émetteur, le destinataire, le sujet, le corps…)

 
Sélectionnez
JobDetail jobDetail = new JobDetail("myJob", Scheduler.DEFAULT_GROUP, SendMailJob.class); 
JobDataMap map = new JobDataMap(); 
map.put(SendMailJob.PROP_SMTP_HOST,"smtp.bidule.fr"); 
map.put(SendMailJob.PROP_SENDER,"sender@bidule.fr"); 
map.put(SendMailJob.PROP_RECIPIENT,"recipient@bidule.fr"); 
map.put(SendMailJob.PROP_SUBJECT,"hello"); 
map.put(SendMailJob.PROP_MESSAGE,"just say hello. Please don't reply..."); 
jobDetail.setJobDataMap(map);

bibliothèques supplémentaires requises

  • lib/javamail.jar ;
  • lib/activation.jar.

EJBInvokerJob- Invocation d'EJB : ce type de JobJob permet de déléguer le traitement à un EJB. Trois paramètres sont nécessaires dans la définition du job par le client : le nom JNDI de l'EJB, la méthode exécutée par le et la liste d'arguments de cette méthode.

Attention : il n'est pas nécessaire de créer un contexte de connexion au serveur si le scheduler est lancé dans la même JVM. Pour lancer le scheduler en même temps qu'un serveur voir . Si le scheduler réside dans une autre JVM, les paramètres de connexion au serveur peuvent être placés dans le JobDataMap ou dans les propriétés d'environnement de la JVM (System.properties). Toutefois, s'il est nécessaire de fournir une authentification (Principal et Credentials), on ne peut manifestement le faire que dans System.properties.

Exemple avec un serveur Weblogic et un scheduler séparés.

 
Sélectionnez
JobDetail jobDetail = new JobDetail("myJob", Scheduler.DEFAULT_GROUP, EJBInvokerJob.class); 
JobDataMap map = new JobDataMap(); 
//System.setProperty(InitialContext.INITIAL_CONTEXT_FACTORY,"weblogic.jndi.WLInitialContextFactory"); 
//System.setProperty(InitialContext.PROVIDER_URL,"t3://192.168.1.2:7001"); 
map.put(EJBInvokerJob.INITIAL_CONTEXT_FACTORY,"weblogic.jndi.WLInitialContextFactory"); 
map.put(EJBInvokerJob.PROVIDER_URL,"t3://192.168.1.2:7001"); 
map.put(EJBInvokerJob.EJB_JNDI_NAME_KEY,"application/Sales"); 
map.put(EJBInvokerJob.EJB_METHOD_KEY,"reportByClientId"); 
map.put(EJBInvokerJob.EJB_ARGS_KEY,new Object[]{"152-12568"});

bibliothèques supplémentaires requises :

  • bibliothèque du serveur d'application (par ex le jar client weblogic).

JMXInvokerJob- Invocation de MBean : ce job permet d'invoquer une méthode sur un ManagementBean d'un serveur d'applications, via l'API JMX. Les détails d'invocation sont totalement transparents.

Attention : il est nécessaire que le schedulerscheduler soit dans la même JVM que le serveur. Pour lancer le en même temps qu'un serveur voir . La recherche du serveur de MBean s'effectue au moyen de MBeanServerFactory.findMBeanServer(null) qui renvoie la liste des serveurs présents dans la JVM. Pour une invocation à distance, il faudra créer un Job spécifique.

FileScanJob : vérifie si un fichier a été modifié entre deux exécutions du jobjob. Ce type de est stateful, car il doit conserver la date lue à l'exécution précédente.

NativeJob : lance une commande du système d'exploitation.

NoOpJob : Ne fait rien… ou presque ! Le JobListener est quand même notifié, ce qui permet de déporter le traitement dans ce dernier (c'est-à-dire côté client…)

IV-E. Démarrer le scheduler en même temps qu'un serveur web

Selon votre choix d'architecture, vous souhaiterez peut-être que le scheduler réside sur le même serveur que l'application cliente. Or comment s'assurer simplement que celui-ci soit disponible dès le démarrage ? Quartz fournit pour cela une Servlet à appeler au démarrage. Pour l'utiliser, il suffit d'insérer ces quelques lignes dans le fichier de configuration de votre application web (web.xml) :

 
Sélectionnez
<servlet> 
    <servlet-name> 
        QuartzInitializer 
    </servlet-name> 
    <display-name> 
        Quartz Initializer Servlet 
    </display-name> 
    <servlet-class> 
        org.quartz.ee.servlet.QuartzInitializerServlet 
    </servlet-class> 
    <load-on-startup> 
        1 
    </load-on-startup> 
    <init-param> 
        <param-name>config-file</param-name> 
        <param-value>/un/dossier/quartz.properties</param-value> 
    </init-param> 
    <init-param> 
        <param-name>shutdown-on-unload</param-name> 
        <param-value>true</param-value> 
    </init-param> 
</servlet>

Les paramètres d'initialisation sont :

  • config-file : le fichier quartz.properties à charger ;
  • shutdown-on-unload : lorsque le serveur est arrêté, permet également d'arrêter le scheduler proprement.

V. Conclusion

Nous venons d'aborder deux façons différentes de planifier des tâches en Java. Le Timer du JDK est certes moins évolué que Quartz mais a l'avantage d'être plus simple et de faire partie de l'API Standard. Le Timer est réservé à des applications qui peuvent se passer de services tels que la persistance et l'équilibrage de charge, qui ne se soucient pas d'être notifiés des tâches perdues lors d'un crash ou qui ne souhaitent pas de contrôle sophistiqué des planifications et des tâches.

Il est à noter qu'IBM et BEA ont déposé une spécification (JSR 236: Timer for Application Servers) d'API standard pour la planification de tâches gérée par un serveur d'application (prise en charge des ressources par le conteneur).

VI. Remerciement

Le gabarisateur remercie Claude Leloup pour sa correction orthographique.

VII. Références