Cron : planifier des tâches avec Spring Boot et Linux

Publié le · Mis à jour le

L'idée en une phrase

Une expression cron décrit quand une tâche doit se lancer automatiquement : « tous les jours à 2 h », « toutes les 15 minutes », « le premier du mois ». On la retrouve dans Spring Boot (@Scheduled), sur les serveurs Linux (la crontab), dans les pipelines planifiés de GitLab ou chez les hébergeurs. Le principe est le même partout, mais la syntaxe de Spring a un champ de plus, et c'est le premier piège.

Une image pour comprendre : le réveil programmé

Un réveil se règle avec une heure et des jours : « 7 h, du lundi au vendredi ». Il ne sonne pas « dans 8 heures » : il sonne à chaque fois que l'heure et le jour correspondent.

Une expression cron, c'est un réglage de réveil un peu plus riche : on y précise les minutes, les heures, les jours du mois, les mois et les jours de la semaine. À chaque minute, le système regarde l'horloge et se demande : « est-ce que l'heure actuelle correspond au réglage ? ». Si oui, la tâche se lance.

La syntaxe Unix : 5 champs

C'est la syntaxe d'origine, utilisée par les serveurs Linux, GitLab et la plupart des hébergeurs :

┌───────────── minute        (0 - 59)
│ ┌─────────── heure         (0 - 23)
│ │ ┌───────── jour du mois  (1 - 31)
│ │ │ ┌─────── mois          (1 - 12)
│ │ │ │ ┌───── jour semaine  (0 - 7, 0 et 7 = dimanche)
│ │ │ │ │
* * * * *

Les caractères spéciaux

Caractère Signification Exemple Se lit
* toutes les valeurs * dans les heures chaque heure
, une liste 1,15 dans les jours le 1er et le 15
- un intervalle 1-5 dans les jours de la semaine du lundi au vendredi
/ un pas */15 dans les minutes toutes les 15 minutes

Des exemples à lire

Expression Se lit
0 2 * * * tous les jours à 2 h 00
*/15 * * * * toutes les 15 minutes
0 8 * * 1-5 à 8 h, du lundi au vendredi
30 6 1 * * le 1er de chaque mois à 6 h 30
0 0 * * 0 chaque dimanche à minuit
0 9-18 * * 1-5 toutes les heures pleines de 9 h à 18 h, en semaine

Astuce : pour vérifier une expression Unix, le site crontab.guru la traduit en phrase et affiche les prochaines exécutions. Le réflexe à avoir avant chaque mise en production.

Le piège : * dans les minutes

* 2 * * *

On voulait « tous les jours à 2 h ». Cette expression signifie « chaque minute entre 2 h 00 et 2 h 59 » : 60 exécutions au lieu d'une. Pour une heure précise, la minute doit être précisée : 0 2 * * *.

La syntaxe Spring : 6 champs

Spring utilise une syntaxe proche, mais avec un champ de plus au début : les secondes.

┌─────────────── seconde       (0 - 59)
│ ┌───────────── minute        (0 - 59)
│ │ ┌─────────── heure         (0 - 23)
│ │ │ ┌───────── jour du mois  (1 - 31)
│ │ │ │ ┌─────── mois          (1 - 12 ou JAN - DEC)
│ │ │ │ │ ┌───── jour semaine  (0 - 7 ou MON - SUN)
│ │ │ │ │ │
* * * * * *
Besoin Unix (5 champs) Spring (6 champs)
tous les jours à 2 h 0 2 * * * 0 0 2 * * *
toutes les 15 minutes */15 * * * * 0 */15 * * * *
à 8 h en semaine 0 8 * * 1-5 0 0 8 * * MON-FRI
le 1er du mois à 6 h 30 30 6 1 * * 0 30 6 1 * *

⚠️ Une expression Unix copiée telle quelle dans Spring ne fait pas ce que tu crois. Avec 5 champs, Spring refuse de démarrer, ce qui est le cas favorable. Mais en ajoutant un champ au mauvais endroit, on obtient une expression valide… et fausse. Vérifie toujours que le premier champ est la seconde.

Spring accepte aussi les noms de jours et de mois (MON-FRI, JAN), plus lisibles que des chiffres, et quelques raccourcis :

Raccourci Équivalent Spring
@hourly 0 0 * * * *
@daily (ou @midnight) 0 0 0 * * *
@weekly 0 0 0 * * SUN
@monthly 0 0 0 1 * *

Planifier une tâche avec Spring Boot

Activer la planification

@Configuration
@EnableScheduling
public class PlanificationConfig {
}

@Scheduled

@Component
public class PurgeTaches {

    private final SessionService sessionService;

