Configuration de Spring Task Scheduler

Le TaskScheduler de Spring permet de planifier des tâches de façon simple et pratique.

Configuration

XML

On utilise le namespace task avec l’élément <task:scheduled>. Celui-ci fait référence à une méthode d’un bean déclaré par ailleurs.

Il a 4 variantes :

  • fixed-rate permet de fixer un délai entre le début d’une exécution et le début de la suivante.

  • fixed-delay permet de fixer le délai entre la fin d’une exécution et le début de la suivante.

  • cron utilise une expression cron.

  • trigger fait référence à un bean qui implémente org.springframework.scheduling.Trigger.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="http://www.springframework.org/schema/task
                           https://www.springframework.org/schema/task/spring-task.xsd">
  <task:scheduled-tasks>
      <task:scheduled fixed-rate="10000" ref="scheduledBean" method="scheduledRate" />
      <task:scheduled fixed-delay="10000" ref="scheduledBean" method="scheduledDelay" />
      <task:scheduled cron="*/10 * * * * *" ref="scheduledBean" method="scheduledCron" />
      <task:scheduled trigger="advancedTrigger" ref="scheduledBean" method="scheduledTrigger" />
  </task:scheduled-tasks>
</beans>

Annotations

Les annotations spécifiques au scheduling peuvent activés par XML avec l’élément <task:annotation-driven/>.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="http://www.springframework.org/schema/task
                           https://www.springframework.org/schema/task/spring-task.xsd">
  <task:annotation-driven/>
</beans>

Les annotations spécifiques au scheduling peuvent activés avec l’annotation de configuration @EnableScheduling.

@Configuration
@EnableScheduling
@ComponentScan("info.jtips.spring")
public class ApplicationConfiguration {
  ...
}

Une fois cette activation faire, on peut annoter les méthodes d’un bean avec @Scheduled.

On retrouve trois des options déjà présentes en XML :

  • fixedDelay avec la possibilité de choisir l’unité de temps (par défaut, milliseconde),

  • fixedRate avec la possibilité de choisir l’unité de temps (par défaut, milliseconde),

  • cron avec la possibilité de choisir le fuseau horaire de l’expression (par défaut, celui de la JVM).

L’option trigger n’existe pas avec cette annotation.

@Component
public class ScheduledBean {

  @Scheduled(fixedDelay = 10, timeUnit = TimeUnit.SECONDS)
  private void scheduledRate() throws InterruptedException {
    ...
  }

  @Scheduled(fixedRate = 10, timeUnit = TimeUnit.SECONDS)
  private void scheduledDelay() throws InterruptedException {
    ...
  }

  @Scheduled(cron = "*/10 * * * * *", zone = "UTC")
  private void scheduledCron() throws InterruptedException {
    ...
  }
}

Programmation

Les tâches peuvent aussi être planifiée en utilisant directement les méthodes de la classe org.springframework.scheduling.TaskScheduler. On retrouve à nouveau les quatre options :

  • scheduleAtFixedRate(Runnable task, Duration delay)

  • scheduleWithFixedDelay(Runnable task, Duration delay)

  • schedule(Runnable task, Trigger trigger), où le trigger peut être une instance de CronTrigger.

Il est aussi possible de planifier une tâche à exécution unique avec schedule(Runnable task, Instant startTime).

Ce mode de fonctionnement a comme prérequis l’existence d’un bean de type TaskScheduler, qui était optionnel en XML et avec les annotations.

@Component
public class ScheduledDynamicBean {

  private final TaskScheduler taskScheduler;

  public ScheduledDynamicBean(TaskScheduler taskScheduler) {
    this.taskScheduler = taskScheduler;
  }

  @PostConstruct
  private void init() {
    taskScheduler.scheduleWithFixedDelay(
        () -> ...,
        Duration.of(10, SECONDS)
    );

    taskScheduler.scheduleAtFixedRate(
        () -> ...,
        Duration.of(10, SECONDS)
    );

    taskScheduler.schedule(
        () -> ...,
        new CronTrigger("*/10 * * * * *")
    );

    taskScheduler.schedule(
        () -> ...,
        triggerContext -> nextCustomDate()
    );
  }

}

Transactions

Quel que soit le mode de planification des tâches, la gestion des transactions doit être indépendante. Par exemple, les méthodes annotées par @Scheduled ne peuvent pas être annotées en plus par @Transactional, ça provoque une TransactionRequiredException comme si la méthode n’était pas annotée.

