IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

 

Développons en Java   2.30  
Copyright (C) 1999-2022 Jean-Michel DOUDOUX    (date de publication : 15/06/2022)

[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

 

111. La planification de tâches

 

chapitre    1 1 1

 

Niveau : niveau 3 Intermédiaire 

 

Il est fréquent de devoir exécuter des tâches planifiées :

  • pour une seule exécution
  • pour plusieurs exécutions
  • pour des exécutions récurrentes
  • avec éventuellement un délai d'attente avant la première exécution

La planification de tâches peut requérir différentes fonctionnalités :

  • configuration avancée des déclenchements
  • persistance des tâches

Le JDK propose des solutions basiques pour planifier l'exécution de tâches. Pour des besoins plus évolués, il est nécessaire d'utiliser une solution tierce : une des plus utilisée est l'API open source Quartz.

Ce chapitre contient plusieurs sections :

 

111.1. La planification de tâches avec l'API du JDK

Le JDK propose deux solutions pour la planification basique de l'exécution de tâches :

  • Timer et TimerTask : à partir de Java 1.3
  • ScheduledExecutorService : à partir de Java 5

La planification peut être pour une exécution unique ou pour une exécution répétée périodiquement avec ou sans délai d'attente avant la première exécution.

 

111.1.1. Les classes Timer et TimerTask

Depuis Java 1.3, la classe java.util.Timer permet d'exécuter des traitements de manière périodique : elle joue le rôle d'un scheduler simple.

Elle offre une solution simple, fournie en standard dans le JDK, pour exécuter des tâches de manière répétée.

La tâche à exécuter doit être encapsulée dans une classe qui hérite de la classe TimerTask.

 

111.1.1.1. La classe java.util.TimerTask

La classe abstraite java.util.TimerTask encapsule les traitements d'une tâche qui sera exécutée par un Timer. Elle implémente l'interface Runnable.

Son exécution pourra alors être planifiée en utilisant une instance de type Timer.

La classe java.util.TimerTask permet la planification d'une seule exécution ou de plusieurs. La classe TimerTask est ajoutée à partir de Java 1.3.

Elle ne possède qu'un seul constructeur qui n'attend aucun paramètre. Elle possède plusieurs méthodes :

Méthode

Rôle

boolean cancel()

Annuler la planification des prochaines exécutions. Cette méthode n'interrompt pas les exécutions en cours. Elle renvoie un booléen qui précise si au moins une exécution a été annulée

abstract void run()

Contenir les traitements à exécuter

long scheduledExecutionTime()

Renvoyer l'heure planifiée de la prochaine exécution ou de l'exécution courante si elle est en cours


Pour créer une tâche, il faut créer une classe fille qui hérite de la classe TimerTask.

Exemple ( code Java 1.3 ) :
package fr.jmdoudoux.dej.timer;
			
import java.util.Date;
import java.util.TimerTask;

public class MaTask extends TimerTask {
  @Override
  public void run() {
    System.out.println(new Date() + " Execution de ma tache");
  }
}

 

111.1.1.2. La classe java.util.Timer

La classe Timer est un scheduler basique qui permet la planification de l'exécution d'une tâche pour une seule exécution ou pour une exécution répétée à intervalles réguliers. Elle utilise pour cela un thread dédié.

La classe Timer permet de planifier l'exécution d'une tâche pour :

  • une seule exécution à un moment donné
  • plusieurs exécutions périodiques espacées d'un même intervalle

Les possibilités de planification du Timer sont donc très limitées : les répétitions ne peuvent se faire que sur l'attente d'un délai exprimé en millisecondes. Il n'est par exemple pas possible de définir des planifications quotidiennes à heures fixes, mensuelles, ... Il faut dans ce cas utiliser une autre solution proposée par un tiers.

Elle possède plusieurs constructeurs :

Constructeur

Rôle

Timer()

Construire une instance, le thread associé n'est pas un démon

Timer(boolean isDaemon)

Construire une instance en précisant si le thread associé au timer est un démon

Timer(String name)

Construire une instance en précisant le nom du thread associé au timer (depuis Java 5)

Timer(String name, boolean isDaemon)

Construire une instance en précisant le nom du thread et si celui-ci est un démon (depuis Java 5)


Tous ces constructeurs lancent le thread qui leur est associé.

Elle possède plusieurs méthodes :

Méthode

Rôle

void cancel()

Terminer le timer : toutes les planifications sont supprimées

int purge()

Retirer de la file toutes les tâches annulées ce qui les rend éligibles pour le ramasse-miettes

void schedule(TimerTask task, Date time)

Planifier l'exécution de la tâche à la date/heure fournies en paramètres. Si celle-ci est dans le passé alors la tâche est exécutée immédiatement

void schedule(TimerTask task, Date firstTime, long period)

Planifier l'exécution répétée de la tâche : la première exécution est prévue à firstTime et le délai entre chaque exécution est précisé par le paramètre period en millisecondes

void schedule(TimerTask task, long delay)

Planifier l'exécution de la tâche après avoir attendu delay millisecondes

void schedule(TimerTask task, long delay, long period)

Planifier l'exécution répétée de la tâche : la première exécution intervient après avoir attendu delay millisecondes et le délai entre chaque exécution est précisé par le paramètre period en millisecondes

void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

Planifier l'exécution répétée de la tâche : la première exécution est prévue à firstTime et le délai entre chaque exécution est précisé par le paramètre period en millisecondes

void scheduleAtFixedRate(TimerTask task, long delay, long period)

Planifier l'exécution répétée de la tâche : la première exécution intervient après avoir attendu delay millisecondes et le délai entre chaque exécution est précisé par le paramètre period en millisecondes


La planification peut se faire de deux manières en utilisant une des surcharges des méthodes :

  • schedule() : l'exécution suivante survient dès que le délai précisé est atteint après la fin de l'exécution précédente. Les déclenchements de l'exécution de la tâche ne sont donc pas fixes puisqu'ils dépendent du temps de l'exécution de la tâche précédente
  • scheduleAtFixedRate() : l'exécution est déterminée par rapport au début de la première exécution en utilisant la période de manière fixe. Si une tâche est plus longue que prévue c'est sans influence sur les exécutions suivantes à moins que le temps d'exécution dépasse le délai auquel cas l'exécution suivante attend l'achèvement de la tâche précédente
Exemple ( code Java 1.3 ) :
package fr.jmdoudoux.dej.timer;
			
import java.util.Timer;

public class TestTimer {
  public static void main(final String[] args) {
    Timer timer;
    timer = new Timer();
    timer.schedule(new MaTask(), 1000, 5000);
  }
}
Résultat :
Sun Jun 15 14:00:11 CEST 2014 Execution de ma tache
Sun Jun 15 14:00:16 CEST 2014 Execution de ma tache
Sun Jun 15 14:00:21 CEST 2014 Execution de ma tache
Sun Jun 15 14:00:26 CEST 2014 Execution de ma tache
Sun Jun 15 14:00:31 CEST 2014 Execution de ma tache
Sun Jun 15 14:00:36 CEST 2014 Execution de ma tache
Sun Jun 15 14:00:41 CEST 2014 Execution de ma tache
Sun Jun 15 14:00:46 CEST 2014 Execution de ma tache

Si une tâche est en cours d'exécution par le thread, le Timer attend la fin de son exécution pour exécuter la planification suivante même si le délai est largement dépassé.

Exemple ( code Java 1.3 ) :
package fr.jmdoudoux.dej.timer;

import java.util.Date;
import java.util.TimerTask;

public class MaTache extends TimerTask {
  @Override
  public void run() {
    System.out.println("Debut execution tache " + new Date());
    try {
      Thread.sleep(5000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("Fin execution tache " + new Date());
  }
}
Exemple ( code Java 1.3 ) :
package fr.jmdoudoux.dej.timer;
			
import java.util.Timer;
import java.util.TimerTask;

public class TestTimer {
  public static void main(final String[] args) {
    TimerTask timerTask = new MaTache();
    Timer timer = new Timer(true);
    timer.scheduleAtFixedRate(timerTask, 0, 2000);
    System.out.println("Lancement execution");

    try {
      Thread.sleep(20000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    timer.cancel();

    System.out.println("Abandon execution");
    try {
      Thread.sleep(20000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}
Résultat :
Lancement execution
Debut execution tache Sun Jun 15 14:25:12 CET 2014
Fin execution tache Sun Jun 15 14:25:17 CET 2014
Debut execution tache Sun Jun 15 14:25:17 CET 2014
Fin execution tache Sun Jun 15 14:25:22 CET 2014
Debut execution tache Sun Jun 15 14:25:22 CET 2014
Fin execution tache Sun Jun 15 14:25:27 CET 2014
Debut execution tache Sun Jun 15 14:25:27 CET 2014
Abandon execution
Fin execution tache Sun Jun 15 14:25:32 CET 2014

Dans l'exemple ci-dessus, l'exécution de la tâche est prévue toutes les deux secondes mais son temps d'exécution est de 5 secondes. Il faut s'assurer que le délai d'exécution de la tâche est inférieur au délai d'attente entre deux exécutions, sinon les exécutions vont s'empiler dans la queue.

Une surcharge de la méthode schedule() qui attend en paramètres un objet de type TimerTask et un de type Date permet de demander l'exécution de la tâche à la date/heure fournie en paramètre. Si celle-ci est dans le passé alors elle est exécutée immédiatement.

Exemple ( code Java 1.3 ) :
package fr.jmdoudoux.dej.timer;

import java.util.Calendar;
import java.util.Date;
import java.util.Timer;

public class TestTimer {

  public static void main(final String[] args) {

    Calendar calendar = Calendar.getInstance();
    calendar.set(Calendar.HOUR_OF_DAY, 18);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.SECOND, 0);
    Date time = calendar.getTime();

    Timer timer = new Timer();
    timer.schedule(new MaTask(), time);
  }
}

Une surcharge de la méthode schedule() qui attend en paramètres un objet de type TimerTask, un de type Date et un de type long permet de demander l'exécution répétée de la tâche à la date/heure fournie en paramètre. Le délai d'attente entre deux exécutions est précisé en millisecondes.

Exemple ( code Java 1.3 ) :
package fr.jmdoudoux.dej.timer;

import java.util.Calendar;
import java.util.Date;
import java.util.Timer;

public class TestTimer {

  public static final long VINGT_QUATRE_HEURES = 1000 * 60 * 60 * 24;

  public static void main(final String[] args) {
    Calendar calendar = Calendar.getInstance();
    calendar.set(Calendar.HOUR_OF_DAY, 18);
    calendar.set(Calendar.MINUTE, 00);
    calendar.set(Calendar.SECOND, 0);
    Date time = calendar.getTime();

    Timer timer = new Timer();
    timer.schedule(new MaTask(), time, VINGT_QUATRE_HEURES);
  }
}

La méthode cancel() permet d'arrêter le timer.

La classe Timer est thread-safe. Elle ne possède qu'un seul thread pour exécuter ses tâches planifiées. Il est donc important que le temps d'exécution des tâches soit le plus rapide possible pour ne pas éventuellement bloquer l'exécution d'autres tâches.

Le type de thread utilisé par le Timer influe sur la capacité de la JVM à s'arrêter :

  • par défaut, le thread est bloquant (il empêche l'arrêt de la JVM tant qu'il est en cours d'exécution) et pour l'arrêter il est nécessaire d'invoquer la méthode cancel()
  • démon : le thread n'empêche pas l'arrêt de la JVM

Si le thread du Timer n'est pas un démon, alors il est nécessaire pour arrêter l'application d'invoquer la méthode cancel() : celle-ci va supprimer toutes les planifications et permettre au thread de s'arrêter. Une fois la méthode cancel() invoquée, il n'est plus possible de replanifier l'exécution de tâches par le Timer : par exemple, l'invocation de la méthode schedule() lèvera une exception de type IllegalStateException.

L'utilisation d'un Timer est simple mais présente quelques limitations :

  • pas de planification complexe
  • exécution d'une seule tâche à la fois
  • pas de persistance des planifications et des tâches

 

111.1.2. Le ScheduledExecutorService

Java 5.0 propose, dans l'API Executor, le ScheduledExecutorService qui permet l'exécution planifiée et éventuellement répétée de tâches en utilisant un pool de threads.

Cette solution dispose de plusieurs avantages par rapport à l'utilisation d'un Timer :

  • utilise un pool de threads, ce qui améliore les performances notamment dans le cas de l'exécution en parallèle de plusieurs tâches
  • les délais peuvent être précisés avec plusieurs unités
  • les tâches à exécuter n'ont plus besoin d'hériter de la classe TimerTask

 

111.1.2.1. L'interface java.util.concurrent.ScheduledExecutorService

L'interface java.util.concurrent.ScheduledExecutorService hérite de l'interface ExecutorService. Elle définit les fonctionnalités pour planifier l'exécution d'une tâche après un certain délai ou son exécution répétée avec un intervalle de temps fixe.

L'exécution d'une tâche se fait de manière asynchrone dans un thread dédié.

La classe Executors est une fabrique qui permet de créer des instances de type Executor. Elle propose notamment plusieurs méthodes pour créer une instance de type ScheduledExecutorService.

Méthode

Rôle

static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

Renvoyer une instance de type ScheduledExecutorService qui utilise un pool de threads dont la taille est fournie en paramètre

static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)

Renvoyer une instance de type ScheduledExecutorService qui utilise un pool de threads dont la taille et la fabrique sont fournies en paramètres

static ScheduledExecutorService newSingleThreadScheduledExecutor()

Renvoyer une instance de type ScheduledExecutorService qui utilise un seul thread pour l'exécution des tâches

static ScheduledExecutorService newSingleThreadScheduledExecutor( ThreadFactory threadFactory)

Renvoyer une instance de type ScheduledExecutorService qui utilise un seul thread pour l'exécution des tâches

static ScheduledExecutorService unconfigurableScheduledExecutorService( ScheduledExecutorService executor)

Renvoyer une instance de type ScheduledExecutorService qui délègue les invocations de ses méthodes vers celles de l'instance fournie en paramètre


Il est possible d'utiliser la méthode execute() héritée de l'interface Executor ou des surcharges de la méthode submit() héritée de l'interface ExecutorService pour demander l'exécution immédiate d'une tâche.

Elle définit aussi plusieurs méthodes qui peuvent avoir en paramètres un délai et ou une période relative mais pas de valeur absolue comme une date ou un timestamp :

Méthode

Rôle

<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)

Planifier l'exécution du Callable fourni en paramètre après le délai précisé

ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)

Planifier l'exécution du Runnable fourni en paramètre après le délai précisé

ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)

Planifier l'exécution du Runnable fourni en paramètre après le délai précisé. La prochaine exécution est déterminée en ajoutant l'initialDelay et la période multipliée par le nombre d'exécutions

ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)

Planifier l'exécution du Runnable fourni en paramètre après le délai précisé. La prochaine exécution est déterminée en ajoutant le delay au timestamp de la fin de la dernière exécution


Les méthodes qui permettent de demander une planification renvoient une instance de type ScheduledFuture. Cette interface hérite des interfaces Delayed et Future : elle permet de demander l'annulation de l'exécution, de déterminer si elle est terminée et d'obtenir le résultat de l'exécution.

Lors de l'exécution d'un Runnable, comme il ne peut pas renvoyer de valeur, l'invocation de la méthode get() du ScheduledFuture renvoie toujours null lorsque l'exécution de la tâche est terminée.

 

111.1.2.2. L'interface java.util.concurrent.ScheduledFuture

Cette interface hérite des interfaces Comparable<Delayed>, Delayed et Future<V>.

Une instance de type ScheduledFuture permet de gérer le statut de l'exécution d'une tâche planifiée : déterminer si elle est terminée, obtenir le résultat, annuler l'exécution, ... C'est notamment une instance de ce type qui est retournée par les méthodes de l'interface ScheduledExecutorService.

 

111.1.2.3. L'utilisation d'un ScheduledExecutorService

Java 5.0 propose la classe java.util.concurrent.ScheduledThreadPoolExecutor qui implémente l'interface ScheduledExecutorService et hérite de la classe ThreadPoolExecutor.

Elle permet d'exécuter des tâches après un délai d'attente ou de manière périodique en utilisant un pool de threads.

La classe ScheduledExecutorService est un ExecutorService qui peut planifier l'exécution de tâches après un délai d'attente ou l'exécution répétée de tâches avec un intervalle de temps fixe entre chaque exécution. Les tâches sont exécutées de manière asynchrone par un des threads du pool.

Pour obtenir une instance de type ScheduledExecutorService, il est possible :

  • d'invoquer un des constructeurs de la classe ScheduledThreadPoolExecutor 
  • d'utiliser des méthodes de la classe Executors qui sont des fabriques pour obtenir une instance de type ScheduledThreadPoolExecutor (newSingleThreadExecutor() qui permet de créer un seul thread pour l'exécution ou newFixedThreadPool() qui permet de créer un pool de threads dont la taille est fournie en paramètre)
Exemple :
package fr.jmdoudoux.dej.executor;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;

public class TestScheduledExecutorService {

  public static void main(String[] args) {
    ScheduledExecutorService ses1 = new ScheduledThreadPoolExecutor(10);
    ScheduledExecutorService ses2 = Executors.newScheduledThreadPool(10);
  }
}

La méthode schedule() permet de demander l'exécution d'une tâche éventuellement différée d'un certain délai initial.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.executor;

import static java.util.concurrent.TimeUnit.SECONDS;

import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;

public class TestScheduledExecutorService {

  public static void main(String[] args) {
    final ScheduledExecutorService executor = Executors
        .newScheduledThreadPool(2);

    final Runnable maTache = new Runnable() {
      public void run() {
        System.out.println("Ma tache (" + Thread.currentThread().getName()
            + ") " + new Date());
      }
    };

    ScheduledFuture<String> maTacheFuture = executor.schedule(
        new Callable<String>() {
          @Override
          public String call() throws Exception {
            System.out.println("Execution de la tache ("
                + Thread.currentThread().getName() + ") " + new Date());
            return "Resultat de la tache";
          }
        }, 10, SECONDS);

    System.out.println("Autre traitement (" + Thread.currentThread().getName()
        + ") " + new Date());

    try {
      System.out.println("Resultat = " + maTacheFuture.get());
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (ExecutionException e) {
      e.printStackTrace();
    }

    executor.shutdown();
  }
}
Résultat :
Autre traitement (main) Sun Jun 15 17:42:45 CET 2014
Execution de la tache (pool-1-thread-1) Sun Jun 15 17:42:55 CET 2014
Resultat = Resultat de la tache

La méthode scheduleAtFixedRate() permet de demander l'exécution périodique d'une tâche. La première exécution peut être différée d'un certain délai. La période est utilisée à partir de la première exécution pour déterminer l'exécution suivante en ajoutant la période à l'heure de début de l'exécution. Si le temps d'exécution de la tâche est supérieur à la période lors l'exécution suivante est effectuée immédiatement. La tâche ne sera exécutée que par un seul thread.

La tâche est exécutée périodiquement jusqu'à ce qu'une des exécutions lève une exception ou que la méthode shutdown() ou shutdownNow() du ScheduleExecutorService soit invoquée.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.executor;

import static java.util.concurrent.TimeUnit.SECONDS;

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;

public class TestScheduledExecutorService {

  public static void main(String[] args) {
    final ScheduledExecutorService executor = Executors
        .newScheduledThreadPool(2);

    final Runnable maTache = new Runnable() {
      public void run() {
        System.out.println("Ma tache (" + Thread.currentThread().getName()
            + ") " + new Date());
      }
    };

    final ScheduledFuture<?> maTacheFuture = executor.scheduleAtFixedRate(
        maTache, 10, 10, SECONDS);
    final ScheduledFuture<?> maTacheFuture2 = executor.scheduleAtFixedRate(
        maTache, 5, 10, SECONDS);

    Runtime.getRuntime().addShutdownHook(new Thread() {
      public void run() {
        executor.shutdown();
      }
    });
  }
}

Pour planifier l'exécution d'une tâche de manière répétée durant une certaine période, il faut planifier l'exécution de la tâche et l'exécution d'une autre tâche déclenchée à la fin du délai dont le but est d'arrêter l'exécution de la tâche planifiée.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.executor;

import static java.util.concurrent.TimeUnit.SECONDS;

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;

public class TestScheduledExecutorService {

  public static void main(String[] args) {
    final ScheduledExecutorService executor = Executors
        .newScheduledThreadPool(2);

    final Runnable maTache = new Runnable() {
      public void run() {
        System.out.println("Ma tache " + new Date());
      }
    };

    final ScheduledFuture<?> maTacheFuture = executor.scheduleAtFixedRate(
        maTache, 10, 10, SECONDS);

    executor.schedule(new Runnable() {
      public void run() {
        maTacheFuture.cancel(true);
        executor.shutdown();
        System.out.println("Arret de l'executor");
      }
    }, 60, SECONDS);
  }
}

La méthode scheduleAtFixedDelay() permet de demander l'exécution périodique d'une tâche. La première exécution peut être différée d'un certain délai initial. La prochaine exécution est déterminée en ajoutant le délai à l'heure de fin d'exécution de la tâche.

Les exécutions successives d'une tâche planifiée en utilisant les méthodes scheduleAtFixedRate() ou scheduleWithFixedDelay() ne se chevauchent pas.

Comme tout ExecutorService, il est important d'invoquer la méthode shutdown() ou shutdownNow() lorsqu'il n'est plus utilisé pour arrêter le thread qui lui est associé.

Par défaut, si une tâche est annulée avant son invocation alors son exécution est supprimée mais elle reste dans la pile des tâches. Pour supprimer les tâches annulées de la pile, il faut au préalable avoir invoqué la méthode setRemoveOnCancelPolicy() en lui passant la valeur true en paramètre.

 

111.2. Quartz

Quartz est une bibliothèque open source largement utilisée pour planifier l'exécution de tâches. C'est une des bibliothèques de planification open source les plus populaires.

Quartz utilise plusieurs concepts

  • scheduler : c'est le moteur de gestion de la planification
  • job : c'est une tâche à exécuter
  • trigger : c'est un déclencheur qui permet de définir la planification d'exécution d'un job
  • calendar : définir des moments dans le temps où la planification est omise
  • listener : permettre de s'abonner à des événements

Un job peut être associé à plusieurs triggers mais un trigger n'est associé qu'à un seul job.

Quartz peut être téléchargé à l'url : http://www.quartz-scheduler.org/downloads/

Quartz possède une dépendance sur la bibliothèque slf4j.

Plusieurs versions de Quartz ont été diffusées :

  • 1.7.2, 1.7.3 (février 2010) :
  • 1.8.0, 1.8.1, 1.8.2, 1.8.3, 1.8.4, 1.8.5, 1.8.6 (novembre 2012) :
  • 2.0.0, 2.0.1, 2.0.2 (mai 2011) :
  • 2.1.0, 2.1.1, 2.1.2, 2.1.3, 2.1.4, 2.1.5, 2.1.6, 2.1.7 (août 2013) :
  • 2.2.0, 2.2.1 (septembre 2013) :

Attention : il existe de grosses différences entre les API de la version 1.x et 2.x de Quartz notamment des classes qui sont devenues des interfaces.

 

111.2.1. Un exemple simple

Cet exemple va simplement afficher un message sur la console toutes les 5 secondes.

Il faut ajouter les dépendances requises (quartz, log4j et slf4j-log4j) au classpath, par exemple avec Maven.

Exemple :
<project xmlns="http://maven.apache.org/POM/4.0.0" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
      http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>fr.jmdoudoux.dej.quartz</groupId>
  <artifactId>TestQuartz</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  
  <dependencies> 
    <dependency> 
      <groupId>org.quartz-scheduler</groupId> 
      <artifactId>quartz</artifactId> 
      <version>2.2.1</version> 
    </dependency>    
    <dependency> 
      <groupId>log4j</groupId> 
      <artifactId>log4j</artifactId> 
      <version>1.2.17</version> 
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.6.6</version>
    </dependency>  
 </dependencies>
</project>

Il faut définir les traitements à exécuter dans une classe qui implémente l'interface org.quartz.Job.

Exemple :
package fr.jmdoudoux.dej.quartz;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class MonJob implements Job {

  @Override
  public void execute(final JobExecutionContext context) throws JobExecutionException {
    System.out.println("Execution de mon job");
  }
}

La mise en oeuvre de Quartz pour exécuter une tâche de manière répétitive suit plusieurs étapes :

  • invoquer la fabrique pour obtenir une instance de type Scheduler
  • créer une instance de type JobDetail
  • créer une instance de type Trigger
  • démarrer le scheduler
  • enregistrer l'association du job et du trigger dans le scheduler
Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;
import java.util.Date;

import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.impl.JobDetailImpl;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.triggers.SimpleTriggerImpl;

public class TestQuartz {

  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();

      final JobDetailImpl jobDetail = new JobDetailImpl();
      jobDetail.setName("Mon job");
      jobDetail.setJobClass(MonJob.class);

      final SimpleTriggerImpl simpleTrigger = new SimpleTriggerImpl();
      simpleTrigger.setStartTime(new Date(System.currentTimeMillis() + 1000));
      simpleTrigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
      simpleTrigger.setRepeatInterval(5000);
      simpleTrigger.setName("Trigger execution toutes les 5 secondes");

      scheduler.start();
      scheduler.scheduleJob(jobDetail, simpleTrigger);

      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

 

111.2.2. Les principales classes et interfaces

Quartz propose une API dont les principales classes et interfaces sont :

  • Scheduler : permet d'interagir avec le moteur de planification
  • Job : interface qui doit être implémentée par tous les jobs
  • JobDetail : permet de définir des instances de type Job
  • Trigger : permet de définir un ou plusieurs déclenchements
  • JobExecutionContext : interface qui permet un accès aux différentes entités du context d'exécution d'un job
  • SchedulerFactory : interface qui décrit les fonctionnalités d'une fabrique d'instances de Scheduler (par défaut, c'est l'implémentation StdSchedulerFactory qui est utilisée)

Depuis la version 2.0, Quartz propose plusieurs builders qui facilitent la création d'instances de différents types grâce à l'utilisation de fluent API :

  • JobBuilder : builder pour créer des instances de type JobDetail
  • TriggerBuilder : builder pour créer des instances de type Trigger
  • DateBuilder : builder pour faciliter la création d'instances de type Date
  • ScheduleBuilder (SimpleScheduleBuilder, CronScheduleBuilder, CalendarIntervalScheduleBuilder, DailyTimeIntervalScheduleBuilder) : builder pour créer une planification

Chaque builder propose une ou plusieurs méthodes statiques. Il est possible de faire un import static de ces méthodes pour faciliter leur utilisation :

Exemple :
import static org.quartz.JobBuilder.*;
import static org.quartz.SimpleScheduleBuilder.*;
import static org.quartz.CronScheduleBuilder.*;
import static org.quartz.CalendarIntervalScheduleBuilder.*;
import static org.quartz.TriggerBuilder.*;
import static org.quartz.DateBuilder.*;

La classe DateBuilder propose des méthodes pour faciliter la création d'instances de type java.util.Date : elle est particulièrement utile pour créer une instance de type Date en fonction de différents critères et/ou de données fournies ce qui est pratique lors de la définition de triggers notamment pour le calcul de dates de début et de fin.

Méthode

Rôle

static Date dateOf(int hour, int minute, int second)

Obtenir une instance de type Date à partir de l'heure précisée en paramètre

static Date dateOf(int hour, int minute, int second, int dayOfMonth, int month)

Obtenir une instance de type Date à partir des éléments précisés en paramètres et l'année courante

static Date dateOf(int hour, int minute, int second, int dayOfMonth, int month, int year)

Obtenir une instance de type Date à partir des différents éléments passés en paramètre

static Date evenHourDate(Date date)

Renvoyer la prochaine heure à partir de celle fournie en paramètre

static Date evenHourDateAfterNow()

Renvoyer la prochaine heure à partir de maintenant

static Date evenHourDateBefore(Date date)

Renvoyer la précédente heure à partir de celle fournie en paramètre

static Date evenMinuteDate(Date date)

Renvoyer la prochaine minute à partir de celle fournie en paramètre

static Date evenMinuteDateAfterNow()

Renvoyer la prochaine minute à partir de celle fournie en paramètre

static Date evenMinuteDateBefore(Date date)

Renvoyer la précédente minute à partir de celle fournie en paramètre

static Date evenSecondDate(Date date)

Renvoyer la prochaine seconde à partir de celle fournie en paramètre

static Date evenSecondDateAfterNow()

Renvoyer la prochaine seconde à partir de maintenant

static Date evenSecondDateBefore(Date date)

Renvoyer la précédente seconde à partir de celle fournie en paramètre

static Date futureDate(int interval, DateBuilder.IntervalUnit unit)

Obtenir la date correspondant à maintenant plus l'intervalle de temps fourni en paramètre (durée et unité)

static Date translateTime(Date date, TimeZone src, TimeZone dest)

Obtenir une instance de type Date qui encapsule la date fournie en paramètre relative au fuseau horaire passé en paramètre


Généralement, les builders proposent une valeur par défaut pour les propriétés qui ne sont pas explicitement valorisées.

 

111.2.3. Le scheduler

Le scheduler est le coeur de Quartz. Il stocke un ensemble de JobDetail et de Trigger : il a la charge de déclencher les Trigger selon leur configuration et d'exécuter les Job qui lui sont associés.

Ces fonctionnalités sont décrites dans l'interface org.quartz.Scheduler. Elle propose plusieurs méthodes pour :

  • gérer le cycle de vie du scheduler
  • gérer les JobDetail et les Trigger qu'elle contient
  • accéder au ListenerManager qui permet d'enregistrer des listeners sur des événements

Pour obtenir une instance de type Scheduler, il faut utiliser une fabrique de type SchedulerFactory.

Quartz fournit en standard la classe org.quartz.impl.StdSchedulerFactory qui implémente l'interface SchedulerFactory pour créer des instances à partir d'une configuration sous la forme d'un fichier properties. Cette fabrique recherche un fichier de configuration pour paramétrer l'instance créée. Si ce fichier facultatif n'est pas trouvé alors c'est une instance avec la configuration par défaut qui est créée.

La configuration peut aussi être fournie à la fabrique en invoquant une des surcharges de la méthode initialize() qui attend en paramètre :

  • un objet de type Properties qui contient la configuration
  • une chaîne de caractères qui précise le fichier properties à utiliser
  • un InputStream qui permet de lire le flux d'un fichier properties
  • sans paramètre : la configuration est chargée du fichier précisé par la variable d'environnement org.quartz.properties de la JVM. Si elle n'est pas définie, la configuration est recherchée dans un fichier quartz.properties qui doit être dans le répertoire de travail ou dans le classpath. S'il n'est pas trouvé, alors c'est celui contenu dans la bibliothèque quartz.jar qui est utilisé pour configurer la nouvelle instance

A sa création, une instance de type Scheduler est en mode stand-by : elle doit être démarrée en invoquant la méthode start().

Exemple :
  SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
  Scheduler sched = schedFact.getScheduler();
  sched.start();

Tant que le scheduler n'est pas démarré aucun trigger n'est déclenché.

Pour arrêter un scheduler, il faut invoquer une des surcharges de la méthode shutdown() :

  • en passant en paramètre la valeur true, le scheduler va attendre que tous les jobs en cours d'exécution soient terminés avant que la méthode shutdown() ne se termine
  • en passant la valeur false ou aucun paramètre, le scheduler s'arrête mais les jobs en cours d'exécution se poursuivent

Dès qu'un scheduler est éteint en invoquant sa méthode shutdown(), il ne peut pas être redémarré : il est nécessaire de créer une nouvelle instance.

Il est possible de mettre le scheduler en pause en utilisant la méthode standby(). Pour le réactiver, il faut de nouveau invoquer la méthode start().

Pour démarrer Quartz dans une webapp, il y a deux solutions :

  • utiliser un ServletContextListener de type QuartzInitializerListener
  • utiliser une servlet de type QuartzInitializerServlet

La première solution consiste à déclarer un ServletContextListener de type QuartzInitializerListener. Plusieurs paramètres de configuration peuvent être définis dans les paramètres du contexte.

Exemple :
     <context-param>
         <param-name>quartz:config-file</param-name>
         <param-value>/mawebapp/config/webapp_quartz.properties</param-value>
     </context-param>
     <context-param>
         <param-name>quartz:shutdown-on-unload</param-name>
         <param-value>true</param-value>
     </context-param>
     <context-param>
         <param-name>quartz:wait-on-shutdown</param-name>
         <param-value>false</param-value>
     </context-param>
     <context-param>
         <param-name>quartz:start-scheduler-on-load</param-name>
         <param-value>true</param-value>
     </context-param>

     <listener>
         <listener-class>
             org.quartz.ee.servlet.QuartzInitializerListener
         </listener-class>
     </listener>

Plusieurs paramètres d'initialisation peuvent être définis dans le contexte :

  • quartz:config-file permet de préciser le chemin du fichier properties de configuration de Quartz. Par défaut, quartz.properties
  • quartz:shutdown-on-unload : booléen qui précise si la méthode shutdown() du scheduler doit être invoquée lorsque la webapp est déchargée. La valeur par défaut est true
  • quartz:wait-on-shutdown : lorsque quartz:shutdown-on-unload vaut true, permet de préciser un booléen qui sera passé en paramètre de la méthode shutdown(). La valeur par défaut est false
  • quartz:start-on-load : booléen qui permet de préciser si la méthode start() du scheduler sera invoquée lorsque la webapp est chargée. La valeur par défaut est true
  • quartz:servlet-context-factory-key : permet de préciser le nom de la clé du contexte qui va contenir l'instance de la fabrique
  • quartz:scheduler-context-servlet-context-key : permet de préciser le nom de la clé dans le SchedulerContext qui sera associé à l'instance de type ServletContext
  • quartz:start-delay-seconds : permet de préciser un délai entre l'initialisation du scheduler et l'invocation de sa méthode start()

La seconde solution est de déclarer une servlet de type org.quartz.ee.servlet.QuartzInitializerServlet en demandant son chargement au démarrage de la webapp.

Exemple :
    <servlet> 
      <servlet-name>QuartzInitializer</servlet-name> 
      <servlet-class>org.quartz.ee.servlet.QuartzInitializerServlet</servlet-class> 
      <init-param>
        <param-name>shutdown-on-unload</param-name>
        <param-value>true</param-value>
      </init-param>
      <load-on-startup>1</load-on-startup>
    </servlet>

La servlet va stocker une instance de type StdSchedulerFactory dans le contexte associé par défaut à la clé QuartzFactoryServlet.QUARTZ_FACTORY_KEY.

Exemple :
     StdSchedulerFactory factory = (StdSchedulerFactory) ctx
                .getAttribute(QuartzFactoryServlet.QUARTZ_FACTORY_KEY);

La servlet possède plusieurs paramètres :

  • config-file qui permet de préciser le chemin du fichier de configuration. La valeur par défaut est quartz.properties
  • shutdown-on-unload : valeur booléenne qui permet de préciser si la méthode shutdown() du scheduler doit être invoquée lorsque la servlet est déchargée. La valeur par défaut est true
  • wait-on-shutdown : lorsque shutdown-on-unload vaut true, permet de préciser un booléen qui sera passé en paramètre de la méthode shutdown(). La valeur par défaut est false
  • start-scheduler-on-load : booléen qui permet de préciser si la méthode start() du scheduler sera invoquée lorsque la servlet est chargée. La valeur par défaut est true
  • servlet-context-factory-key permet de préciser le nom de la clé du contexte qui va contenir l'instance de la fabrique
  • scheduler-context-servlet-context-key permet de préciser le nom de la clé dans le SchedulerContext qui sera associée à l'instance de type ServletContext
  • start-delay-seconds permet de préciser un délai entre l'initialisation du scheduler et l'invocation de sa méthode start()

 

111.2.4. Les Jobs et les Triggers

Les Triggers permettent de définir la condition de déclenchement de l'exécution d'un job. Un trigger peut être associé à un objet de type JobDataMap qui permet de passer à un job des paramètres spécifiques au déclenchement.

Quartz propose plusieurs implémentations de l'interface Trigger notamment SimpleTrigger et CronTrigger.

La classe SimpleTrigger permet de définir la condition :

  • d'une seule exécution à un moment donné
  • de plusieurs exécutions espacées chacune d'une même durée

La classe CronTrigger permet de définir des conditions d'exécution basées sur le calendrier. Par exemple : exécution tous les jours à 08:00, exécution tous les lundis à 23:00, ...

Pour respecter le principe SOC (Separation of Concern), l'interface Job encapsule les traitements et l'interface Trigger encapsule les conditions de déclenchement d'une ou plusieurs exécutions.

Un Job peut être créé et stocké dans le scheduler sans être associé à un Trigger. Cela permet par exemple de conserver un job dans le scheduler après que tous les triggers qui lui étaient associés soient expirés : il est possible de les replanifier en les associant à de nouveaux triggers. Plusieurs triggers peuvent être associés à un même job.

Les jobs et les triggers sont identifiés par une clé (respectivement JobKey et TriggerKey) lorsqu'ils sont enregistrés dans le scheduler.

Ils peuvent être regroupés dans des catégories par exemple « reporting »,  « mise à jour », « maintenance », ...

Le nom de la clé doit être unique dans un même groupe.

 

111.2.5. Les jobs

Les traitements à exécuter sont encapsulés dans une classe qui implémente l'interface org.quartz.Job.

L'interface Job ne définit qu'une seule méthode execute().

Exemple :
package fr.jmdoudoux.dej.quartz;
			
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class MonJob implements Job {
  @Override
  public void execute(final JobExecutionContext context) throws JobExecutionException {
    System.out.println("Execution de mon job");
  }
}

Il ne faut pas créer une instance de type Job directement mais créer une instance de type JobDetail qui va permettre à Quartz de créer et gérer une instance.

La classe JobDetailImpl implémente l'interface JobDetail.

Exemple :
      final JobDetailImpl jobDetail = new JobDetailImpl();
      jobDetail.setName("Mon job");
      jobDetail.setJobClass(MonJob.class);

Il faut au minimum définir le nom qui est l'identifiant et la classe de type Job qui encapsule les traitements.

Il est possible d'associer des paramètres au job en utilisant une instance de type org.quartz.JobDataMap.

Exemple :
      JobDataMap map = new JobDataMap();
      map.put("monParametre","12345");
      jobDetail.setJobDataMap(map);

Il est alors possible de récupérer ces paramètres dans la méthode execute() du job en invoquant les méthodes getJobDetail() et getJobDataMap() du paramètre de type JobExecutionContext.

Exemple :
package fr.jmdoudoux.dej.quartz;
			
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class MonJob implements Job {
  @Override
  public void execute(final JobExecutionContext context) throws JobExecutionException {
    final JobDataMap map = context.getJobDetail().getJobDataMap();
    final String monParametre = map.getString("monParametre");
    System.out.println("Execution de mon job pour " + monParametre);
  }
}

La classe JobDetailImpl possède plusieurs constructeurs qui sont tous deprecated sauf le constructeur par défaut. Plutôt que d'utiliser ces constructeurs, il est préférable d'utiliser la classe JobBuilder qui implémente le motif de conception monteur.

Elle définit plusieurs méthodes pour configurer et obtenir l'instance de type JobDetail.

Méthode

Rôle

JobDetail build()

Obtenir l'instance de type JobDetail

static JobBuilder newJob()

Fabrique pour obtenir l'instance de type JobBuilder

static JobBuilder newJob(Class<? extends Job> jobClass)

Fabrique pour obtenir l'instance de type JobBuilder en passant en paramètre la classe qui implémente l'interface Job

JobBuilder ofType(Class<? extends Job> jobClazz)

Préciser la classe qui implémente l'interface Job

JobBuilder requestRecovery()

Préciser au scheduler de réexécuter le job si une situation de recovery ou de fail-over survient

JobBuilder requestRecovery(boolean jobShouldRecover)

Préciser au scheduler de réexécuter ou non le job si une situation de recovery ou de fail-over survient

JobBuilder setJobData(JobDataMap newJobDataMap)

Préciser les paramètres associés au job

JobBuilder storeDurably()

Préciser au scheduler de conserver le job si celui-ci n'est plus associé à un trigger

JobBuilder storeDurably(boolean jobDurability)

Préciser au scheduler de conserver ou non le job si celui-ci n'est plus associé à un trigger

JobBuilder usingJobData(JobDataMap newJobDataMap)

Ajouter les paramètres fournis à ceux associés au job

JobBuilder usingJobData(String dataKey, Boolean value)
JobBuilder usingJobData(String dataKey, Double value)
JobBuilder usingJobData(String dataKey, Float value)
JobBuilder usingJobData(String dataKey, Integer value)
JobBuilder usingJobData(String dataKey, Long value)
JobBuilder usingJobData(String dataKey, String value)

Ajouter un paramètre au JobDataMap

JobBuilder withDescription(String jobDescription)

Préciser une description

JobBuilder withIdentity(JobKey jobKey)

Préciser l'identifiant du job

JobBuilder withIdentity(String name)

Préciser le nom du job qui sera utilisé dans son identifiant

JobBuilder withIdentity(String name, String group)

Préciser le nom du job et le nom de son groupe qui seront utilisés dans son identifiant


La méthode newJob() est une fabrique pour créer une instance de type JobBuilder. Il suffit alors d'invoquer les différentes méthodes pour configurer le builder et invoquer sa méthode build() pour obtenir l'instance correspondante.

Exemple :
      final JobDetail jobDetail = JobBuilder
          .newJob(MonJob.class)
          .withIdentity("monJob", "groupe_1")
          .usingJobData("monParametre", "12345")
          .build();

Les données contenues dans le JobDataMap sont partagées entre les différentes exécutions d'un même Trigger. Il est donc nécessaire de s'assurer de la concurrence d'accès pour mettre à jour ces données.

Depuis la version 2.0, il faut annoter la classe du job avec l'annotation @PersistJobDataAfterExecution pour préciser que le job met à jour les données partagées dans son contexte.

L'interface StatefulJob doit être implémentée par la classe d'un job pour préciser qu'il n'est pas possible d'exécuter le job en parallèle. Si plusieurs Triggers déclenchent l'exécution du job en même temps, ceux-ci seront exécutés les uns après les autres.

Depuis la version 2.0, cette interface est deprecated : il est préférable d'annoter la classe du Job avec l'annotation @DisallowConcurrentExecution. Pour maintenir la compatibilité, l'interface StatefulJob est annotée avec @DisallowConcurrentExecution et @PersistJobDataAfterExecution.

La classe JobExecutionContext encapsule des informations liées à l'exécution du job notamment en permettant un accès aux instances du Scheduler, du Trigger et du JobDetail.

La classe JobDetail encapsule la configuration du job à fournir au scheduler.

A chaque fois que l'exécution d'un job est déclenchée, le moteur de Quartz va créer une nouvelle instance à partir du type fourni en paramètre dans le JobDetail en utilisant par défaut la fabrique JobFactory.

C'est la raison pour laquelle, la classe qui implémente :

  • doit avoir un constructeur par défaut
  • ne doit pas avoir de membres pour conserver son état

L'échange de données entre plusieurs exécutions peut se faire en utilisant une instance de type JobDataMap.

Toutes les données stockées dans un JobDataMap doivent être sérialisables.

Exemple :
package fr.jmdoudoux.dej.quartz;

import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;

public class MonJob implements Job {

  @Override
  public void execute(final JobExecutionContext context) throws JobExecutionException {
    final JobDataMap map = context.getJobDetail().getJobDataMap();
    final String monParametre = map.getString("monParametre");

    final JobKey key = context.getJobDetail().getKey();

    System.out.println("Execution de '" + key + "' pour " + monParametre);
  }
}

Dans la classe qui encapsule le job, il est possible de définir des setters pour des propriétés correspondants aux noms des paramètres passés. L'implémentation de JobFactory va alors par introspection invoquer ses setters en leur passant en paramètres les valeurs correspondantes au moment de la création de l'instance. Ceci évite d'avoir à obtenir l'instance de type JobDataMap du context et d'extraire les valeurs.

Exemple :
package fr.jmdoudoux.dej.quartz;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;

public class MonJob implements Job {

  private String monParametre;

  @Override
  public void execute(final JobExecutionContext context) throws JobExecutionException {

    final JobKey key = context.getJobDetail().getKey();

    System.out.println("Execution de '" + key + "' pour " + this.monParametre);
  }

  public void setMonParametre(final String monParametre) {
    this.monParametre = monParametre;
  }
}

Les données contenues dans une instance de type JobDataMap obtenue à partir du contexte contiennent les paramètres du JobDataMaps, du JobDetail et du Trigger. Si deux paramètres ont le même nom c'est celui du trigger qui est prioritaire.

L'annotation @DisallowConcurrentExecution utilisée sur une classe qui encapsule un job permet de préciser à Quartz de ne pas exécuter de manière concurrente plusieurs instances d'un même JobDetail utilisant la classe. La discrimination ne se fait donc pas sur une instance de la classe mais sur une instance de type JobDetail.

L'annotation @PersistJobDataAfterExecution utilisée sur une classe qui encapsule un job permet de préciser à Quartz de mettre à jour les données de la copie de JobDataMap après que la méthode execute() a été correctement exécutée (aucune exception n'est levée durant ses traitements). La prochaine exécution du JobDetail recevra les données mises à jour dans son instance de type JobDataMap. Cette annotation s'utilise sur la classe du job mais s'applique par instance de type JobDetail.

Lorsque l'annotation @PersistJobDataAfterExecution est utilisée, il est fortement recommandé d'utiliser aussi l'annotation @DisallowConcurrentExecution pour éviter des problèmes liés aux valeurs sauvegardées dans le JobDataMap à la fin de l'exécution concurrente de JobDetail.

Un JobDetail contient plusieurs propriétés :

  • Durability : un job qui n'est pas durable est automatiquement retiré du scheduler lorsque plus aucun trigger actif ne lui est associé
  • RequestsRecovery : permet de demander la réexécution d'un job qui était en cours d'exécution alors que le scheduler ou la JVM ont été arrêté inopinément. Lors de la réexécution, la méthode JobExecutionContext.isRecovering() renvoie true

Par défaut, le scheduler supprime les jobs qui ne sont plus associés à aucun trigger : pour demander au scheduler de les conserver, il faut invoquer la méthode setDurability(true) du JobDetail correspondant.

En cas de soucis dans les traitements de la méthode execute(), il faut uniquement lever une exception de type JobExecutionException. Généralement le code de la méthode est contenu dans un try/catch global à la méthode pour catcher les exceptions et les chainer dans une nouvelle exception de type JobExecutionException.

L'exception JobExecutionException possède trois propriétés qui seront utilisées par le scheduler soit pour réexécuter le JobDetail soit pour annuler la planification:

  • refireImmediately : réexécuter le JobDetail avec le même JobExecutionContext
  • unscheduleAllTriggers :
  • unscheduleFiringTrigger :

Si la propriété refireImmediately est à true alors les deux autres propriétés sont ignorées.

 

111.2.5.1. Les jobs fournis par Quartz

Quartz fournit en standard plusieurs implémentations de type Job permettant de répondre à quelques besoins basiques.

Pour utiliser ces jobs, il faut ajouter la bibliothèque quartz-jobs-x.y.z.jar au classpath.

La classe org.quartz.jobs.ee.mail.SendMailJob est une implémentation d'un job qui permet l'envoi d'un mail.

Exemple :
JobDetail jobDetail = new JobDetail("emailJob", Scheduler.DEFAULT_GROUP, SendMailJob.class); 
JobDataMap map = new JobDataMap(); 
map.put(SendMailJob.PROP_SMTP_HOST,"smtp.test.fr"); 
map.put(SendMailJob.PROP_SENDER,"emetteur@test.fr"); 
map.put(SendMailJob.PROP_RECIPIENT,"destinataire@test.fr"); 
map.put(SendMailJob.PROP_SUBJECT,"Sujet du mail"); 
map.put(SendMailJob.PROP_MESSAGE,"Corps du mail"); 
jobDetail.setJobDataMap(map);

L'utilisation de ce job requiert que les bibliothèques javamail et activation soient incluses dans le classpath.

La classe org.quartz.jobs.NoOpJob est un Job qui ne fait rien. Il peut être utile pour permettre de déclencher des traitements exécutés par des listeners sur le trigger ou le job.

La classe org.quartz.jobs.NativeJob permet d'exécuter une commande sur le système d'exploitation.

Exemple :
      final JobDataMap map = new JobDataMap();
      map.put(org.quartz.jobs.NativeJob.PROP_COMMAND, "c:\\windows\\notepad.exe");
      map.put("PROP_WAIT_FOR_PROCESS", "true");

      final JobDetail jobDetail = JobBuilder.newJob(NativeJob.class)
        .withIdentity("monJob").usingJobData(map).build();

La classe org.quartz.jobs.ee.ejb.EJBInvokerJob permet d'invoquer un EJB.

Les informations nécessaires pour connaitre l'EJB à invoquer doivent être fournies dans le JobDataMap associé au job en utilisant les clés :

  • EJB_JNDI_NAME_KEY: le nom JNDI de l'interface home de l'EJB
  • EJB_METHOD_KEY: le nom de la méthode de l'EJB à invoquer
  • EJB_ARGS_KEY: un tableau contenant les arguments à fournir lors de l'invocation de la méthode de l'EJB
  • EJB_ARG_TYPES_KEY: un tableau contenant les types des arguments fournis à la méthode de l'EJB
  • INITIAL_CONTEXT_FACTORY (optionnel) : le nom pleinement qualifié de la classe de type ContextFactory à utiliser
  • PROVIDER_URL (optionnel) : URL d'accès au conteneur d'EJB

La classe org.quartz.jobs.ee.jmx.JMXInvokerJob permet d'invoquer un MBean.

Les informations nécessaires pour connaitre le MBean à invoquer doivent être fournies dans le JobDataMap associé au job en utilisant les clés : JMX_OBJECTNAME, JMX_METHOD et JMX_PARAMDEFS.

Attention : ce job ne peut invoquer qu'un MBean qui soit enregistré sur un serveur de MBean de la même JVM que le scheduler.

Les classes org.quartz.jobs.ee.jms.SendDestinationMessageJob, org.quartz.jobs.ee.jms.SendQueueMessageJob et org.quartz.jobs.ee.jms.SendTopicMessageJob permettent d'envoyer un message JMS respectivement vers une destination, une queue ou un topic.

Les informations nécessaires à l'émission du message doivent être fournies dans le JobDataMap associé au job.

Attention : cette classe n'est utilisable qu'à partir de JMS 1.1.

La classe org.quartz.jobs.FileScanJob permet de vérifier si la date de dernière modification d'un fichier a été modifiée par rapport à la précédente invocation. Si le fichier a été modifié, un FileScanListener est invoqué.

Les informations nécessaires à l'exécution de ce job doivent être fournies dans le JobDataMap associé au job en utilisant les clés :

  • FILE_NAME : chemin et nom du fichier à surveiller
  • FILE_SCAN_LISTENER_NAME : nom du FileScanListener à invoquer
  • MINIMUM_UPDATE_AGE : durée en millisecondes qui doit s'être écoulée après la date/heure de dernière modification pour considérer le fichier comme modifié

 

111.2.6. Les triggers

Quartz propose plusieurs implémentations de type Trigger pour répondre à différents besoins de planifications. Les triggers possèdent un certain nombre de propriétés qui permettent de définir les critères de déclenchement.

Chaque trigger est identifié de manière unique par une instance de type TriggerKey.

Plusieurs propriétés sont communes à tous les types de Trigger :

Propriété

Rôle

jobKey

Identité du job qui sera exécuté lors du déclenchement

startTime

Date/heure à partir de laquelle le déclenchement d'événements peut débuter. Ceci permet de définir des triggers par avance

endTime

Date/heure de fin de déclenchement du trigger

priority

Priorité du trigger. Au cas où Quartz ne possède plus de threads pour exécuter des jobs déclenchés au même moment. Dans ce cas, Quartz prend d'abord les triggers ayant la priorité la plus élevée. La valeur peut être négative ou positive. L'heure de déclenchement est toujours prioritaire

misfireInstruction

Une misfire instruction permet de préciser au scheduler comment il doit se comporter si le déclenchement d'un trigger est raté.

Le déclenchement raté d'un trigger peut avoir plusieurs origines notamment :

  • le scheduler est arrêté
  • aucun thread du pool n'est disponible pour l'exécution du job

Chaque trigger possède aussi des misfire instructions qui lui sont propres. L'instruction MISFIRE_INSTRUCTION_SMART_POLICY définit celle par défaut pour un type de trigger.


A partir de la version 2.0 de Quartz, il faut créer des instances de type Trigger en utilisant la classe TriggerBuilder. La classe TriggerBuilder est un builder qui facilite la création d'une instance de type Trigger.

Elle possède plusieurs méthodes pour configurer le trigger et obtenir une instance à partir de cette configuration :

Méthode

Rôle

T build()

Obtenir l'instance de type Trigger

TriggerBuilder<T> endAt(Date endTime)

Définir la date/heure de fin de déclenchement du trigger

TriggerBuilder<T> forJob(JobDetail jobDetail)

Définir le job associé au trigger qui sera obtenu à partir du JobDetail fourni en paramètre

TriggerBuilder<T> forJob(JobKey jobKey)

Définir le job associé au trigger qui sera celui correspondant au JobKey fourni en paramètre

TriggerBuilder<T> forJob(String jobName)

Définir le job associé au trigger qui sera celui correspondant au JobKey dont le nom est fourni en paramètre avec le nom du groupe par défaut

TriggerBuilder<T> forJob(String jobName, String jobGroup)

Définir le job associé au trigger qui sera celui correspondant au JobKey dont le nom et le nom du groupe sont fournis en paramètres

TriggerBuilder<T> modifiedByCalendar(String calendarName)

Définir le nom du Calendar qui sera appliqué pour déterminer les déclenchements

static TriggerBuilder<Trigger> newTrigger()

Obtenir une nouvelle instance de type TriggerBuilder

TriggerBuilder<T> startAt(Date startTime)

Définir la date/heure de début de déclenchement du trigger avec celle fournie en paramètre

TriggerBuilder<T> startNow()

Définir la date/heure de début de déclenchement du trigger à maintenant

TriggerBuilder<T> usingJobData(JobDataMap newJobDataMap)

Définir l'instance de type JobDataMap associée au trigger

TriggerBuilder<T> usingJobData(String key, Boolean value)
TriggerBuilder<T> usingJobData(String key, Double value) TriggerBuilder<T> usingJobData(String key, Float value)
TriggerBuilder<T> usingJobData(String key, Integer value)
TriggerBuilder<T> usingJobData(String key, Long value)
TriggerBuilder<T> usingJobData(String key, String value)

Ajouter une paire clé/valeur fournie en paramètres dans l'instance de type JobDataMap

TriggerBuilder<T> withDescription(String description)

Définir la description du trigger

TriggerBuilder<T> withIdentity(String name)

Définir l'identité du trigger : l'instance de type TriggerKey sera composée du nom fourni en paramètre et du nom du groupe par défaut

TriggerBuilder<T> withIdentity(String name, String group)

Définir l'identité du trigger : l'instance de type TriggerKey sera composée du nom et du nom du groupe fournis en paramètres

TriggerBuilder<T> withIdentity(TriggerKey key)

Définir l'identité du trigger : l'instance de type TriggerKey est celle fournie en paramètre

TriggerBuilder<T> withPriority(int priority)

Définir la priorité du trigger

<SBT extends T> TriggerBuilder<SBT> withSchedule(ScheduleBuilder<SBT> scheduleBuilder)

Définir l'instance de type ScheduleBuilder qui permettra de définir la planification des déclenchements


Exemple :
package fr.jmdoudoux.dej.quartz;
			
import java.io.IOException;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class TestQuartz {
  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();
      final JobDetail jobDetail = JobBuilder.newJob(MonJob.class)
        .withIdentity("monJob").build();
      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity("monTrigger", "monGroup")
          .startNow()
          .withSchedule(SimpleScheduleBuilder.simpleSchedule()
            .withIntervalInHours(1).repeatForever())
          .build();
      scheduler.start();
      scheduler.scheduleJob(jobDetail, trigger);
      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

Une instance de type JobDataMaps peut être associée à un trigger pour passer des données sous la forme de paires clé/valeur disponibles à chaque déclenchement.

Lorsqu'un déclenchement est raté (le job associé ne peut pas être exécuté au moment prévu), le trigger passe à l'état misfired. Ceci peut par exemple survenir lorsque tous les threads du pool sont déjà occupés à exécuter d'autres jobs déclenchés.

Il est possible de préciser au scheduler le comportement qu'il doit avoir dans une telle circonstance à l'aide d'une misfire instruction. Chaque type de trigger possède un ensemble défini de misfire instructions.

Lorsque le scheduler démarre, il recherche si le déclenchement de triggers persistants a été raté et pour ceux concernés il applique le comportement défini par la misfire instruction configurée dans le trigger.

Quartz propose trois implémentations de l'interface Trigger :

  • SimpleTrigger
  • CronTrigger
  • CalendarIntervalTrigger

 

111.2.6.1. SimpleTrigger

Un SimpleTrigger permet de planifier :

  • une exécution unique immédiate
  • une exécution unique à un moment donné
  • des exécutions récurrentes en utilisant une période fixe avec un nombre d'exécutions fixe ou illimité

L'implémentation de SimpleTrigger possède plusieurs propriétés qui permettent de définir la ou les conditions de déclenchement :

Propriété

Rôle

repeatCount

Définir le nombre de déclenchements souhaité avant la suppression du trigger. Les valeurs possibles sont zéro, une valeur positive ou la constante SimpleTrigger.REPEAT_INDEFINITELY

endTime

Date/heure de fin des déclenchements et de la suppression du trigger. Si elle est définie, elle prend le pas sur la propriété repeatCount. Ceci permet d'éviter d'avoir à calculer le nombre de répétitions voulues durant la plage de dates. Dans ce cas, la propriété repeatCount doit être supérieure au nombre de déclenchement qui doivent survenir avant la date/heure de fin

repeatInterval

Définir l'intervalle en millisecondes entre chaque répétition. Les valeurs possibles sont zéro ou une valeur positive. La valeur zéro provoque tous les déclenchements de manière concurrente ou quasi concurrente selon les capacités du Scheduler

startTime

Date/heure de début du premier déclenchement


Avant la version 2.0 de Quartz, org.quartz.SimpleTrigger est une classe que l'on peut instancier en invoquant un de ses constructeurs.

Exemple :
SimpleTrigger trigger = new SimpleTrigger("monTrigger", 
  Scheduler.DEFAULT_GROUP, SimpleTrigger.REPEAT_INDEFINITELY, 30000);

Il est possible d'invoquer le constructeur par défaut et d'utiliser ses setters pour configurer l'instance.

Exemple :
SimpleTrigger trigger = new SimpleTrigger();
      trigger.setName("monTrigger");
      trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
      trigger.setRepeatInterval(30000);

A partir de la version 2.0 de Quartz, org.quartz.SimpleTrigger est une interface. La classe SimpleTriggerImpl implémente cette interface. Elle possède plusieurs constructeurs qui sont tous deprecated sauf le constructeur par défaut.

Plutôt que d'utiliser ces constructeurs, il faut utiliser la classe TriggerBuilder qui implémente le motif de conception monteur. Pour obtenir une instance de type SimpleTrigger, il faut lui associer un SimpleScheduleBuilder.

La classe SimpleScheduleBuilder implémente le motif de conception monteur pour faciliter la création d'une instance configurée de type SimpleTrigger.

Elle possède plusieurs méthodes pour configurer et obtenir une instance :

Méthode

Rôle

MutableTrigger build()

Obtenir l'instance de type Trigger selon la configuration courante : ne devrait pas être invoquée par le développeur mais uniquement par un TriggerBuilder

SimpleScheduleBuilder repeatForever()

Préciser que la répétition des déclenchements est infinie

static SimpleScheduleBuilder repeatHourlyForever()

Créer une instance configurée pour une exécution infinie à une heure d'intervalle

static SimpleScheduleBuilder repeatHourlyForever(int hours)

Créer une instance configurée pour une exécution infinie au nombre d'heures passé en paramètre d'intervalle

static SimpleScheduleBuilder repeatHourlyForTotalCount(int count)

Créer une instance configurée pour une exécution du nombre de fois passé en paramètre à une heure d'intervalle

static SimpleScheduleBuilder repeatHourlyForTotalCount(int count, int hours)

Créer une instance configurée pour une exécution du nombre de fois passé en paramètre au nombre d'heures passé en paramètre d'intervalle

static SimpleScheduleBuilder repeatMinutelyForever()

Créer une instance configurée pour une exécution infinie à une minute d'intervalle

static SimpleScheduleBuilder repeatMinutelyForever(int minutes)

Créer une instance configurée pour une exécution infinie au nombre de minutes passé en paramètre d'intervalle

static SimpleScheduleBuilder repeatMinutelyForTotalCount(int count)

Créer une instance configurée pour une exécution du nombre de fois passé en paramètre à une minute d'intervalle

static SimpleScheduleBuilder repeatMinutelyForTotalCount(int count, int minutes)

Créer une instance configurée pour une exécution du nombre de fois passé en paramètre au nombre de minutes passé en paramètre d'intervalle

static SimpleScheduleBuilder repeatSecondlyForever()

Créer une instance configurée pour une exécution infinie à une seconde d'intervalle

static SimpleScheduleBuilder repeatSecondlyForever(int seconds)

Créer une instance configurée pour une exécution infinie au nombre de secondes passé en paramètre d'intervalle

static SimpleScheduleBuilder repeatSecondlyForTotalCount(int count)

Créer une instance configurée pour une exécution du nombre de fois passé en paramètre à une seconde d'intervalle

static SimpleScheduleBuilder repeatSecondlyForTotalCount(int count, int seconds)

Créer une instance configurée pour une exécution du nombre de fois passé en paramètre au nombre de secondes passé en paramètre d'intervalle

static SimpleScheduleBuilder simpleSchedule()

Créer une instance

SimpleScheduleBuilder withIntervalInHours(int intervalInHours)

Préciser un intervalle de répétitions en heures

SimpleScheduleBuilder withIntervalInMilliseconds(long intervalInMillis)

Préciser un intervalle de répétitions en millisecondes

SimpleScheduleBuilder withIntervalInMinutes(int intervalInMinutes)

Préciser un intervalle de répétitions en minutes

SimpleScheduleBuilder withIntervalInSeconds(int intervalInSeconds)

Préciser un intervalle de répétitions en secondes

SimpleScheduleBuilder withMisfireHandlingInstructionFireNow()

Définir l'instruction de type Trigger.MISFIRE_INSTRUCTION_FIRE_NOW si un déclenchement est raté

SimpleScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires()

Définir l'instruction de type Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY

SimpleScheduleBuilder withMisfireHandlingInstructionNextWithExistingCount()

Définir l'instruction de type Trigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT si un déclenchement est raté

SimpleScheduleBuilder withMisfireHandlingInstructionNextWithRemainingCount()

Définir l'instruction de type Trigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT si un déclenchement est raté

SimpleScheduleBuilder withMisfireHandlingInstructionNowWithExistingCount()

Définir l'instruction de type Trigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT si un déclenchement est raté

SimpleScheduleBuilder withMisfireHandlingInstructionNowWithRemainingCount()

Définir l'instruction de type Trigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT si un déclenchement est raté

SimpleScheduleBuilder withRepeatCount(int repeatCount)

Préciser le nombre de fois que le trigger sera déclenché ; le nombre total d'exécutions est égal à la valeur en paramètre + 1


Pour obtenir une instance de type SimpleTrigger, il faut utiliser un TriggerBuilder pour les propriétés principales du trigger et un SimpleScheduleBuilder pour les propriétés spécifiques au SimpleTrigger.

L'exemple ci-dessous planifie une exécution à une heure d'intervalle débutant dans 30 minutes.

Exemple :
package fr.jmdoudoux.dej.quartz;
			
import java.io.IOException;
import org.quartz.DateBuilder;
import org.quartz.DateBuilder.IntervalUnit;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;

public class TestQuartz {
 
  public static void main(final String[] args) {
   
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();
      final JobDetail jobDetail = JobBuilder.newJob(MonJob.class)
        .withIdentity("monJob").build();
      final Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity(new TriggerKey("monTrigger", "monGroup"))
        .withSchedule(SimpleScheduleBuilder.simpleSchedule()
          .withIntervalInHours(1).repeatForever())
        .startAt(DateBuilder.futureDate(30, IntervalUnit.MINUTE))
        .build();
      scheduler.start();
      scheduler.scheduleJob(jobDetail, trigger);
      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

L'exemple ci-dessous planifie une exécution immédiate.

Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class TestQuartz {
 
  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
      try {
        scheduler = factory.getScheduler();
        final JobDetail jobDetail =
JobBuilder.newJob(MonJob.class).withIdentity("monJob").build();
        final SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder
          .newTrigger()
          .withIdentity("monTrigger", "monGroup")
          .build();
      scheduler.start();
      scheduler.scheduleJob(jobDetail, trigger);
      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

Dans l'exemple ci-dessous, la propriété repeatCount est initialisée à 10 ce qui générera 11 déclenchements.

Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class TestQuartz {
  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();
      final JobDetail jobDetail = JobBuilder.newJob(MonJob.class)
        .withIdentity("monJob").build();
      final SimpleTrigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity("monTrigger", "monGroup")
          .withSchedule(SimpleScheduleBuilder.simpleSchedule()
            .withIntervalInSeconds(10).withRepeatCount(10))
          .build();
      scheduler.start();
      scheduler.scheduleJob(jobDetail, trigger);
      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

La classe SimpleTrigger définit plusieurs instructions pour préciser le comportement du scheduler au cas où le déclenchement du trigger est raté.

  • MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY : indique au scheduler d'ignorer les déclenchements ratés
  • MISFIRE_INSTRUCTION_FIRE_NOW : indique au scheduler de déclencher le trigger immédiatement (à utiliser pour des triggers à déclenchement unique de préférence)
  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT : indique au scheduler de déclencher le trigger immédiatement avec le nombre de répétitions inchangé
  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT : indique au scheduler de déclencher le trigger immédiatement (à utiliser pour des triggers à déclenchements répétés). Le trigger peut passer directement au statut COMPLETE si le nombre de déclenchement est atteint même si certains d'entre-eux sont ratés
  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT : indique au scheduler de replanifier la prochaine l'exécution avec le nombre de répétitions égal à ce qu'il serait si le déclenchement n'avait pas été raté
  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT : indique au scheduler de replanifier la prochaine l'exécution avec le nombre de répétitions inchangé

 

111.2.6.2. CronTrigger

La classe CronTrigger permet de planifier des déclenchements en utilisant la syntaxe des expressions de l'outil cron des systèmes Unix. Le CronTrigger est particulièrement utile pour définir des répétitions basées sur des éléments du calendrier.

La configuration se fait en utilisant une expression de type cron. L'expression est une chaîne de caractères contenant des champs séparés par un caractère espace. Celle-ci est analysée et évaluée par une instance de type org.quartz.CronExpression.

La syntaxe d'une expression cron utilise 6 champs obligatoires et un champ facultatif. Chaque champ correspond à un élément permettant de définir la planification. Les champs doivent respecter un certain ordre :

Secondes Minutes Heures Jour_du_mois mois Jour_de_la_semaine Année (optionnel)

Chaque champ possède des valeurs acceptables et utilise éventuellement un caractère ou une combinaison de caractères spéciaux.

Champs

Obligatoire

Valeurs

Caractères spéciaux

Seconde

Oui

0-59

, - * /

Minute

Oui

0-59

, - * /

Heure

Oui

0-23

, - * /

Jour du mois

Oui

1-31

, - * ? L W

Mois

Oui

1-12 ou JAN-DEC ou jan-dec

, - * /

Jour de la semaine

Oui

1-7 ou SUN-SAT ou sun-sat

, - * ? L #

Année

Non

1970-2099

, - * /


Le jour du mois peut avoir une valeur entre 1 et 31 mais cette valeur doit exister pour le mois considéré : c'est particulièrement vrai pour les mois de février (28 ou 29 jours selon que l'année soit bissextile ou non) et avril, juin, septembre et novembre qui ne possède pas de 31.

Le mois peut être précisé en utilisant son numéro (1-12) ou les trois premières lettres de son nom en anglais (JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV et DEC)

Le jour de la semaine peut avoir une valeur entre 1 et 7 où 1 est dimanche ou être désigné par les trois premières lettres de son nom en anglais (SUN, MON, TUE, WED, THU, FRI et SAT).

Les valeurs alphabétiques ne sont pas sensibles à la casse.

Les caractères spéciaux permettent de définir des situations particulières :

Caractère

Description

*

Désigner toutes les valeurs possibles pour le champ. Par exemple : dans le champ heure, cela signifie toutes les heures

?

Ne désigner aucune valeur particulière. Permet de préciser dans les champs jour du mois ou jour de la semaine que le champ doit être ignoré au profit de l'autre

-

Désigner une plage de valeurs dont les bornes sont incluses. Par exemple, 1-5 correspond aux valeurs 1, 2, 3, 4 et 5

,

Désigner plusieurs valeurs. Par exemple, MON, WED, FRI dans le champ jour de la semaine correspond à lundi, mercredi et vendredi

/

Désigner une valeur d'incrémentation à partir d'une valeur initiale limitée selon la plage de valeurs acceptables par le champ concerné. Par exemple, 0/15 dans le champ seconde correspond aux valeurs 0, 15, 30 et 45 et 10/10 correspond aux valeurs 10, 20, 30, 40 et 50.

Attention : /35 ne signifie pas tous les 35 minutes mais toutes les 35 minutes à partir de la minute 0, ce qui est identique à 0,35 et déclenchera donc deux événements dans une même heure

L

Désigner la dernière valeur acceptable par le champ concerné. Il ne peut être utilisé que sur les champs jour de la semaine et jour du mois.

Dans le champ jour du mois, il désigne le dernier jour du mois en tenant compte des années bissextiles pour le mois de février.

Par exemple, L dans le champ jour du mois correspond au 31 janvier, 28 février pour les années non bissextiles, le 29 février pour les années bissextiles, le 31 mars, le 30 avril, ...

Dans le champ jour de la semaine, cela correspond à la valeur 7 soit samedi. Il est possible de préciser un jour avant le L pour désigner le dernier jour correspondant du mois. Par exemple, 6L correspond au dernier vendredi du mois. Il est aussi possible de préciser un offset après le caractère L. Par exemple, L-3 correspond au troisième avant le dernier jour du mois.

Il ne faut surtout pas utiliser plusieurs valeurs avec le caractère L

W

Désigner un jour de la semaine hors week end le plus proche de celui indiqué avant le caractère W. Par exemple, 15W. Si le 15 du mois est un samedi alors l'exécution aura lieu le vendredi 14. Si le 15 du mois est un dimanche alors l'exécution aura lieu le lundi 16. Si le 15 n'est ni un samedi ni un dimanche alors l'exécution a lieu le jour précisé.

Il ne faut surtout pas utiliser plusieurs valeurs avec le caractère W.

LW

Désigner le dernier jour du mois hors week end dans le champ jour du mois

#

Désigner le n-ieme jour de la semaine du mois. Par exemple, LUN#1 correspond au premier lundi du mois, 6#3 correspond au troisième vendredi du mois


Les listes de valeurs et les plages peuvent être combinées dans un même champ.

Exemple : MON-WED,SAT qui correspond à lundi, mardi, mercredi et samedi.

Les champs jour du mois et jour de la semaine sont généralement mutuellement exclusifs : si l'un à une ou plusieurs valeurs définies, l'autre peut avoir le caractère ? pour préciser de ne pas tenir compte du champ.

Exemple

Expression cron

Description du déclenchement

0 0 12 * * ?

Tous les jours à midi

0/30 * * * * ?

Toutes les 30 secondes

0 0/5 * * * ?

Toutes les 5 minutes

10 0/5 * * * ?

10 secondes après toutes les 5 minutes

0 30 14 ? * *
0 30 14 * * ?
0 30 14 * * ? *

Tous les jours à 14h30

0 30 14 * * ? 2014

Tous les jours de 2014 à 14h30

0 30 14 * * ? 2014-2016

Tous les jours de 2014, 2015 et 2016 à 14h30

0 * 15 * * ?

Toutes les minutes entre 15h et 15h59

0 0/5 15 * * ?

Toutes les 5 minutes entre 15h et 15h59

0 0/5 10,15 * * ?

Toutes les 5 minutes entre 10h et 10h59 et entre 15h et 15h59

0 0/30 8-9 * * ?

Toutes les demi-heures entre 8h00 et 10h00 (exclu) : 8:00, 8:30, 9:00 et 9:30

0 0-5 15 * * ?

Toutes les minutes entre 10h et 10h05

0 15,45 15 * * ?

Tous les jours à 15h15 et 15h45

0 0 12 ? * WED

Tous les mercredis à midi

0 0 12 ? 1 WED

Tous les mercredis du mois de janvier à midi

0 0 12 ? * MON-FRI

Du lundi au vendredi à midi

0 30 11-13 ? * WED,FRI

Tous les mercredis, jeudis et vendredi à 11H30, 12H30 et 13H30

0 0 12 15 * ?

Le 15 de chaque mois à 12h00

0 0 12 L * ?

Le dernier jour de chaque mois à midi

0 0 12 L-1 * ?

L'avant dernier jour de chaque mois à midi

0 0 12 ? * 6L

Le dernier vendredi de chaque mois à midi

0 0 12 ? * 6#3

Le troisième vendredi de chaque mois à midi

0 0 12 1/5 * ?

Tous les 5 jours de chaque mois à partir du premier jour du mois à midi

59 59 23 25 12 ?

La visite du père Noël chaque année


Parfois, il n'est pas possible de créer certaines conditions dans un seul Trigger notamment si celle-ci est une composition de plusieurs conditions. Dans ce cas, il faut définir plusieurs triggers qui devront tous exécuter le même job.

Attention lors de la définition de triggers qui pourraient être déclenchés durant le passage à l'heure d'été ou d'hiver (daylight savings) car cela pourrait faire rater un déclenchement ou le faire exécuter en double selon le cas.

Avant la version 2.0, CronTrigger était une classe qui possédait de nombreux constructeurs.

Exemple :
CronTrigger trigger = new CronTrigger("monTrigger",
Scheduler.DEFAULT_GROUP, "0/30 * * * * ?");

Il est aussi possible de créer une instance et d'utiliser les différents setters pour configurer l'instance.

Exemple :
      CronTrigger trigger = new CronTrigger();
      trigger.setName("monTrigger");
      trigger.setCronExpression("0/30 * * * * ?");

A partir de la version 2.0, CronTrigger est une interface dont la classe CronTriggerImpl est une implémentation.

Pour créer une instance de type CronTrigger, il faut utiliser un TriggerBuilder pour les propriétés générales du trigger et utiliser un CronSchedulerBuilder pour les propriétés spécifiques au CronTrigger.

La classe CronSchedulerBuilder met en oeuvre le motif de conception monteur et propose donc plusieurs méthodes pour configurer et obtenir l'instance.

Méthode 

Rôle

static CronScheduleBuilder atHourAndMinuteOnGivenDaysOfWeek(int hour, int minute, Integer... daysOfWeek)

Obtenir une instance de type CronScheduleBuilder correspondant à une expression pour l'heure, la minute et les jours de la semaine fournis en paramètre

MutableTrigger build()

Obtenir l'instance du Trigger. Cette méthode ne devrait être appelée que par un TriggerBuilder

static CronScheduleBuilder cronSchedule(CronExpression cronExpression)

Obtenir une instance de type CronScheduleBuilder correspondant à la CronExpression fournie en paramètre

static CronScheduleBuilder cronSchedule(String cronExpression)

Obtenir une instance de type CronScheduleBuilder correspondant à l'expression fournie en paramètre qui doit être valide

static CronScheduleBuilder cronScheduleNonvalidatedExpression(String cronExpression)

Obtenir une instance de type CronScheduleBuilder correspondant à l'expression fournie en paramètre. Lève une exception de type ParseException si l'expression n'est pas valide

static CronScheduleBuilder dailyAtHourAndMinute(int hour, int minute)

Obtenir une instance de type CronScheduleBuilder correspondant à une expression pour l'heure et la minute fournies en paramètre pour tous les jours de la semaine

CronScheduleBuilder inTimeZone(TimeZone timezone)

Préciser le fuseau horaire à utiliser

static CronScheduleBuilder monthlyOnDayAndHourAndMinute(int dayOfMonth, int hour, int minute)

Obtenir une instance de type CronScheduleBuilder correspondant à une expression pour l'heure, la minute et le jour du mois fournis en paramètre

static CronScheduleBuilder weeklyOnDayAndHourAndMinute(int dayOfWeek, int hour, int minute)

Obtenir une instance de type CronScheduleBuilder correspondant à une expression pour l'heure, la minute et le jour de la semaine fournis en paramètre

CronScheduleBuilder withMisfireHandlingInstructionDoNothing()

Préciser d'utiliser l'instruction de type: CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING en cas de déclenchement raté

CronScheduleBuilder withMisfireHandlingInstructionFireAndProceed()

Préciser d'utiliser l'instruction de type : CronTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW en cas de déclenchement raté

CronScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires()

Préciser d'utiliser l'instruction de type: Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY en cas de déclenchement raté


Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;
import java.util.TimeZone;

import org.quartz.CronScheduleBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class TestQuartz {

  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();

      final JobDetail jobDetail = JobBuilder
          .newJob(MonJob.class)
          .withIdentity("monJob", "groupe_1")
          .usingJobData("monParametre", "12345")
          .build();

      final Trigger cronTrigger = TriggerBuilder
          .newTrigger()
          .withIdentity("monTrigger", "groupe_1")
          .withSchedule(
              CronScheduleBuilder.cronSchedule("0/5 * * * * ?")
                .inTimeZone(TimeZone.getTimeZone("Europe/Paris")))
          .build();

      scheduler.start();
      scheduler.scheduleJob(jobDetail, cronTrigger);

      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

La classe CronTrigger définit plusieurs constantes pour désigner une misfire instruction :

  • MISFIRE_INSTRUCTION_DO_NOTHING : déterminer le prochain déclenchement sans effectuer un déclenchement immédiat
  • MISFIRE_INSTRUCTION_FIRE_NOW : demander au scheduler d'effectuer un déclenchement immédiat

 

111.2.6.3. CalendarIntervalTrigger

Depuis la version 2.0 de Quartz, un CalendarIntervalTrigger permet de planifier des déclenchements répétés selon un intervalle de temps dans le calendrier. Ce type de trigger ne peut pas être défini avec SimpleTrigger parce que par exemple tous les mois ne possèdent pas le même nombre de secondes ni avec un CronTrigger par ce que par exemple 5 mois n'est pas divisible de manière entière par 12.

L'interface CalendarIntervalTrigger définit plusieurs méthodes :

Méthode 

Rôle

int getRepeatInterval()

Obtenir la valeur de l'interface de répétitions

DateBuilder.IntervalUnit getRepeatIntervalUnit()

Obtenir l'unité de l'intervalle

int getTimesTriggered()

Obtenir le nombre de fois que le trigger a déjà été déclenché

TriggerBuilder<CalendarIntervalTrigger> getTriggerBuilder()

Obtenir un TriggerBuilder configuré pour produire un nouveau trigger identique


L'unité de l'intervalle est définie par l'énumération DateBuilder.IntervalUnit qui définit les valeurs suivantes : DAY, HOUR, MILLISECOND, MINUTE, MONTH, SECOND, WEEK et YEAR

La classe org.quartz.CalendarIntervalScheduleBuilder met en oeuvre le motif de conception monteur et propose donc plusieurs méthodes pour configurer et obtenir l'instance.

Méthode

Rôle

MutableTrigger build()

Obtenir l'instance du Trigger. Cette méthode ne devrait être appelée que par un TriggerBuilder

static CalendarIntervalScheduleBuilder calendarIntervalSchedule()

Obtenir une instance de type CalendarIntervalScheduleBuilder

CalendarIntervalScheduleBuilder withInterval(int interval, DateBuilder.IntervalUnit unit)

Définir l'unité de temps et l'intervalle entre chaque déclenchement

CalendarIntervalScheduleBuilder withIntervalInDays(int intervalInDays)

Définir l'unité de temps et l'intervalle entre chaque déclenchement avec l'unité de temps en jour (IntervalUnit.DAY)

CalendarIntervalScheduleBuilder withIntervalInHours(int intervalInHours)

Définir l'unité de temps et l'intervalle entre chaque déclenchement avec l'unité de temps en heure (IntervalUnit.HOUR)

CalendarIntervalScheduleBuilder withIntervalInMinutes(int intervalInMinutes)

Définir l'unité de temps et l'intervalle entre chaque déclenchement avec l'unité de temps en minute (IntervalUnit.MINUTE)

CalendarIntervalScheduleBuilder withIntervalInMonths(int intervalInMonths)

Définir l'unité de temps et l'intervalle entre chaque déclenchement avec l'unité de temps en mois (IntervalUnit.MONTH)

CalendarIntervalScheduleBuilder withIntervalInSeconds(int intervalInSeconds)

Définir l'unité de temps et l'intervalle entre chaque déclenchement avec l'unité de temps en seconde (IntervalUnit.SECOND)

CalendarIntervalScheduleBuilder withIntervalInWeeks(int intervalInWeeks)

Définir l'unité de temps et l'intervalle entre chaque déclenchement avec l'unité de temps en semaine (IntervalUnit.WEEK)

CalendarIntervalScheduleBuilder withIntervalInYears(int intervalInYears)

Définir l'unité de temps et l'intervalle entre chaque déclenchement avec l'unité de temps en année (IntervalUnit.YEAR)

CalendarIntervalScheduleBuilder withMisfireHandlingInstructionDoNothing()

Utiliser l'instruction CalendarIntervalTrigger.MISFIRE_INSTRUCTION_DO_NOTHING en cas d'échec du déclenchement du trigger

CalendarIntervalScheduleBuilder withMisfireHandlingInstructionFireAndProceed()

Utiliser l'instruction CalendarIntervalTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW en cas d'échec du déclenchement du trigger

CalendarIntervalScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires()

Utiliser l'instruction CalendarIntervalTrigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY en cas d'échec du déclenchement du trigger


Exemple :
package fr.jmdoudoux.dej.quartz;
			
import java.io.IOException;
import org.quartz.CalendarIntervalScheduleBuilder;
import org.quartz.DateBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class TestQuartz {
  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();
      final JobDetail jobDetail = JobBuilder.newJob(MonJob.class)
        .withIdentity("monJob").build();
      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity("monTrigger", "monGroupe")
          .startAt(DateBuilder.tomorrowAt(12, 0, 0))
          .withSchedule(CalendarIntervalScheduleBuilder
            .calendarIntervalSchedule().withIntervalInDays(2))
          .build();
      scheduler.start();
      scheduler.scheduleJob(jobDetail, trigger);
      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

Si l'intervalle de temps utilisé est le mois, il faut faire attention quand la propriété startTime est proche de la fin du mois. Par exemple, si le trigger est configuré avec un intervalle de 1, une unité de temps égale à MONTH et la propriété startTime initialisée avec 31 janvier alors les déclenchements auront lieu le 28 février, le 28 mars et ainsi de suite même si le mois comporte 30 ou 31 jours. Dans ce cas, il est préférable d'utiliser un CronTrigger configuré pour se déclencher le dernier jour du mois.

 

111.2.6.4. DailyTimeIntervalTrigger

Depuis la version 2.1 de Quartz, un DailyTimeIntervalTrigger permet de planifier des déclenchements répétés selon un intervalle de temps journalier.

Il permet de planifier un intervalle de répétition exprimé en secondes, minutes ou heures qui peut survenir durant une plage horaire tous les jours ou pour certains jours de la semaine.

L'interface DailyTimeIntervalTrigger définit plusieurs méthodes :

Méthode

Rôle

Set<Integer> getDaysOfWeek()

Définir les jours de la semaine où le déclenchement peut s'appliquer

TimeOfDay getEndTimeOfDay()

Définir l'heure de fin de la plage horaire

int getRepeatInterval()

Définir la valeur de l'intervalle selon l'unité précisée par la propriété repeatIntervalUnit

DateBuilder.IntervalUnit getRepeatIntervalUnit()

Définir l'unité de temps de l'intervalle de répétitions

TimeOfDay getStartTimeOfDay()

Définir l'heure de début de la plage horaire

int getTimesTriggered()

Obtenir le nombre de fois que le trigger a été déclenché

TriggerBuilder<DailyTimeIntervalTrigger> getTriggerBuilder()

Obtenir une instance de type TriggerBuilder qui soit configuré pour créer une instance identique à celle courante


Par défaut, un DailyTimeIntervalScheduleBuilder utilise des valeurs par défaut pour ses propriétés :

  • startTimeOfDay : 00:00:00
  • endTimeOfDay : 23:59:59
  • daysOfWeek : tous les jours (ALL_DAYS_OF_THE_WEEK)
  • startTime : maintenant

Tous les jours, la propriété startTime est initialisée avec la valeur de startTimeOfDay. A chaque déclenchement la valeur de repeatinterval est ajoutée jusqu'à atteindre la valeur de endTimeOfDay.

La classe org.quartz.DailyTimeIntervalScheduleBuilder met en oeuvre le motif de conception monteur et propose donc plusieurs méthodes pour configurer et obtenir l'instance.

Méthode

Rôle

MutableTrigger build()

Obtenir l'instance du Trigger. Cette méthode ne devrait être appelée que par un TriggerBuilder

static DailyTimeIntervalScheduleBuilder dailyTimeIntervalSchedule()

Obtenir une instance de DailyTimeIntervalScheduleBuilder

DailyTimeIntervalScheduleBuilder endingDailyAfterCount(int count)

Déterminer l'heure de fin de la plage horaire selon le nombre de répétitions fournis en paramètre, la durée de l'intervalle et l'heure de début de la plage horaire. Ces deux dernières informations doivent être définies avant d'invoquer cette méthode

DailyTimeIntervalScheduleBuilder endingDailyAt(TimeOfDay timeOfDay)

Préciser l'heure de fin de la plage horaire de planification

DailyTimeIntervalScheduleBuilder onDaysOfTheWeek(Integer... onDaysOfWeek)

Définir les jours de la semaine où les déclenchements peuvent survenir

DailyTimeIntervalScheduleBuilder onDaysOfTheWeek(Set<Integer> onDaysOfWeek)

Définir les jours de la semaine où les déclenchements peuvent survenir

DailyTimeIntervalScheduleBuilder onEveryDay()

Définir les déclenchements tous les jours de la semaine

DailyTimeIntervalScheduleBuilder onMondayThroughFriday()

Définir les déclenchements uniquement du lundi au vendredi inclus

DailyTimeIntervalScheduleBuilder onSaturdayAndSunday()

Définir les déclenchements uniquement les samedis et les dimanches

DailyTimeIntervalScheduleBuilder startingDailyAt(TimeOfDay timeOfDay)

Préciser l'heure de début de la plage horaire de planification

DailyTimeIntervalScheduleBuilder withInterval(int timeInterval, DateBuilder.IntervalUnit unit)

Préciser l'intervalle de temps entre chaque répétition (durée et unité de temps)

DailyTimeIntervalScheduleBuilder withIntervalInHours(int intervalInHours)

Préciser un intervalle de temps en heures entre chaque répétition

DailyTimeIntervalScheduleBuilder withIntervalInMinutes(int intervalInMinutes)

Préciser un intervalle de temps en minutes entre chaque répétition

DailyTimeIntervalScheduleBuilder withIntervalInSeconds(int intervalInSeconds)

Préciser un intervalle de temps en secondes entre chaque répétition

DailyTimeIntervalScheduleBuilder withMisfireHandlingInstructionDoNothing()

Utiliser l'instruction DailyTimeIntervalTrigger.MISFIRE_INSTRUCTION_FIRE_DO_NOTHING en cas d'échec du déclenchement du trigger

DailyTimeIntervalScheduleBuilder withMisfireHandlingInstructionFireAndProceed()

Utiliser l'instruction DailyTimeIntervalTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW en cas d'échec du déclenchement du trigger

DailyTimeIntervalScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires()

Utiliser l'instruction DailyTimeIntervalTrigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY en cas d'échec du déclenchement du trigger

DailyTimeIntervalScheduleBuilder withRepeatCount(int repeatCount)

Définir le nombre de répétitions de l'intervalle


L'exemple ci-dessous planifie l'exécution du job du lundi au vendredi à 9h00, 12h00, 15h00 et 18h00.

Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;

import org.quartz.DailyTimeIntervalScheduleBuilder;
import org.quartz.DailyTimeIntervalTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.TimeOfDay;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class TestQuartz {

  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();

      final JobDetail jobDetail = JobBuilder
          .newJob(MonJob.class)
          .withIdentity("monJob")
          .storeDurably()
          .build();

      final DailyTimeIntervalTrigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity("monTrigger")
          .withSchedule(
              DailyTimeIntervalScheduleBuilder
                  .dailyTimeIntervalSchedule()
                  .withIntervalInHours(3)
                  .onDaysOfTheWeek(
                      DailyTimeIntervalScheduleBuilder.MONDAY_THROUGH_FRIDAY)
                  .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 00))
                  .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(19, 00)))
          .build();

      scheduler.scheduleJob(jobDetail, trigger);
      scheduler.start();

      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

La classe DailyTimeIntervalTrigger définit plusieurs constantes pour désigner une misfire instruction :

  • MISFIRE_INSTRUCTION_DO_NOTHING : déterminer le prochain déclenchement sans effectuer un déclenchement immédiat
  • MISFIRE_INSTRUCTION_FIRE_NOW : demander au scheduler d'effectuer un déclenchement immédiat

 

111.2.6.5. L'exclusion de périodes dans la planification

L'interface org.quartz.Calendar permet de définir des périodes de temps durant lesquelles un trigger ne pourra pas être déclenché. L'utilisation d'un Calendar ne permet que d'exclure des périodes dans le temps.

Par exemple, il est possible de définir un trigger qui s'exécute tous les jours à midi et utiliser un Calendar pour exclure les jours fériés.

Un Calendar Quartz peut être associé à un trigger et est stocké dans le scheduler. Il permet de définir des périodes de temps durant lesquelles un trigger ne pourra pas être déclenché.

L'interface Calendar définit plusieurs méthodes :

Méthode

Rôle

Calendar getBaseCalendar()

Obtenir le calendrier de base

String getDescription()

Obtenir la description

long getNextIncludedTime(long timeStamp)

Obtenir le prochain moment (en millisecondes) qui est inclus dans le calendrier par rapport au moment fourni en paramètre

boolean isTimeIncluded(long timeStamp)

Déterminer si le moment fourni en paramètre (en millisecondes) est inclus dans le calendrier ou pas

void setBaseCalendar(Calendar baseCalendar)

Définir le calendrier de base ou retirer le calendrier de base actuel en passant null en paramètre

void setDescription(String description)

Définir la description du calendrier. C'est une donnée informative qui n'est pas utilisée par Quartz


Ceci permet d'exclure par exemple des jours particuliers : jours fériés, fériés bancaires, jours de fermeture dominicale, ...

La granularité utilisée par un Calendar est la milliseconde, ce qui est beaucoup plus fin que la plupart des besoins qui sont généralement de l'ordre de l'heure ou de la journée. Pour faciliter cette gestion, Quartz propose, dans le package org.quartz.impl.calendar, plusieurs implémentations de l'interface Calendar qui héritent de la classe BaseCalendar.

Classe

Rôle

AnnualCalendar

Exclure un ou plusieurs jours dans l'année

CronCalendar

Exclure les périodes définies par une expression de type cron

DailyCalendar

Exclure une période continue dans une même journée

HolidayCalendar

Exclure les jours de congés

MonthlyCalendar

Exclure un ou plusieurs jours dans le mois

WeeklyCalendar

Exclure un ou plusieurs jours de la semaine. Par exemple : les samedis et les dimanches


Pour utiliser un Calendar, il faut :

  • en créer une instance
  • la configurer
  • l'enregistrer dans le scheduler pour le stocker dans le jobstore courant
  • l'associer avec chacun des triggers concernés

L'enregistrement d'un Calendar dans un Scheduler se fait en utilisant sa méthode addCalendar() qui attend en paramètres : le nom du Calendar, son instance, un booléen qui précise si l'instance doit remplacer celle précédemment enregistrée et un autre booléen qui précise si les triggers utilisant déjà le Calendar doivent être mis à jour.

Le nom du Calendar sera utilisé pour associer le Calendar à un trigger. Il est possible d'associer le même Calendar à plusieurs triggers.

Attention : il est nécessaire de bien vérifier le nom du Calendar fournit en paramètre

Exemple :
scheduler.addCalendar("jours_feries", cal, true, true);

Avant la version 2.0 de Quartz, il faut invoquer la méthode setCalendar() de la classe Trigger pour lui associer un Calendar.

Exemple :
trigger.setCalendarName("jours_feries");

A partir de la version 2.0, il faut utiliser la méthode modifiedByCalendar() de la classe TriggerBuilder.

Exemple :
      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity("monTrigger", "monGroupe")
          .modifiedByCalendar("jours_feries")
          .withSchedule(simpleSchedule()
            .withIntervalInSeconds(30)
            .repeatForever()) 
          .build();

La classe org.quartz.impl.calendar.WeeklyCalendar permet de définir des jours de la semaine durant lesquels le déclenchement d'une planification peut avoir lieu. Par défaut, elle exclue le samedi et le dimanche.

La méthode setDayExcluded() permet de préciser pour un jour s'il doit être exclu ou non. Elle attend en paramètre un entier qui précise le jour (le plus simple est d'utiliser une des constantes de la classe java.util.Calendar) et un booléen (true pour exclusion et false pour inclusion).

Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;
import java.util.Date;
import java.util.List;

import org.quartz.CalendarIntervalScheduleBuilder;
import org.quartz.DateBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.calendar.WeeklyCalendar;
import org.quartz.spi.OperableTrigger;

public class TestQuartz {

  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();

      final WeeklyCalendar cal = new WeeklyCalendar();
      cal.setDayExcluded(java.util.Calendar.MONDAY, true);
      cal.setDayExcluded(java.util.Calendar.SUNDAY, true);
      cal.setDayExcluded(java.util.Calendar.SATURDAY, false);

      scheduler.addCalendar("jours_d_ouverture", cal, true, true);

      final JobDetail jobDetail = JobBuilder.newJob(MonJob.class)
        .withIdentity("monJob").storeDurably().build();

      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity("monTrigger", "monGroupe")
          .modifiedByCalendar("jours_d_ouverture")
          .startAt(DateBuilder.todayAt(14, 0, 0))
          .withSchedule(CalendarIntervalScheduleBuilder
            .calendarIntervalSchedule().withIntervalInDays(2))
          .build();

      scheduler.scheduleJob(jobDetail, trigger);
      scheduler.start();

      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

La classe org.quartz.impl.calendar.AnnualCalendar permet de définir des jours dans une année pour lesquels la planification d'un trigger ne se fera pas. Ce type de Calendar ne tient pas compte de l'année : les jours exclus le sont pour toutes les années. Il est donc pratique pour définir les jours fériés fixes d'une année à l'autre.

La méthode setDayExcluded() permet de définir un ou plusieurs jours à exclure. Elle possède deux surcharges :

  • la première attend en paramètre une instance de type java.util.Calendar qui encapsule le jour et un booléen qui précise si le jour est exclu ( true) ou non (false)
  • la seconde attend en paramètre une collection de type ArrayList contenant une ou plusieurs instances de type java.util.Calendar

La méthode removeExcludedDay() permet d'enlever le jour fourni en paramètre de la liste des jours exclus.

Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import org.quartz.CalendarIntervalScheduleBuilder;
import org.quartz.DateBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.calendar.AnnualCalendar;
import org.quartz.spi.OperableTrigger;

public class TestQuartz {

  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();

      final ArrayList<Calendar> joursFeriesFixes =
        new ArrayList<Calendar>(7);
      final AnnualCalendar cal = new AnnualCalendar();

      Calendar jourFerie = Calendar.getInstance();
      jourFerie.set(Calendar.MONTH, Calendar.JANUARY);
      jourFerie.set(Calendar.DATE, 1);
      joursFeriesFixes.add(jourFerie);

      jourFerie = Calendar.getInstance();
      jourFerie.set(Calendar.MONTH, Calendar.MAY);
      jourFerie.set(Calendar.DATE, 1);
      joursFeriesFixes.add(jourFerie);

      jourFerie = Calendar.getInstance();
      jourFerie.set(Calendar.MONTH, Calendar.MAY);
      jourFerie.set(Calendar.DATE, 8);
      joursFeriesFixes.add(jourFerie);

      jourFerie = Calendar.getInstance();
      jourFerie.set(Calendar.MONTH, Calendar.JULY);
      jourFerie.set(Calendar.DATE, 14);
      joursFeriesFixes.add(jourFerie);

      jourFerie = Calendar.getInstance();
      jourFerie.set(Calendar.MONTH, Calendar.AUGUST);
      jourFerie.set(Calendar.DATE, 15);
      joursFeriesFixes.add(jourFerie);

      jourFerie = Calendar.getInstance();
      jourFerie.set(Calendar.MONTH, Calendar.NOVEMBER);
      jourFerie.set(Calendar.DATE, 1);
      joursFeriesFixes.add(jourFerie);

      jourFerie = Calendar.getInstance();
      jourFerie.set(Calendar.MONTH, Calendar.NOVEMBER);
      jourFerie.set(Calendar.DATE, 11);
      joursFeriesFixes.add(jourFerie);

      jourFerie = Calendar.getInstance();
      jourFerie.set(Calendar.MONTH, Calendar.DECEMBER);
      jourFerie.set(Calendar.DATE, 25);
      joursFeriesFixes.add(jourFerie);

      cal.setDaysExcluded(joursFeriesFixes);

      scheduler.addCalendar("Jours feries fixes", cal, true, true);

      final JobDetail jobDetail = JobBuilder.newJob(MonJob.class)
        .withIdentity("monJob").storeDurably().build();

      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity("monTrigger", "monGroupe")
          .modifiedByCalendar("Jours feries fixes")
          .startAt(DateBuilder.todayAt(14, 0, 0))
          .withSchedule(CalendarIntervalScheduleBuilder
            .calendarIntervalSchedule().withIntervalInDays(2))
          .build();

      scheduler.scheduleJob(jobDetail, trigger);
      scheduler.start();

      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

La classe org.quartz.impl.calendar.HolidayCalendar permet de définir des jours à exclure en tenant compte de l'année.

La méthode addExcludedDate() permet d'exclure un jour : elle attend en paramètre une instance de type Date.

La méthode removeExcludedDate() permet de retirer un jour de la liste de ceux à exclure.

Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import org.quartz.CalendarIntervalScheduleBuilder;
import org.quartz.DateBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.calendar.HolidayCalendar;
import org.quartz.spi.OperableTrigger;

public class TestQuartz {

  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();

      final HolidayCalendar cal = new HolidayCalendar();

      final Calendar gCal = Calendar.getInstance();

      gCal.set(2014, Calendar.JANUARY, 1);
      cal.addExcludedDate(gCal.getTime());
      gCal.set(2014, Calendar.APRIL, 21);
      cal.addExcludedDate(gCal.getTime());
      gCal.set(2014, Calendar.MAY, 1);
      cal.addExcludedDate(gCal.getTime());
      gCal.set(2014, Calendar.MAY, 8);
      cal.addExcludedDate(gCal.getTime());
      gCal.set(2014, Calendar.MAY, 29);
      cal.addExcludedDate(gCal.getTime());
      gCal.set(2014, Calendar.JUNE, 9);
      cal.addExcludedDate(gCal.getTime());
      gCal.set(2014, Calendar.JULY, 14);
      cal.addExcludedDate(gCal.getTime());
      gCal.set(2014, Calendar.AUGUST, 15);
      cal.addExcludedDate(gCal.getTime());
      gCal.set(2014, Calendar.NOVEMBER, 1);
      cal.addExcludedDate(gCal.getTime());
      gCal.set(2014, Calendar.NOVEMBER, 11);
      cal.addExcludedDate(gCal.getTime());
      gCal.set(2014, Calendar.DECEMBER, 25);
      cal.addExcludedDate(gCal.getTime());

      scheduler.addCalendar("Jour feries 2014", cal, true, true);

      final JobDetail jobDetail = JobBuilder.newJob(MonJob.class)
        .withIdentity("monJob").storeDurably().build();

      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity("monTrigger", "monGroupe")
          .modifiedByCalendar("Jour feries 2014")
          .startAt(DateBuilder.todayAt(14, 0, 0))
          .withSchedule(CalendarIntervalScheduleBuilder
            .calendarIntervalSchedule().withIntervalInDays(2))
          .build();

      scheduler.scheduleJob(jobDetail, trigger);
      scheduler.start();

      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

Il est possible de définir sa propre implémentation de type org.quartz.Calendar qui doit être sérialisable.

 

111.2.6.6. La classe TriggerUtils

La classe TriggerUtils propose des utilitaires qui peuvent être pratiques.

Avant la version 2.0, elle propose :

  • plusieurs surcharges de la méthode getDateOf() pour obtenir une instance de type Date
  • de nombreuses méthodes getXXXDate() et getXXXDateBefore() pour obtenir une instance de type Date
  • de nombreuses méthodes et leurs surcharges makeXXXTrigger() pour obtenir des instances de type Trigger
  • deux surcharges de la méthode setTriggerIdentity()

A partir de la version 2.0, elle ne propose plus que trois méthodes qui permettent de faire des calculs relatifs au nombre de déclenchements d'un trigger.

  • static Date computeEndTimeToAllowParticularNumberOfFirings(OperableTrigger trigg, Calendar cal, int numTimes) : renvoyer une instance de type Date encapsulant le déclenchement suivant les numTimes premiers déclenchements pour le trigger et le Calendar fournis
  • static List<Date> computeFireTimes(OperableTrigger trigg, Calendar cal, int numTimes) : renvoyer une collection de type Date contenant les numTimes premiers déclenchements pour le trigger et le Calendar fournis
  • static List<Date> computeFireTimesBetween(OperableTrigger trigg, Calendar cal, Date from, Date to) : renvoyer une collection de type Date contenant les déclenchements entre les deux dates fournies en paramètres pour le trigger et le Calendar fournis
Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;
import java.util.Date;
import java.util.List;

import org.quartz.CalendarIntervalScheduleBuilder;
import org.quartz.DateBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.spi.OperableTrigger;

public class TestQuartz {

  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();

      final JobDetail jobDetail = JobBuilder.newJob(MonJob.class)
        .withIdentity("monJob").storeDurably().build();

      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity("monTrigger", "monGroupe")
          .startAt(DateBuilder.tomorrowAt(12, 0, 0))
          .withSchedule(CalendarIntervalScheduleBuilder
            .calendarIntervalSchedule().withIntervalInDays(2))
          .build();

      scheduler.scheduleJob(jobDetail, trigger);
      scheduler.start();

      final List<Date> dates = TriggerUtils
        .computeFireTimes((OperableTrigger) trigger, null, 20);

      for (final Date date : dates) {
        System.out.println(date);
      }

      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

 

111.2.7. La gestion des jobs et des triggers

L'interface Scheduler propose de nombreuses méthodes qui permettent de gérer les triggers et les jobs.

Méthodes

Rôle

void addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers)

Enregistrer le Calendar avec le nom fourni en paramètres

void addJob(JobDetail jobDetail, boolean replace)

Enregistrer le job dans le scheduler sans l'associer avec un trigger

boolean checkExists(JobKey jobKey)

Vérifier si un job dont l'identifiant est fourni en paramètre est déjà enregistré dans le scheduler

boolean checkExists(TriggerKey triggerKey)

Vérifier si un trigger dont l'identifiant est fourni en paramètre est déjà enregistré dans le scheduler

void clear()

Supprimer tous les éléments enregistrés dans le scheduler (Triggers, Jobs et Calendars)

boolean deleteCalendar(String calName)

Supprimer le Calendar dont le nom est fourni en paramètre

boolean deleteJob(JobKey jobKey)

Supprimer le job dont l'identifiant est fourni en paramètre. Les triggers associés sont aussi retirés

boolean deleteJobs(List<JobKey> jobKeys)

Supprimer les jobs dont les identifiants sont fournis en paramètre. Les triggers associés sont aussi retirés

Calendar getCalendar(String calName)

Obtenir l'instance de Calendar enregistré avec le nom fourni en paramètre

List<String> getCalendarNames()

Obtenir la liste des noms de Calendar enregistrés

SchedulerContext getContext()

Obtenir l'instance de type SchedulerContext

List<JobExecutionContext> getCurrentlyExecutingJobs()

Obtenir la liste des JobExecutionContext des jobs en cours d'exécution dans le scheduler

JobDetail getJobDetail(JobKey jobKey)

Obtenir le JobDetail du job dont l'identifiant est fourni en paramètre

List<String> getJobGroupNames()

Obtenir la liste de tous les groupes associés à au moins un job

Set<JobKey> getJobKeys(GroupMatcher<JobKey> matcher)

Obtenir les identifiants des jobs qui sont associés à des groupes répondant au GroupMatcher fourni en paramètre

ListenerManager getListenerManager()

Obtenir l'instance de type ListenerManager permettant de gérer les listeners

SchedulerMetaData getMetaData()

Obtenir l'instance de type SchedulerMetaData

Set<String> getPausedTriggerGroups()

Obtenir la liste des noms de groupes de triggers qui sont en pause

String getSchedulerInstanceId()

Renvoyer l'identifiant de l'instance

String getSchedulerName()

Renvoyer le nom du scheduler

Trigger getTrigger(TriggerKey triggerKey)

Obtenir le trigger dont l'identifiant est fourni en paramètre

List<String> getTriggerGroupNames()

Obtenir la liste de tous les groupes associés à au moins un trigger

Set<TriggerKey> getTriggerKeys(GroupMatcher<TriggerKey> matcher)

Obtenir les identifiants des triggers qui sont associés à des groupes répondant au GroupMatcher fourni en paramètre

List<? extends Trigger> getTriggersOfJob(JobKey jobKey)

Obtenir une liste des triggers qui sont associés au job dont l'identifiant est passé en paramètre

Trigger.TriggerState getTriggerState(TriggerKey triggerKey)

Obtenir l'état d'un trigger dont l'identifiant est fourni en paramètre

boolean interrupt(JobKey jobKey)

Demander l'interruption de l'exécution dans l'instance du scheduler du ou des jobs dont l'identifiant est fourni en paramètre. Les jobs concernés doivent implémenter l'interface InterruptableJob interface

boolean isInStandbyMode()

Déterminer si le scheduler est en mode standby

boolean isShutdown()

Déterminer si le scheduler a été arrêté

boolean isStarted()

Déterminer si le scheduler est démarré

void pauseAll()

Mettre en pause tous les triggers

void pauseJob(JobKey jobKey)

Mettre en pause le job dont l'identifiant est fourni en paramètre

void pauseJobs(GroupMatcher<JobKey> matcher)

Mettre en pause les jobs dont l'identifiant répond au GroupMatcher fourni en paramètre

void pauseTrigger(TriggerKey triggerKey)

Mettre en pause le trigger dont l'identifiant est fourni en paramètre

void pauseTriggers(GroupMatcher<TriggerKey> matcher)

Mettre en pause les triggers dont l'identifiant répond au GroupMatcher fourni en paramètre

Date rescheduleJob(TriggerKey triggerKey, Trigger newTrigger)

Supprimer le trigger dont l'identifiant est fourni en paramètre et enregistrer celui fourni en paramètre à sa place. Ils n'ont pas l'obligation d'avoir le même nom mais ils doivent être associé au même identifiant de job

void resumeAll()

Réactiver tous les triggers

void resumeJob(JobKey jobKey)

Réactiver le job dont l'identifiant est fourni en paramètre

void resumeJobs(GroupMatcher<JobKey> matcher)

Réactiver les jobs dont l'identifiant répond au GroupMatcher fourni en paramètre

void resumeTrigger(TriggerKey triggerKey)

Réactiver le trigger dont l'identifiant est fourni en paramètre

void resumeTriggers(GroupMatcher<TriggerKey> matcher)

Mettre en pause les triggers dont l'identifiant répond au GroupMatcher fourni en paramètre

Date scheduleJob(JobDetail jobDetail, Trigger trigger)

Enregistrer la planification du job avec le trigger fournis en paramètres

Date scheduleJob(Trigger trigger)

Enregistrer la planification du job associé au trigger fourni en paramètre

void scheduleJobs(Map<JobDetail,List<Trigger>> triggersAndJobs, boolean replace)

Planifier les jobs associés aux triggers fournis en paramètres

void setJobFactory(JobFactory factory)

Définir l'instance de la fabrique pour créer des instances de type Job

void shutdown()

Arrêter le scheduler

void shutdown(boolean waitForJobsToComplete)

Arrêter le scheduler avec une attente de la fin de l'exécution des jobs en cours

void standby()

Mettre le scheduler en mode standby : le déclenchement des triggers est suspendu

void start()

Démarrer le scheduler

void startDelayed(int seconds)

Démarrer le scheduler après avoir attendu le nombre de secondes fourni en paramètre

void triggerJob(JobKey jobKey)

Exécuter immédiatement le Job dont l'identifiant est fourni en paramètre

void triggerJob(JobKey jobKey, JobDataMap data)

Exécuter immédiatement le Job dont l'identifiant est fourni en paramètre avec les métadonnées fournies

boolean unscheduleJob(TriggerKey triggerKey)

Retirer le trigger dont l'identifiant est fourni en paramètre

boolean unscheduleJobs(List<TriggerKey> triggerKeys)

Retirer la liste de triggers fournie en paramètre


La planification d'un job

Il est nécessaire d'enregistrer la planification d'un job dans le scheduler pour permettre son déclenchement. Il faut utiliser une des surcharges de la méthode scheduleJob() ou la méthode scheduleJobs() du scheduler.

Exemple :
      scheduler.scheduleJob(jobDetail, trigger);

L'enregistrement d'un job pour une utilisation ultérieure

Il faut invoquer la méthode addJob() du scheduler en lui passant en paramètre l'instance de type JobDetail et le booléen true.

Exemple :
      final JobDetail jobDetail = JobBuilder
        .newJob(MonJob.class).withIdentity("monJob").storeDurably().build();
      scheduler.addJob(jobDetail, true);

La modification d'un trigger ou d'un job enregistré

Il faut invoquer la méthode rescheduleJob() du scheduler en lui passant en paramètre l'identifiant du trigger à remplacer et l'instance du Trigger. Le nouveau trigger sera associé au même job même si un autre job lui est déjà associé.

Exemple :
      final JobDetail jobDetail = JobBuilder
          .newJob(MonJob.class)
          .withIdentity("monJob")
          .storeDurably()
          .build();

      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity(new TriggerKey("monTrigger", "monGroup"))
          .withSchedule(
              SimpleScheduleBuilder
                  .simpleSchedule()
                  .withIntervalInHours(1)
                  .repeatForever())
          .startAt(DateBuilder.evenHourDate(new Date()))
          .build();

      scheduler.scheduleJob(jobDetail, trigger);

      final Trigger triggerNouveau = TriggerBuilder
          .newTrigger()
          .withIdentity(new TriggerKey("monTriggerNouveau", "monGroup"))
          .withSchedule(
              SimpleScheduleBuilder
                  .simpleSchedule()
                  .withIntervalInHours(2)
                  .repeatForever())
          .startAt(DateBuilder.evenMinuteDate(new Date()))
          .build();

      scheduler.rescheduleJob(trigger.getKey(), triggerNouveau);

La suppression d'un job ou de sa planification

Pour retirer la planification d'un job, il faut invoquer la méthode unscheduleJob() en lui passant en paramètre l'identifiant du trigger concerné. Pour retirer plusieurs jobs, il faut invoquer la méthode unscheduleJobs().

Exemple :
      scheduler.unscheduleJob(trigger.getKey());

Pour retirer un job de la ou les planifications qui lui sont associées, il faut invoquer la méthode deleteJob() en lui passant en paramètre l'identifiant du job concerné.

Exemple :
      scheduler.deleteJob(jobDetail.getKey());

La mise en pause d'un job ou d'un trigger

La méthode pauseJob() permet de suspendre la planification de l'exécution d'un job.

Exemple :
      scheduler.pauseJob(jobDetail.getKey());

La méthode resumeJob() permet de rependre la planification de l'exécution d'un job.

Exemple :
      scheduler.resumeJob(jobDetail.getKey());

L'obtention de la liste des jobs et des triggers

La méthode getJobGroupNames() permet d'obtenir la liste des groupes. Avant Quartz 2.0, la méthode getJobNames() qui attend en paramètre le nom d'un groupe permet d'obtenir les jobs qui appartiennent à ce groupe.

Exemple :
    for (String groupName : scheduler.getJobGroupNames()) {
        for (String jobName : scheduler.getJobNames(groupName)) {
          System.out.println("jobName : " + jobName + ", group : " + groupName); 
        } 
    }

A partir de Quartz 2.0, la méthode getJobKeys() qui attend en paramètre une instance de type GroupMatcher renvoie les jobs dont le nom du groupe répond au matcher.

Exemple :
    for(String group: scheduler.getJobGroupNames()) {
        for(JobKey jobKey : scheduler.getJobKeys(GroupMatcher.groupEquals(group))) {
            System.out.println("job : " + jobKey);
        }
    }

Pour obtenir la liste des prochains déclenchements de chaque job, il faut :

  • itérer sur chaque job
  • obtenir la liste des triggers associés au job
  • invoquer la méthode getNextFireTime() du premier trigger s'il existe

Avant Quartz 2.0 :

Exemple :
    for (String nomGroupe : scheduler.getJobGroupNames()) {
 
        for (String nomJob : scheduler.getJobNames(nomGroupe)) {
          Trigger[] triggers = scheduler.getTriggersOfJob(nomJob, nomGroupe);
          Date nextFireTime = null;
          if (triggers.length > 0) {
              nextFireTime = triggers[0].getNextFireTime();
          }
          System.out.println("Job : " + nomJob + " groupe : " + nomGroupe
              + ", prochain declenchement " + nextFireTime);
        } 
    }

A partir de Quartz 2.0 :

Exemple :
      for (final String nomGroupe : scheduler.getJobGroupNames()) {

        for (final JobKey jobKey : scheduler.getJobKeys(GroupMatcher
            .jobGroupEquals(nomGroupe))) {
          Date nextFireTime = null;
          final List<Trigger> triggers = (List<Trigger>) scheduler
              .getTriggersOfJob(jobKey);
          if (!triggers.isEmpty()) {
            nextFireTime = triggers.get(0).getNextFireTime();
          }
          System.out.println("Job : " + jobKey.getName() + " groupe : " + jobKey.getGroup()
              + ", prochain declenchement " + nextFireTime);
        }
      }

A partir de Quartz 2.0, il faut utiliser la méthode getTriggerKeys() qui attend en paramètre une instance de type GroupMatcher et renvoie les triggers dont le nom du groupe répond au matcher.

Exemple :
      for (final String group : scheduler.getTriggerGroupNames()) {
        for (final TriggerKey triggerKey : scheduler
            .getTriggerKeys(GroupMatcher.triggerGroupEquals(group))) {
          System.out.println("Trigger : " + triggerKey.getName() + ", groupe "
              + triggerKey.getGroup());
        }
      }

L'obtention des jobs associés à un trigger

La méthode getTriggerOfJob() permet d'obtenir la liste des triggers qui sont associés au job dont l'identifiant est fourni en paramètre.

Exemple :
      System.out.println("Trigger for job " + jobDetail.getKey());
      final List<? extends Trigger> jobTriggers = scheduler
          .getTriggersOfJob(jobDetail.getKey());
      for (final Trigger jobTrigger : jobTriggers) {
        System.out.println("Trigger : " + jobTrigger.getKey());
      }

L'arrêt d'un job en cours d'exécution

Pour permettre l'arrêt d'un job, sa classe doit implémenter l'interface InterruptableJob. Elle hérite de l'interface Job et ne définit qu'une seule méthode : void interrupt().

Cette méthode sera invoquée par le scheduler pour tenter d'arrêter l'exécution du job. En aucun cas elle ne permet d'arrêter directement l'exécution mais elle permet de demander son arrêt. Généralement son implémentation consiste à changer la valeur d'un boolean qui doit être périodiquement testé dans les traitements du job pour les arrêter. La logique d'interruption doit donc être codée dans les traitements du job eux-mêmes.

Pour interrompre un job en cours d'exécution, il faut :

  • Avant Quartz 2.0 : invoquer la méthode boolean interrupt(JobKey) qui attend en paramètre l'identifiant du job à arrêter
  • A partir de Quartz 2.0 : invoquer la méthode boolean interrupt(String, String) qui attend en paramètre le nom et le groupe du job à arrêter

 

111.2.8. Les listeners

Quartz propose la possibilité de réagir à des événements en utilisant des Listeners.

Quartz propose trois types de Listeners :

  • JobListener : pour être informé d'événements liés à l'exécution d'un job
  • TriggerListener : pour être informé d'événements liés au déclenchement d'un trigger
  • SchedulerListener : pour être informé d'événements liés au moteur de planification

L'implémentation d'un listener peut se faire de deux manières :

  • implémenter l'interface
  • hériter de la classe xxxSupport où xxx est le nom du listener et redéfinir la ou les méthodes utiles

Un listener doit avoir un nom retourné par la méthode getName()

Pour utiliser un listener, il faut :

  • créer une instance du listener
  • l'enregistrer dans le scheduler en utilisant son ListenerManager

Avant la version 2.0 de Quartz, l'enregistrement d'un listener dans le scheduler se fait en invoquant une de ses méthodes addXXXListener(). A partir de la version 2.0, l'enregistrement d'un listener dans le scheduler se fait en utilisant le ListenerManager qui lui est associé.

L'interface ListenerManager définit des méthodes pour gérer les listeners associés à un scheduler :

Méthode

Rôle

void addJobListener(JobListener jobListener, List<Matcher<JobKey>> matchers)
void addJobListener(JobListener jobListener, Matcher<JobKey>... matchers)

Ajouter un JobListener au Scheduler et l'enregistrer pour être invoqué sur les événements des Job dont la clé correspond à au moins un des Matcher fournis

boolean addJobListenerMatcher(String listenerName, Matcher<JobKey> matcher)

Ajouter un Matcher au listener dont le nom est fourni en paramètre

void addSchedulerListener(SchedulerListener schedulerListener)

Ajouter un SchedulerListener

void addTriggerListener(TriggerListener triggerListener, List<Matcher<TriggerKey>> matchers)
void addTriggerListener(TriggerListener triggerListener, Matcher<TriggerKey>... matchers)

Ajouter un TriggerListener et l'enregistrer dans le scheduler pour être invoqué sur les événements des triggers dont la clé correspond à au moins un des Matcher fournis

boolean addTriggerListenerMatcher(String listenerName, Matcher<TriggerKey> matcher)

Ajouter un Matcher au listener dont le nom est fourni en paramètre

JobListener getJobListener(String name)

Obtenir un JobListener à partir de son nom

List<Matcher<JobKey>> getJobListenerMatchers(String listenerName)

Obtenir les Matcher associés au listener dont le nom est fourni en paramètre

List<JobListener> getJobListeners()

Obtenir tous les JobListener enregistrés

List<SchedulerListener> getSchedulerListeners()

Obtenir tous les SchedulerListener

TriggerListener getTriggerListener(String name)

Obtenir un TriggerListener à partir de son nom

List<Matcher<TriggerKey>> getTriggerListenerMatchers(String listenerName)

Obtenir les Matcher associés au listener dont le nom est fourni en paramètre

List<TriggerListener> getTriggerListeners()

Obtenir tous les TriggerListener enregistrés

boolean removeJobListener(String name)

Retirer le JobListener dont le nom est fourni en paramètre

boolean removeJobListenerMatcher(String listenerName, Matcher<JobKey> matcher)

Retirer le Matcher associé au listener dont le nom est fourni en paramètre

boolean removeSchedulerListener(SchedulerListener schedulerListener)

Retirer le schedulerListener fourni en paramètre du scheduler

boolean removeTriggerListener(String name)

Retirer le TriggerListener dont le nom est fourni en paramètre

boolean removeTriggerListenerMatcher(String listenerName, Matcher<TriggerKey> matcher)

Retirer le Matcher associé au listener dont le nom est fourni en paramètre

boolean setJobListenerMatchers(String listenerName, List<Matcher<JobKey>> matchers)

Définir les Matchers associés au listener dont le nom est fourni en paramètre

boolean setTriggerListenerMatchers(String listenerName, List<Matcher<TriggerKey>> matchers)

Définir les Matchers associés au listener dont le nom est fourni en paramètre


Lors de l'enregistrement d'un listener, il est possible de filtrer les éléments (Job ou Trigger) qui seront concernés par les événements en utilisant un objet de type org.quartz.Matcher.

Quartz fournit en standard plusieurs implémentations de type Matcher pour définir le filtre : AndMatcher, EverythingMatcher, GroupMatcher, KeyMatcher, NameMatcher, NotMatcher, OrMatcher et StringMatcher.

Les listeners ne sont pas enregistrés dans le JobStore : il est donc nécessaire d'enregistrer les listeners à chaque démarrage du scheduler. Il est cependant possible de configurer des listeners globaux qui seront instanciés et enregistrés par la fabrique au démarrage du scheduler.

 

111.2.8.1. JobListener

Un JobListener permet de réagir à des événements liés à l'exécution d'un job.

L'interface JobListener définit plusieurs méthodes :

Méthode

Rôle

String getName()

Obtenir le nom du listener

void jobExecutionVetoed(JobExecutionContext context)

Invoquée par le scheduler lorsqu'un JobDetail est sur le point d'être exécuté (suite au déclenchement d'un trigger lié au JobDetail) mais qu'un TriggerListener a mis un véto sur son exécution

void jobToBeExecuted(JobExecutionContext context)

Invoquée par le scheduler lorsqu'un JobDetail est sur le point d'être exécuté (suite au déclenchement d'un trigger lié au JobDetail)

void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException)

Invoquée par le scheduler lorsqu'un JobDetail a été exécuté


Exemple :
package fr.jmdoudoux.dej.quartz;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.listeners.JobListenerSupport;

public class MonJobListener extends JobListenerSupport {

  @Override
  public String getName() {
    return "MonJobListener";
  }

  @Override
  public void jobToBeExecuted(final JobExecutionContext context) {
    final String jobName = context.getJobDetail().getKey().toString();
    System.out.println("Le job : " + jobName + " va demarrer");
  }

  @Override
  public void jobWasExecuted(final JobExecutionContext context,
      final JobExecutionException jobException) {

    final String jobName = context.getJobDetail().getKey().toString();
    System.out.println("Le job : " + jobName + " est termine");

    if (jobException != null) {
      System.out.println("Exception levee par le job " + jobName + " : "
          + jobException.getMessage());
    }
  }
}
Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;

import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.KeyMatcher;

public class TestQuartz {

  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();

      final JobDetail jobDetail = JobBuilder
          .newJob(MonJob.class)
          .withIdentity("monJob")
          .storeDurably()
          .build();

      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity(new TriggerKey("monTrigger", "monGroup"))
          .withSchedule(
              SimpleScheduleBuilder
                  .simpleSchedule()
                  .withIntervalInHours(1)
                  .repeatForever())
          .startNow()
          .build();

      scheduler.getListenerManager().addJobListener(new MonJobListener(),
          KeyMatcher.keyEquals(jobDetail.getKey()));

      scheduler.scheduleJob(jobDetail, trigger);
      scheduler.start();

      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

 

111.2.8.2. TriggerListener

Un TriggerListener permet de réagir à des événements lies à l'exécution d'un trigger.

L'interface TriggerListener définit plusieurs méthodes :

Méthode

Rôle

String getName()

Obtenir le nom du listener

void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode)

Invoquée par le scheduler lorsque les traitements du déclenchement d'un trigger sont terminés (le job associé a été exécuté)

void triggerFired(Trigger trigger, JobExecutionContext context)

Invoquée par le scheduler lorsqu'un trigger est déclenché et que son Job associé est prêt à être exécuté

void triggerMisfired(Trigger trigger)

Invoquée par le scheduler lorsque le déclenchement d'un trigger est raté

boolean vetoJobExecution(Trigger trigger, JobExecutionContext context)

Invoquée par le scheduler lorsqu'un trigger est déclenché et que son Job associé est prêt à être exécuté


La méthode vetoJobExecution() permet de mettre un véto sur l'exécution du job lié au trigger simplement en renvoyant true. Elle est invoquée par le scheduler après la méthode triggerFired().

Exemple :
package fr.jmdoudoux.dej.quartz;

import org.quartz.JobExecutionContext;
import org.quartz.Trigger;
import org.quartz.listeners.TriggerListenerSupport;

public class MonTriggerListener extends TriggerListenerSupport {

  private final String name;

  public MonTriggerListener(final String name) {
    this.name = name;
  }

  @Override
  public String getName() {
    return this.name;
  }

  @Override
  public void triggerFired(final Trigger trigger,
      final JobExecutionContext context) {
    final String triggerName = context.getTrigger().getKey().toString();
    System.out.println("Le trigger : " + triggerName + " est declenche");
  }
}
Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;

import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.KeyMatcher;

public class TestQuartz {

  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();

      final JobDetail jobDetail = JobBuilder
          .newJob(MonJob.class)
          .withIdentity("monJob")
          .storeDurably()
          .build();

      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity(new TriggerKey("monTrigger", "monGroup"))
          .withSchedule(
              SimpleScheduleBuilder
                  .simpleSchedule()
                  .withIntervalInHours(1)
                  .repeatForever())
          .startNow()
          .build();

      scheduler.getListenerManager().addTriggerListener(
          new MonTriggerListener("monTriggerListener"),
          KeyMatcher.keyEquals(trigger.getKey()));

      scheduler.scheduleJob(jobDetail, trigger);
      scheduler.start();

      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

 

111.2.8.3. SchedulerListener

Un SchedulerListener permet de réagir à des événements liés au scheduler lui-même notamment ceux concernant son cycle de vie : l'ajout/suppression de jobs/triggers, la mise en pause ou la reprise de jobs/triggers et la levée d'erreurs.

L'interface SchedulerListener définit plusieurs méthodes :

Méthode

Rôle

void jobAdded(JobDetail jobDetail)

Invoquée par le scheduler lorsqu'un JobDetail est ajouté

void jobDeleted(JobKey jobKey)

Invoquée par le scheduler lorsqu'un job est supprimé

void jobPaused(JobKey jobKey)

Invoquée par le scheduler lorsqu'un job est mis en pause

void jobResumed(JobKey jobKey)

Invoquée par le scheduler lorsqu'un job est redémarré

void jobScheduled(Trigger trigger)

Invoquée par le scheduler lorsqu'un job est planifié

void jobsPaused(String jobGroup)

Invoquée par le scheduler lorsqu'un groupe de jobs est mis en pause

void jobsResumed(String jobGroup)

Invoquée par le scheduler lorsqu'un group de jobs est redémarré

void jobUnscheduled(TriggerKey triggerKey)

Invoquée par le scheduler lorsque la planification d'un job est supprimée

void schedulerError(String msg, SchedulerException cause)

Invoquée par le scheduler lorsqu'une erreur grave survient dans ses propres traitements (échec de mises à jour du JobStore, impossible d'instancier un job lors du déclenchement d'un trigger, ...)

void schedulerInStandbyMode()

Invoquée par le scheduler lorsque son état passe à standby

void schedulerShutdown()

Invoquée par le scheduler lorsqu'il s'arrête

void schedulerShuttingdown()

Invoquée par le scheduler lorsqu'il débute ses traitements pour son arrêt

void schedulerStarted()

Invoquée par le scheduler lorsqu'il démarre

void schedulerStarting()

Invoquée par le scheduler lorsqu'il débute ses traitements pour son démarrage

void schedulingDataCleared()

Invoquée par le scheduler lorsque tous les jobs, triggers et calendars ont été supprimés

void triggerFinalized(Trigger trigger)

Invoquée par le scheduler lorsque tous les déclenchements du trigger ont été réalisés

void triggerPaused(TriggerKey triggerKey)

Invoquée par le scheduler lorsqu'un trigger est mis en pause

void triggerResumed(TriggerKey triggerKey)

Invoquée par le scheduler lorsqu'un trigger est redémarré

void triggersPaused(String triggerGroup)

Invoquée par le scheduler lorsqu'un groupe de triggers est mis en pause

void triggersResumed(String triggerGroup)

Invoquée par le scheduler lorsqu'un groupe de triggers est redémarré


Exemple :
package fr.jmdoudoux.dej.quartz;

import org.quartz.Trigger;
import org.quartz.listeners.SchedulerListenerSupport;

public class MonSchedulerListener extends SchedulerListenerSupport {

  @Override
  public void schedulerStarted() {
    System.out.println("Demarrage scheduler");
  }

  @Override
  public void schedulerShutdown() {
    System.out.println("Arret scheduler");
  }

  @Override
  public void jobScheduled(final Trigger trigger) {
    System.out.println("Planification du job " + trigger.getJobKey()
        + " avec le trigger " + trigger.getKey());
  }
}
Exemple :
package fr.jmdoudoux.dej.quartz;

import java.io.IOException;
import java.util.Date;

import org.quartz.DateBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;

public class TestQuartz {

  public static void main(final String[] args) {
    final SchedulerFactory factory = new StdSchedulerFactory();
    Scheduler scheduler = null;
    try {
      scheduler = factory.getScheduler();

      final JobDetail jobDetail = JobBuilder
          .newJob(MonJob.class)
          .withIdentity("monJob")
          .storeDurably()
          .build();

      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity(new TriggerKey("monTrigger", "monGroup"))
          .withSchedule(
              SimpleScheduleBuilder
                  .simpleSchedule()
                  .withIntervalInHours(1)
                  .repeatForever())
          .startAt(DateBuilder.evenMinuteDate(new Date()))
          .build();

      scheduler.getListenerManager().addSchedulerListener(
          new MonSchedulerListener());

      scheduler.scheduleJob(jobDetail, trigger);
      scheduler.start();

      System.in.read();
      if (scheduler != null) {
        scheduler.shutdown();
      }
    } catch (final SchedulerException e) {
      e.printStackTrace();
    } catch (final IOException e) {
      e.printStackTrace();
    }
  }
}

 

111.2.9. Les JobStores

Un JobStore a la responsabilité de stocker les éléments utilisés par le scheduler (Jobs, Triggers, Calendars, ...).

Le JobStore utilisé par le Scheduler est défini et configuré dans les propriétés fournies à la fabrique de type SchedulerFactory.

Quartz fournit plusieurs JobStore en standard :

  • RAMJobStore : il stocke les informations en mémoire
  • JDBCJobStore : il stocke les informations dans une base de données relationnelle
  • TerracottaJobStore : il stocke les informations dans un serveur Terracotta

 

111.2.9.1. Le RAMJobStore

C'est le plus simple et le plus performant des JobStore mais il n'est pas persistant. Toutes les informations sont donc perdues lors de l'arrêt normal ou non de la JVM dans laquelle le scheduler s'exécute. C'est le JobStore utilisé par défaut.

Pour configurer Quartz à utiliser le RAMJobStore, il faut le préciser dans le fichier de configuration :

Exemple :
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

 

111.2.9.2. Le JDBCJobStore

Un JDBCJobStore stocke les informations dans une base de données relationnelles. Il est nécessaire de créer les différentes tables requises en utilisant les scripts SQL contenus dans le sous-répertoire docs/dbTables de la distribution Quartz.

Le préfixe du nom des tables est configurable : cela peut être pratique pour stocker la configuration de plusieurs schedulers dans la même base de données.

Deux classes d'implémentation sont proposées par Quartz selon le type de transactions à utiliser :

  • JobStoreTX pour laisser Quartz gérer lui-même ses transactions
  • JobStoreCMT pour s'intégrer dans une transaction distribuée

Le paramétrage du JDBCJobStore se fait dans le fichier de configuration de Quartz notamment la définition de la Datasource qui peut être créée par Quartz ou obtenue d'un serveur d'applications par JNDI.

 

111.2.10. La configuration

Quartz propose un ensemble de fonctionnalités configurables dans un fichier de type properties :

  • identification du scheduler
  • pool de threads
  • persistance des jobs et triggers
  • utilisation de Quartz en mode client/serveur
  • les listeners globaux
  • les plugins
Exemple :
org.quartz.scheduler.instanceName = MonScheduler
org.quartz.threadPool.threadCount = 3
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

La configuration de Quartz se définit dans un fichier properties qui sera utilisé par la classe StdSchedulerFactory pour créer une instance de type Scheduler selon la configuration.

Par défaut, elle recherche un fichier nommé quart.properties dans le répertoire courant. Si le fichier n'est pas trouvé, alors c'est le fichier quartz.properties contenu dans le package org.quartz qui est chargé.

Pour préciser un autre fichier de configuration à utiliser, il est possible de :

  • le passer comme valeur à la propriété org.quartz.properties de la JVM.
  • le passer en paramètre d'une des surcharges de la méthode initialize() de la classe StdSchedulerFactory. La méthode initialize() doit être invoquée en la méthode getScheduler()

La configuration de certaines instances se fait en les identifiant grâce à un nom unique contenu dans leur clé.

Une propriété peut faire référence à la valeur d'une autre propriété en préfixant son nom avec $@. Par exemple, "$@org.quartz.scheduler.instanceName" sera remplacé par le nom de l'instance du Scheduler.

 

111.2.10.1. La configuration globale

Plusieurs propriétés concernent la configuration globale du scheduler, notamment :

Nom de la propriété

Type

Description

org.quartz.scheduler.instanceName

String

Définir le nom logique du scheduler. Dans un cluster, tous les schedulers doivent avoir le même nom logique.

Valeur par défaut : «QuartzScheduler»

org.quartz.scheduler.instanceId

String

Identifiant de l'instance du scheduler. Doit être unique pour chaque scheduler d'un même cluster. La valeur doit être AUTO pour laisser Quartz donner un identifiant ou SYS_PROP pour que Quartz récupère la valeur de la propriété org.quartz.scheduler.instanceId de la JVM

Valeur par défaut : «NON_CLUSTERED»

org.quartz.scheduler.instanceIdGenerator.class

String

N'est utilisé que si la propriété org.quartz.scheduler.instanceId vaut AUTO.

Plusieurs classes sont proposées en standard par Quartz :

  • org.quartz.simpl.SimpleInstanceIdGenerator qui génère une valeur à partir du hostname et du timestamp courant
  • org.quartz.simpl.SystemPropertyInstanceIdGenerator qui obtient l'identifiant à partir de la propriété org.quartz.scheduler.instanceId
  • org.quartz.simpl.HostnameInstanceIdGenerator qui obtient l'identifiant à partir du hostname

Valeur par défaut : org.quartz.simpl.SimpleInstanceIdGenerator

org.quartz.scheduler.threadName

String

Définir le nom des threads

Valeur par défaut : instanceName + '_QuartzSchedulerThread'

org.quartz.scheduler.makeSchedulerThreadDaemon

boolean

Préciser si le thread principal du scheduler doit être de type démon (true) ou non (false)

Valeur par défaut : false

org.quartz.scheduler.dbFailureRetryInterval

long

Définir le nombre de millisecondes que le scheduler va attendre pour se reconnecter au système sous-jacent du JobStore (par exemple une base de données) après une perte de connexion.

Valeur par défaut : 15000

org.quartz.scheduler.jobFactory.class

String

Nom pleinement qualifié de la classe de type JobFactory utilisée par Quartz pour créer des instances de type Job.

Quartz fournit plusieurs classes d'implémentation possibles :

  • org.quartz.simpl.PropertySettingJobFactory qui crée une nouvelle instance en invoquant newInstance()
  • org.quartz.simpl.PropertySettingJobFactory qui crée une instance par introspection en récupérant les informations dans le contexte du scheduler et les JobDataMaps du Job et du Trigger

Valeur par défaut : org.quartz.simpl.PropertySettingJobFactory

org.quartz.context.key.SOME_KEY

String

Définir une paire clé/valeur qui sera ajoutée dans le contexte du scheduler. Il faut remplacer SOME_KEY par le nom de la clé

Exemple :
org.quartz.context.key.MaCle = maValeur est équivalent à
scheduler.getContext().put("MaCle", "maValeur")

org.quartz.scheduler.userTransactionURL

String

Préciser l'URL JNDI du gestionnaire de transactions distribuées du serveur d'applications.

Valeur par défaut : 'java:comp/UserTransaction'

Org.quartz.scheduler.wrapJobExecutionInUserTransaction

boolean

Préciser si Quartz doit démarrer une nouvelle UserTransaction avant d'exécuter un job. La transaction est validée lorsque le job est terminé.

Valeur par défaut : false

org.quartz.scheduler.skipUpdateCheck

boolean

Préciser si Quartz doit vérifier sur Internet s'il existe une version plus récente.

La propriété org.terracotta.quartz.skipUpdateCheck de la JVM permet de préciser si la vérification doit être activée.

Il est préférable de désactiver cette fonctionnalité en production.

Valeur par défaut : false

 

111.2.10.2. La configuration de listeners globaux

La fabrique StdChedulerFactory peut lire, créer et enregistrer des listeners dans le scheduler au moment de sa création. Ces listeners seront globaux donc concerneront tous les événements de l'instance de leur type (job et trigger).

Chaque listener doit être défini et identifié avec un nom unique contenu dans les clés le concernant.

Pour définir un listener, il faut :

  • utiliser une clé de la forme org.quartz.xxxListener.NAME.class ayant pour valeur le nom pleinement qualifié du Listener
  • une clé de la forme org.quartz.xxxListener.NAME.yyyyyy pour assigner une valeur à la propriété yyyyy du listener

Dans la forme des clés, il faut remplacer :

  • xxx par le type de listener : job ou trigger
  • NAME par le nom unique de l'instance
  • yyyyyy par le nom de la propriété correspondante
Exemple de configuration d'un JobListener :
org.quartz.jobListener.MonJobListener.class = fr.jmdoudoux.dej.quartz.MonJobListener
org.quartz.jobListener.MonJobListener.min = 100
org.quartz.jobListener.MonJobListener.max = 1000

 

111.2.10.3. La configuration des exécutions concurrentes des jobs

Quartz utilise un pool de threads pour permettre d'exécuter des jobs en parallèle.

Lorsqu'un trigger déclenche l'exécution d'un job, le scheduler obtient un thread libre de son pool et lui fait exécuter les traitements du job.

La taille du pool de threads doit être configurée pour permettre l'exécution en simultanée de tous les jobs requis.

Cette configuration se fait grâce à plusieurs propriétés :

Nom

Description

org.quartz.threadPool.class

Nom pleinement qualifié de l'implémentation du pool de threads à utiliser.

Valeur par défaut : null

org.quartz.threadPool.threadCount

Valeur entière positive qui représente le nombre de threads dans le pool

Valeur par défaut : -1 qui signifie pas de pool de threads

org.quartz.threadPool.threadPriority

Entier compris entre Thread.MIN_PRIORITY(1) et Thread.MAX_PRIORITY(10) qui précise la priorité des threads (optionnel)

Valeur par défaut : Thread.NORM_PRIORITY (5)


Plusieurs propriétés sont spécifiques à la classe SimpleThreadPool qui est l'implémentation du pool de threads fournie par défaut par Quartz.

Nom

Description

org.quartz.threadPool.makeThreadsDaemons

Les threads du pool sont des démons (optionnel)
Valeur par défaut : false

org.quartz.threadPool.threadsInheritGroupOfInitializingThread

(optionnel)
Valeur par défaut : true

org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread

(optionnel)
Valeur par défaut : false

org.quartz.threadPool.threadNamePrefix

Préfixe utilisé dans le nom des threads du pool (optionnel)
Valeur par défaut : [Scheduler Name]_Worker

 

111.2.10.4. Le stockage des entités de planification

Les entités utilisées par le scheduler pour réaliser la planification sont stockées dans un JobStore. Quartz propose plusieurs implémentations d'un JobStore, chacune possédant ses propres propriétés pour sa configuration.

Un RAMJobStore stocke les informations de planification du scheduler en mémoire. Pour utiliser un JobStore de type RAMJobStore, il faut préciser le nom pleinement qualifié de la classe comme valeur pour la propriété org.quartz.jobstore.class.

Exemple :
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

Un RAMJobStore peut être configuré grâce à une propriété :

Nom de la propriété

Type

Description

org.quartz.jobStore.misfireThreshold

int

Définir un nombre de millisecondes de tolérance avant que le déclenchement d'un trigger ne soit considéré comme raté

Valeur par défaut : 60000


Un JDBCStore permet de stocker les informations des entités de planification (jobs, triggers, calendars, ...) dans une base de données relationnelle.

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

Deux implémentations sont utilisables selon le comportement transactionnel souhaité :

  • JobStoreTX : le JobStore assure lui-même la gestion des transactions
  • JobStoreCMT : le JobStore utilise un gestionnaire de transactions distribuées

Un JobStoreTX est à mettre en oeuvre si aucune transaction distribuée n'est utilisée.

Pour utiliser un JobStore de type JobStoreTX, il faut préciser le nom pleinement qualifié de la classe comme valeur pour la propriété org.quartz.jobstore.class

Exemple :
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

Un JobStoreTX peut être configuré grâce à plusieurs propriétés, notamment :

Nom de la propriété

Type

Description

org.quartz.jobStore.driverDelegateClass

String

Définir le nom pleinement qualifié de la classe qui va assurer la communication avec une base de données précise. (Obligatoire)

Quartz propose une implémentation générique et plusieurs implémentations pour le support des principales bases de données lorsque celles-ci requièrent des opérations spécifiques :

org.quartz.impl.jdbcjobstore.StdJDBCDelegate : support générique pour un pilote JDBC
org.quartz.impl.jdbcjobstore.MSSQLDelegate pour SQL Server
org.quartz.impl.jdbcjobstore.PostgreSQLDelegate pour PostgreSQL
org.quartz.impl.jdbcjobstore.oracle.OracleDelegate pour Oracle
org.quartz.impl.jdbcjobstore.DB2v6Delegate, org.quartz.impl.jdbcjobstore.DB2v7Delegate, org.quartz.impl.jdbcjobstore.DB2v8Delegate pour DB2
org.quartz.impl.jdbcjobstore.HSQLDBDelegate pour HSQLDB
org.quartz.impl.jdbcjobstore.SybaseDelegate pour Sybase
org.quartz.impl.jdbcjobstore.CloudscapeDelegate pour Cloudscape et Derby

Valeur par défaut : null

org.quartz.jobStore.dataSource

String

Préciser le nom de la datasource : ce nom doit être défini dans la configuration (Obligatoire)

Valeur par défaut : null

org.quartz.jobStore.tablePrefix

String

Définir le préfixe des tables dans la base de données

Valeur par défaut : "QRTZ_"

org.quartz.jobStore.useProperties

boolean

Indiquer au JDBCJobStore que toutes les valeurs des JobDataMaps sont des String ce qui évite d'avoir à les sérialiser et stocker ces valeurs dans un champ de type blob

Valeur par défaut : false

org.quartz.jobStore.misfireThreshold

int

Définir un nombre de millisecondes de tolérance avant que le déclenchement d'un trigger ne soit considéré comme raté

Valeur par défaut : 60000

org.quartz.jobStore.isClustered

boolean

Préciser si la base de données est utilisée par plusieurs instances de Quartz.

Valeur par défaut : false

org.quartz.jobStore.dontSetAutoCommitFalse

boolean

Préciser de ne pas invoquer la méthode setAutoCommit(false) sur les connexions à la DataDource.

Valeur par défaut : false

org.quartz.jobStore.selectWithLockSQL

String

Définir la requête SQL qui est utilisée pour poser un verrou dans la table xxxLOCKS

{0} est remplacé à l'exécution par le préfixe des tables

{1} est remplacé à l'exécution par le nom du scheduler

Valeur par défaut : "SELECT * FROM {0}LOCKS WHERE SCHED_NAME = {1} AND LOCK_NAME = ? FOR UPDATE"

org.quartz.jobStore.txIsolationLevelSerializable

boolean

Demander d'utiliser le niveau d'isolation Connection.TRANSACTION_SERIALIZABLE

Valeur par défaut : false

org.quartz.jobStore.driverDelegateInitString

String

Chaîne de caractères permettant de préciser des paires clé=valeur qui seront utilisées par le DriverDelegate. Les paires son séparées par un caractère |

Exemple : "propriete1=valeur1|propriete2=valeur2"
Valeur par défaut : null


Exemple :
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX 
org.quartz.jobStore.driverDelegateClass =  org.quartz.impl.jdbcjobstore.StdJDBCDelegate 
org.quartz.jobStore.dataSource = QUARTZ_DS 
org.quartz.jobStore.tablePrefix = QUARTZ_ 

Un JobStoreCMT est à mettre en oeuvre si les opérations du JobStore doivent être incluses dans une transaction distribuée. Le gestionnaire de transaction doit être démarré avant le scheduler.

Un JobStoreCMT a besoin de deux datasources :

  • une qui soit incluse dans la transaction JTA
  • une qui ne le soit pas

Pour utiliser un JobStore de type JobStoreCMT, il faut préciser le nom pleinement qualifié de la classe comme valeur pour la propriété org.quartz.jobstore.class

Exemple :
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreCMT

Un JobStoreCMT peut être configuré grâce à plusieurs propriétés :

Nom de la propriété

Type

Description

org.quartz.jobStore.driverDelegateClass

String

Identique à la propriété correspondante du JobStoreTX (Obligatoire)

Valeur par défaut : null

org.quartz.jobStore.dataSource

String

Préciser le nom de la datasource dont les connexions doivent être compatibles JTA : ce nom doit être défini dans la configuration (Obligatoire)

Valeur par défaut : null

org.quartz.jobStore.nonManagedTXDataSource

String

Préciser le nom de la datasource dont les connexions ne doivent pas être compatibles JTA : ce nom doit être défini dans la configuration (Obligatoire)

Valeur par défaut :

org.quartz.jobStore.tablePrefix

String

Identique à la propriété correspondante du JobStoreTX

Valeur par défaut : "QRTZ_"

org.quartz.jobStore.useProperties

boolean

Identique à la propriété correspondante du JobStoreTX

Valeur par défaut : false

org.quartz.jobStore.misfireThreshold

int

Identique à la propriété correspondante du JobStoreTX

Valeur par défaut : 60000

org.quartz.jobStore.isClustered

boolean

Identique à la propriété correspondante du JobStoreTX

Valeur par défaut : false

org.quartz.jobStore.dontSetAutoCommitFalse

boolean

Identique à la propriété correspondante du JobStoreTX

Valeur par défaut : false

org.quartz.jobStore.selectWithLockSQL

String

Identique à la propriété correspondante du JobStoreTX

Valeur par défaut : "SELECT * FROM {0}LOCKS WHERE SCHED_NAME = {1} AND LOCK_NAME = ? FOR UPDATE"

org.quartz.jobStore.txIsolationLevelSerializable

boolean

Identique à la propriété correspondante du JobStoreTX

Valeur par défaut : false

org.quartz.jobStore.txIsolationLevelReadCommitted

boolean

Identique à la propriété correspondante du JobStoreTX

Valeur par défaut : false

org.quartz.jobStore.driverDelegateInitString

String

Identique à la propriété correspondante du JobStoreTX

Valeur par défaut : null


La configuration d'une datasource utilisée par le JDBCJobStore peut se faire de trois manières :

  • définir chacune des propriétés dans le fichier de configuration pour permettre à Quartz de créer sa propre instance
  • préciser le nom JNDI de la Datasource définie dans le serveur d'applications
  • créer et utiliser une implémentation de l'interface org.quartz.utils.ConnectionProvider

Il est préférable que la taille du pool de connexions lié à la Datasource soit plus grande que la taille du pool de threads. Lors de l'utilisation du JobStoreCMT, la taille du pool de la Datasource non JTA doit être au moins de 4.

Une Datasource créée par Quartz peut être configurée grâce à plusieurs propriétés :

Nom de la propriété

Type

Description

org.quartz.dataSource.NAME.driver

String

Nom pleinement qualifié de la classe Java qui correspond au driver JDBC à utiliser (Obligatoire)

Valeur par défaut : null

org.quartz.dataSource.NAME.URL

String

L'url de connexion à la base de données

Valeur par défaut : null

org.quartz.dataSource.NAME.user

String

Le nom de l'utilisateur utilisé pour se connecter à la base de données

Valeur par défaut : ""

org.quartz.dataSource.NAME.password

String

Le mot de passe de l'utilisateur utilisé pour se connecter à la base de données

Valeur par défaut : ""

org.quartz.dataSource.NAME.maxConnections

int

Le nombre maximum de connexions que pourra contenir le pool de connexions

Valeur par défaut : 10

org.quartz.dataSource.NAME.validationQuery

String

Requête SQL utilisée pour tester les connexions invalides

Valeur par défaut : null

org.quartz.dataSource.NAME.validateOnCheckout

boolean

Demander l'exécution d'une requête SQL chaque fois d'une connexion est obtenue du pool pour vérifier qu'elle est toujours valide

Valeur par défaut : false

org.quartz.dataSource.NAME.discardIdleConnectionsSeconds

int

Timeout en secondes pour retirer une connexion inutilisée du pool de connexions

Valeur par défaut : 0


Exemple :
org.quartz.dataSource.QUARTZ_DS.driver = oracle.jdbc.driver.OracleDriver
org.quartz.dataSource.QUARTZ_DS.URL = jdbc:oracle:thin:@localhost:1521:demodb
org.quartz.dataSource.QUARTZ_DS.user = mon_user
org.quartz.dataSource.QUARTZ_DS.password = user_mdp
org.quartz.dataSource.QUARTZ_DS.maxConnections = 10

Chaque Datasource définie doit avoir un nom unique qui correspond à NAME dans le tableau ci-dessous. L'utilisation de Datasource configurée dans le serveur d'applications se fait grâce à plusieurs propriétés :

Nom de la propriété

Type

Description

org.quartz.dataSource.NAME.jndiURL

String

L'URL JNDI de la datasource configurée dans le serveur d'applications. (Obligatoire)

Valeur par défaut : null

org.quartz.dataSource.NAME.java.naming.factory.initial

String

Nom pleinement qualifié de la classe de type InitialContextFactor

Valeur par défaut : null

org.quartz.dataSource.NAME.java.naming.provider.url

String

URL pour se connecter au contexte JNDI

Valeur par défaut : null

org.quartz.dataSource.NAME.java.naming.security.principal

String

Le nom de l'utilisateur pour se connecter au contexte JNDI

Valeur par défaut : null

org.quartz.dataSource.NAME.java.naming.security.credentials

String

Le mot de passe de l'utilisateur pour se connecter au contexte JNDI

Valeur par défaut : null

 

111.2.10.5. L'utilisation du moteur en mode client/serveur

Il est possible d'utiliser Quartz en mode client/serveur : dans ce mode, le scheduler s'exécute dans une JVM distincte. Le client peut interagir avec le scheduler en utilisant de manière transparente RMI.

Cela peut être utile pour mutualiser le scheduler sur plusieurs applications et/ou pour réduire l'activité sur le client puisque l'exécution du scheduler et des tâches qu'il va déclencher se fait sur la JVM distante.

La configuration de l'utilisation en mode client/serveur utilise plusieurs propriétés de la configuration.

Nom de la propriété

Type

Description

org.quartz.scheduler.rmi.export

String

Demander au scheduler d'exporter ses fonctionnalités grâce à RMI

Valeur par défaut : false

org.quartz.scheduler.rmi.registryHost

String

Préciser le nom du host sur lequel s'exécute le registre RMI : c'est généralement la machine locale

Valeur par défaut : localhost

org.quartz.scheduler.rmi.registryPort

String

Préciser le port sur lequel le registre RMI écoute

Valeur par défaut : 1099

org.quartz.scheduler.rmi.createRegistry

String

Définir si oui et comment le registre RMI est créé. Plusieurs valeurs sont utilisables :

  • false ou never : ne jamais créé le registre
  • true ou as_needed : tentative d'utilisation d'un registre existant avant d'en créer un
  • always : toujours tenter de créer un registre et utiliser un existant si cela échoue

Valeur par défaut : never

org.quartz.scheduler.rmi.serverPort

String

Préciser le numéro du port sur lequel le Scheduler attend des connexions

Valeur par défaut : random

org.quartz.scheduler.rmi.proxy

String

Préciser que l'on souhaite se connecter à un scheduler distant. Dans ce cas, l'instance du scheduler sera un proxy permettant d'invoquer des méthodes du scheduler distant

Valeur par défaut : false


Remarque : il n'est pas cohérent de mettre les valeurs true en même temps aux propriétés org.quartz.scheduler.rmi.export et org.quartz.scheduler.rmi.proxy.

Pour lancer le scheduler, il est nécessaire de mettre en place un RMISecurityManager.

Exemple :
package fr.jmdoudoux.dej.quartz;

import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzServeur {

  public static void main(final String[] args) {
    if (System.getSecurityManager() != null) {
      System.setSecurityManager(new java.rmi.RMISecurityManager());
    }
    try {
      final SchedulerFactory schedulerFactory = new StdSchedulerFactory();
      final Scheduler scheduler = schedulerFactory.getScheduler();
      scheduler.start();
    } catch (final SchedulerException se) {
      se.printStackTrace();
    }
  }
}

Il faut configurer le scheduler pour permettre une invocation de ces fonctionnalités en utilisant RMI.

Exemple : le fichier Quartz_serveur.properties
org.quartz.scheduler.instanceName = scheduler_serveur 
org.quartz.scheduler.instanceId = scheduler1 

org.quartz.scheduler.rmi.export = true 
org.quartz.scheduler.rmi.registryHost = localhost 
org.quartz.scheduler.rmi.registryPort = 1099 
org.quartz.scheduler.rmi.createRegistry = true

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool 
org.quartz.threadPool.threadCount = 4

org.quartz.plugin.shutdownhook.class = org.quartz.plugins.management.ShutdownHookPlugin
org.quartz.plugin.shutdownhook.cleanShutdown = true

Il faut préciser le fichier de configuration en tant que paramètre de la JVM :

-Dorg.quartz.properties=src/main/resources/quartz_serveur.properties

Résultat :
INFO StdSchedulerFactory - Using default implementation for ThreadExecutor
INFO SchedulerSignalerImpl - Initialized Scheduler Signaller of type: 
class org.quartz.core.SchedulerSignalerImpl
INFO QuartzScheduler - Quartz Scheduler v.2.2.1 created.
INFO ShutdownHookPlugin - Registering Quartz shutdown hook.
INFO RAMJobStore - RAMJobStore initialized.
INFO QuartzScheduler - Scheduler bound to RMI registry under name 
'scheduler_serveur_$_scheduler1'
INFO QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.2.1) 
'scheduler_serveur' with instanceId 'scheduler1'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - access via RMI.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 4 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support
  persistence. and is not clustered.
 
INFO StdSchedulerFactory - Quartz scheduler 'scheduler_serveur' initialized
 from specified file: 'src/main/resources/quartz_serveur.properties'
INFO StdSchedulerFactory - Quartz scheduler version: 2.2.1
INFO QuartzScheduler - Scheduler scheduler_serveur_$_scheduler1 started.

Dans le mode client/serveur, l'instance du scheduler côté client est uniquement un proxy qui permet d'invoquer les fonctionnalités sur l'instance distante du scheduler.

Exemple :
package fr.jmdoudoux.dej.quartz;

import java.util.Date;

import org.quartz.DateBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzClient {

  public static void main(final String[] args) {
    try {
      final SchedulerFactory factory = new StdSchedulerFactory();
      final Scheduler scheduler = factory.getScheduler();

      final JobDetail jobDetail = JobBuilder
          .newJob(MonJob.class)
          .withIdentity("monJob")
          .storeDurably()
          .build();

      JobBuilder
          .newJob(MonJob.class)
          .withIdentity("monJobNouveau")
          .storeDurably()
          .build();

      final Trigger trigger = TriggerBuilder
          .newTrigger()
          .withIdentity(new TriggerKey("monTrigger", "monGroup"))
          .withSchedule(
              SimpleScheduleBuilder
                  .simpleSchedule()
                  .withIntervalInMinutes(1)
                  .repeatForever())
          .startAt(DateBuilder.evenMinuteDate(new Date()))
          .build();

      scheduler.scheduleJob(jobDetail, trigger);

      System.in.read();
    } catch (final Exception e) {
      e.printStackTrace();
    }
  }
}

La configuration du scheduler précise uniquement les informations utiles pour invoquer le scheduler distant grâce à RMI.

Exemple : Quartz_client.properties
org.quartz.scheduler.instanceName = scheduler_serveur 
org.quartz.scheduler.instanceId = scheduler1 
org.quartz.scheduler.rmi.proxy = true 
org.quartz.scheduler.rmi.registryHost = localhost 
org.quartz.scheduler.rmi.registryPort = 1099

Il faut préciser le fichier de configuration en tant que paramètre de la JVM

-Dorg.quartz.properties=src/main/resources/quartz_client.properties

 

111.2.10.6. La configuration des plugins

Chaque plugin doit être défini avec un nom unique qui doit suivre org.quarts.plugin dans la définition de la configuration.

Pour utiliser un plugin, il faut préciser le nom pleinement qualifié de sa classe comme valeur de la propriété org.quartz.plugin.XXX.class où XXX est le nom unique du plugin dans l'instance du scheduler.

Il est possible de fournir une valeur à un paramètre du plugin en utilisant la propriété org.quartz.plugin.XXX.nom_propriete où XXX est le nom unique du plugin et nom_propriete est le nom de la propriété qui sera valorisée par introspection.

Quartz propose en standard plusieurs plugins contenus dans le package org.quartz.plugins et ses sous-packages :

  • org.quartz.plugins.SchedulerPluginWithUserTransactionSupport
  • org.quartz.plugins.history.LoggingJobHistoryPlugin
  • org.quartz.plugins.history.LoggingTriggerHistoryPlugin
  • org.quartz.plugins.management.ShutdownHookPlugin
  • org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin

La classe abstraite SchedulerPluginWithUserTransactionSupport offre des fonctionnalités de base pour développer des plugins dont les méthodes start() et shutdown() ont en paramètre un UserTransaction. C'est utile lorsqu'un JobStoreCMT est utilisé et que le plugin interagit avec des jobs ou des triggers. Si le JobStore utilisé est de type JobStoreCMT, il faut obligatoirement que la propriété wrapInUserTransaction soit positionnée à la valeur true. Le paramètre de type UserTransaction ne doit pas être null si la propriété wrapInUserTransaction est à true. La méthode initialize() est invoquée par le Scheduler lors de l'initialisation du plugin.

Le plugin LoggingJobHistoryPlugin permet de journaliser l'exécution des jobs dans un log en utilisant la bibliothèque Apache Commons Logging.

De manière similaire, le plugin LoggingTriggerHistoryPlugin permet de journaliser l'exécution des triggers dans un log.

Il est possible de configurer le contenu journalisé selon le type de messages en utilisant la syntaxe de la classe java.util.MessageFormat. En fonction du type de message, plusieurs placeholders ({n} où n varie de 0 à 8) sont utilisables pour configurer le contenu du message. Chaque message associe un numéro à une donnée : chacune de ces valeurs est détaillée dans la Javadoc.

Résultat :
org.quartz.plugin.jobsLogging.class = org.quartz.plugins.history.LoggingTriggerHistoryPlugin
org.quartz.plugin.jobsLogging.jobSuccessMessage=Le job [{1}.{0}] 
a terminé son execution avec le code retour : {8}
org.quartz.plugin.jobsLogging.jobFailedMessage=Le job [{1}.{0}] 
a terminé son execution avec l'exception: {8}
org.quartz.plugin.jobsLogging.jobWasVetoedMessage=L'execution du job [{1}.{0}] 
a été bloquée. Il a été déclenché 
par le trigger [{4}.{3}] à {2, date, dd-MM-yyyy HH:mm:ss.SSS}

org.quartz.plugin.triggersLogging.class=org.quartz.plugins.history.LoggingTriggerHistoryPlugin
org.quartz.plugin.triggersLogging.triggerFiredMessage=Le trigger [{1}.{0}] 
déclenche le job [{6}.{5}] prévu à : 
{2, date, dd-MM-yyyy HH:mm:ss.SSS}, prochaine exécution à : 
{3, date, dd-MM-yyyy HH:mm:ss.SSS}
org.quartz.plugin.triggersLogging.triggerCompleteMessage=Le trigger [{1}.{0}] 
a terminé le déclenchement du job [{6}.{5}] avec le résultat : {9}. 
org.quartz.plugin.triggersLogging.triggerMisfiredMessage=Le trigger [{1}.{0}] 
a rate le déclenchement du job [{6}.{5}] qui aurait du survenir à 
{3, date, dd-MM-yyyy HH:mm:ss.SSS}

Le plugin ShutdownHookPlugin permet de capturer l'événement de terminaison de la JVM pour permettre un arrêt propre du scheduler.

Exemple :
org.quartz.plugin.shutdownhook.class = org.quartz.plugins.management.ShutdownHookPlugin
org.quartz.plugin.shutdownhook.cleanShutdown = true

Le plugin XMLSchedulingDataProcessorPlugin permet de lire un fichier XML contenant la définition de jobs et triggers qui seront lus et enregistrés dans le scheduler lors de son démarrage. Il permet aussi de gérer les jobs et les triggers.

Exemple :
org.quartz.plugin.initialisation.class=org.quartz.plugins.xml.XMLSchedulingDataProcessorPlugin
org.quartz.plugin.initialisation.fileNames=ma_configuration.xml
org.quartz.plugin.initialisation.failOnFileNotFound=true

Pour la version 1.8.5 de Quartz, le schéma du fichier de configuration XML peut être obtenu à l'url :

https://www.quartz-scheduler.org/xml/job_scheduling_data_1_8.xsd

A partir de la version 2.0 de Quartz, le schéma du fichier de configuration XML peut être obtenu à l'url :

https://www.quartz-scheduler.org/xml/job_scheduling_data_2_0.xsd

 

111.2.11. L'utilisation en cluster

Plusieurs instances de Scheduler peuvent être rassemblées dans un cluster qui permet :

  • une meilleure répartition de la charge d'exécution des jobs grâce au load balancing
  • une haute disponibilité grâce au fail over

Le partage des informations entre les instances du cluster se fait en utilisant un JDBCJobStore.

Chaque instance du cluster doit avoir dans sa configuration la valeur true dans sa propriété org.quartz.jobStore.isClustered. Chacun d'eux doit avoir une valeur unique pour un même cluster dans sa propriété org.quartz.scheduler.instanceId.

Toutes les horloges des machines exécutant des instances du cluster doivent être synchronisées.

 

 


[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

78 commentaires Donner une note à l´article (5)

 

Copyright (C) 1999-2022 Jean-Michel DOUDOUX. Vous pouvez copier, redistribuer et/ou modifier ce document selon les termes de la Licence de Documentation Libre GNU, Version 1.1 ou toute autre version ultérieure publiée par la Free Software Foundation; les Sections Invariantes étant constitués du chapitre Préambule, aucun Texte de Première de Couverture, et aucun Texte de Quatrième de Couverture. Une copie de la licence est incluse dans la section GNU FreeDocumentation Licence. La version la plus récente de cette licence est disponible à l'adresse : GNU Free Documentation Licence.