    public PurgeTaches(SessionService sessionService) {
        this.sessionService = sessionService;
    }

    @Scheduled(cron = "0 0 3 * * *", zone = "Europe/Paris") // tous les jours à 3 h, heure de Paris
    public void purgerSessionsExpirees() {
        sessionService.purgerExpirees();
    }
}

La méthode ne prend aucun paramètre et ne renvoie rien. Elle se contente d'appeler un service : la logique reste dans le service, où elle peut être testée et réutilisée.

cron, fixedRate ou fixedDelay ?

Pour une tâche à intervalle régulier plutôt qu'à une heure précise, @Scheduled propose deux autres options :

Option Signification Exemple
cron = "0 0 3 * * *" à des heures précises du calendrier une purge chaque nuit à 3 h
fixedRate = 60_000 toutes les 60 secondes, mesurées entre deux débuts une vérification régulière
fixedDelay = 60_000 60 secondes après la fin de l'exécution précédente une tâche de durée variable, qui ne doit pas s'enchaîner sans pause
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.MINUTES) // 5 minutes après la fin de la précédente
public void synchroniserCatalogue() { ... }

Règle simple : une heure précise → cron ; un intervalle → fixedDelay, qui ne surcharge jamais si la tâche prend plus de temps que prévu.

Rendre l'horaire configurable

Un horaire écrit en dur dans le code oblige à recompiler pour le changer. Mets-le dans application.properties :

@Scheduled(cron = "${purge.sessions.cron}", zone = "Europe/Paris")
public void purgerSessionsExpirees() { ... }
purge.sessions.cron=0 0 3 * * *

L'horaire peut alors être différent selon l'environnement (plus fréquent en recette, par exemple). Et la valeur spéciale - désactive la tâche :

# application-local.properties
purge.sessions.cron=-

Le fuseau horaire

Une expression cron ne contient pas de fuseau horaire. Elle est interprétée dans le fuseau de la machine qui l'exécute. Or les serveurs, et surtout les conteneurs Docker, tournent très souvent en UTC.

@Scheduled(cron = "0 0 8 * * *") // ⚠️ 8 h… mais 8 h UTC, soit 9 h ou 10 h à Paris selon la saison

Avec Spring, précise toujours zone :

@Scheduled(cron = "0 0 8 * * *", zone = "Europe/Paris") // ✅ 8 h à Paris, été comme hiver

Le changement d'heure

En France, l'heure change deux fois par an, entre 2 h et 3 h du matin :

  • en mars, on passe de 2 h à 3 h : 2 h 30 n'existe pas ce jour-là ;
  • en octobre, on revient de 3 h à 2 h : 2 h 30 existe deux fois.

Une tâche planifiée à 2 h 30 heure de Paris peut donc sauter une exécution, ou poser problème si elle s'exécute deux fois. Évite de planifier entre 2 h et 3 h du matin à l'heure de Paris. 1 h ou 4 h ne posent aucun problème.

Ce qui se passe quand une tâche échoue

Si la méthode lance une exception, Spring l'écrit dans les logs… et c'est tout. La tâche sera relancée à l'heure suivante, mais personne n'est prévenu. Une purge qui échoue toutes les nuits pendant trois semaines, sans que personne ne s'en rende compte, c'est un scénario classique.

Pour une tâche importante :

  • écris dans les logs le début, la fin et le résultat (« 1 254 sessions purgées en 3,2 s ») ;
  • surveille-la : une métrique ou une alerte quand elle échoue, ou quand elle ne s'est pas exécutée (voir la fiche sur les métriques) ;
  • rends-la rejouable : si elle échoue à mi-parcours et qu'on la relance, elle doit reprendre sans créer de doublons.

Le dernier point est le plus important. Une tâche qui traite « les factures d'hier » ne peut pas être rejouée le lendemain. Une tâche qui traite « les factures pas encore envoyées » peut être relancée à tout moment, autant de fois que nécessaire :

// ❌ dépend de la date : impossible à rejouer le lendemain
List<Facture> factures = factureRepository.findByDate(LocalDate.now().minusDays(1));

// ✅ dépend de l'état : rejouable sans risque
List<Facture> factures = factureRepository.findByStatut(StatutFacture.A_ENVOYER);

Note : pour un traitement par lot, pense à la fiche sur @Transactional : une transaction par élément, pour qu'un échec n'annule pas tout le lot.

Les pièges de @Scheduled

Un seul thread par défaut

Par défaut, Spring Boot exécute toutes les tâches planifiées avec un seul thread. Si une tâche dure 20 minutes, toutes les autres attendent qu'elle soit terminée :

3 h 00 : export (dure 20 minutes)
3 h 05 : purge prévue… lancée à 3 h 20 seulement