javax.persistence.TransactionRequiredException:
          No EntityManager with actual transaction available for current thread

La solution est d’appeler une méthode transactionnelle sur un bean de service distinct du bean qui supporte les tâches planifiées.

Mécanique interne

Dans ce dernier exemple, on a vu le besoin éventuel d’un bean de type TaskScheduler. Cette interface a des méthodes de planification, mais ses implémentations ont aussi la responsabilité de la gestion des threads.

Sans TaskScheduler

L’exécution des tâches est gérée par le ScheduledTaskRegistrar.

S’il trouve un bean de type TaskScheduler, il l’utilise. Sinon il crée son propre scheduler, mono-thread, mais ne le publie pas sous forme de bean.

  // ScheduledTaskRegistrar
  this.localExecutor = Executors.newSingleThreadScheduledExecutor();
  this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);

Par conséquent, sans TaskScheduler les tâches sont exécutées de façon séquentielle dans un seul thread. Ça peut poser problème si des tâches ont un délai d’exécution long.

Avec TaskScheduler

Lorsqu’il y a un bean de type TaskScheduler, les tâches planifiées par annotation l’utilisent automatiquement. On peut éventuellement ajouter une référence explicite pour résoudre d’éventuels conflits.

C’est facile à faire en XML.

  <task:annotation-driven scheduler="taskScheduler"/>

C’est un peu plus complexe en Java.

@Configuration
@EnableScheduling
public class ApplicationConfiguration implements SchedulingConfigurer {

  @Bean
  public TaskScheduler taskScheduler() {
    //...
    return poolTaskScheduler;
  }

  @Override
  public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    taskRegistrar.setTaskScheduler(taskScheduler());
  }
}

Les tâches déclarées en XML dans l’élément <task:scheduled-tasks> n’utilisent pas le task scheduler par défaut. Il faut y faire référence explicitement.

  <task:scheduled-tasks scheduler="taskScheduler">
    <task:scheduled .../>
  </task:scheduled-tasks>

Implémentations de TaskScheduler

En XML, l’élément <task:scheduler> crée une instance de ThreadPoolTaskScheduler. Cette classe utilise un ScheduledThreadPoolExecutor de l’API concurrent du JDK, avec la possibilité de choisir la taille du pool de threads.

  <task:scheduler id="taskScheduler" pool-size="5"/>

Avec la configuration Java, il n’y a pas d’implémentation par défaut, il faut explicitement instancier l’objet.

  @Bean
  public TaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler poolTaskScheduler = new ThreadPoolTaskScheduler();
    poolTaskScheduler.setPoolSize(5);
    return poolTaskScheduler;
  }

Nous avons le choix avec une autre implémentation, ConcurrentTaskScheduler qui utilise pool de threads via un ScheduledExecutorService de l’API concurrent du JDK, qu’on passe explicitement ou qu’il détecte automatiquement. Ça permet d’avoir un pool de threads au comportement plus élaboré.

En environnement Java EE / Jakarta EE, on choisira la troisième implementation, DefaultManagedTaskScheduler qui hérite de ConcurrentTaskScheduler et qui utilise un executor service récupéré via JNDI.

Avec Spring Boot

Spring Boot crée automatiquement un bean de type TaskScheduler, avec un pool d’un seul thread.

@SpringBootApplication
@EnableScheduling
public class SpringBootExampleApplication {

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

}

La configuration du bean TaskScheduler se fait par des propriété spring.task.scheduling.* dans le fichier application.properties ou application.yml.

# application.properties
spring.task.scheduling.pool.size=5
spring.task.scheduling.thread-name-prefix=app-task-

Pour personnaliser plus profondément le bean, on peut le créer soi-même, ou implémenter SchedulingConfigurer, comme dans un environnement sans boot. On peut aussi créer un bean de type TaskSchedulerBuilder ou un bean de type TaskSchedulerCustomizer.

  @Bean
  public TaskSchedulerCustomizer taskSchedulerCustomizer() {
    return taskScheduler -> taskScheduler.setPoolSize(
                                Runtime.getRuntime().availableProcessors());
  }

Solutions alternatives

Le TaskScheduler de Spring est simple et pratique, mais il a quelques lacunes. Voici quelques fonctionnalités absentes et les solutions pour les avoirs dans Spring.

Intégration de Quartz

  • Persistance des tâches

  • Reprise sur incident

  • Séparation lecture / traitement / écriture

  • Regroupement transactionnel

  • Traitement séquentiel ou parallèle