Si plusieurs tâches tournent aux mêmes moments, augmente le nombre de threads :

spring.task.scheduling.pool.size=4

L'avantage de ce thread unique : une tâche ne se chevauche jamais elle-même. Si une exécution n'est pas finie quand la suivante devrait commencer, la suivante attend.

Plusieurs instances de l'application

C'est le piège le plus sérieux. Si l'application tourne sur trois serveurs, chacun a sa propre planification : la tâche s'exécute trois fois.

3 h 00 : serveur 1 → envoie les relances par e-mail
3 h 00 : serveur 2 → envoie les relances par e-mail
3 h 00 : serveur 3 → envoie les relances par e-mail
→ chaque client reçoit trois e-mails

Les solutions habituelles :

  • un verrou partagé, avec la bibliothèque ShedLock : avant de s'exécuter, chaque instance tente de prendre un verrou dans la base de données, et une seule y parvient ;
  • une instance dédiée aux tâches planifiées, les autres ayant les tâches désactivées (avec la valeur - vue plus haut) ;
  • un planificateur externe (Kubernetes, l'outil d'ordonnancement de l'entreprise) qui déclenche la tâche une seule fois.
@Scheduled(cron = "0 0 3 * * *", zone = "Europe/Paris")
@SchedulerLock(name = "envoi-relances", lockAtMostFor = "30m")
public void envoyerRelances() { ... } // une seule instance l'exécute

Ce choix dépend de l'architecture du projet : renseigne-toi sur ce qui est en place avant d'ajouter une tâche planifiée.

@Scheduled passe par Spring

La méthode doit être dans un bean (@Component, @Service) : sur un objet créé avec new, rien ne se passe (voir la fiche sur l'injection de dépendances).

Sur un serveur Linux : la crontab

Hors de Spring, sur un serveur Linux ou chez un hébergeur, les tâches planifiées sont décrites dans la crontab, avec la syntaxe Unix à 5 champs.

crontab -l   # afficher les tâches planifiées de l'utilisateur
crontab -e   # les modifier (ouvre un éditeur)

Une ligne de crontab, c'est l'expression suivie de la commande :

0 3 * * * /home/app/scripts/sauvegarde.sh >> /home/app/logs/sauvegarde.log 2>&1

Trois pièges spécifiques :

  • des chemins complets : la tâche ne s'exécute pas depuis ton dossier personnel, et presque aucune variable d'environnement n'est définie. sauvegarde.sh seul ne sera pas trouvé ;
  • rediriger la sortie : sans >> fichier.log 2>&1, les messages et les erreurs de la commande disparaissent. Impossible de savoir ce qui s'est passé ;
  • le fuseau du serveur : il n'y a pas d'option zone. Vérifie le fuseau avec la commande date.

Note : les panneaux d'hébergement (cPanel, par exemple) et les pipelines planifiés de GitLab utilisent aussi la syntaxe à 5 champs. GitLab permet en plus de choisir le fuseau horaire.

Les bonnes habitudes

  1. Spring : 6 champs, la seconde d'abord. Unix, GitLab, hébergeurs : 5 champs.
  2. Vérifie chaque expression avant de la mettre en production, et pense au * dans les minutes.
  3. Précise toujours le fuseau avec zone = "Europe/Paris".
  4. Évite le créneau 2 h - 3 h à cause du changement d'heure.
  5. L'horaire dans application.properties, pour le modifier sans recompiler, et - pour désactiver.
  6. La méthode planifiée appelle un service, qui contient la logique.
  7. Une tâche rejouable : elle traite un état (« à envoyer »), pas une date (« hier »).
  8. Logs et surveillance : une tâche planifiée qui échoue ne prévient personne.
  9. Plusieurs instances = plusieurs exécutions : vérifie comment le projet gère ce cas avant d'ajouter une tâche.
  10. Crontab Linux : chemins complets et sortie redirigée vers un fichier de log.

À retenir

  • Une expression cron décrit quand lancer une tâche, comme le réglage d'un réveil.
  • Syntaxe Unix : 5 champs (minute, heure, jour, mois, jour de la semaine). Syntaxe Spring : 6 champs, avec les secondes en premier.
  • @Scheduled lance une méthode selon une expression cron, ou à intervalle avec fixedDelay ; il faut @EnableScheduling.
  • Sans zone, l'expression suit le fuseau du serveur, souvent UTC.
  • Par défaut, Spring exécute les tâches planifiées sur un seul thread, et chaque instance de l'application les exécute.
  • Une tâche planifiée doit être surveillée et rejouable.

Avis sur l'article

Pas encore de note : soyez le premier à donner votre avis.

Les avis sont publiés après validation par le modérateur.

Votre note