CronJobs mit Spring

Mit Spring können zeitgesteuerte Aufgaben in Java Code integriert werden. CronJobs wie wir sie in Linux kennen, definieren wir mit Spring einfach per Annotation. In diesem Artikel zeige ich wie das geht und wie Spring die CronJobs entsprechend unserer Definition ausführt.


Was ist ein CronJob?

Unter CronJob verstehen wir die zeitlich gesteuerte Ausführung eines Kommandos zur Erledigung einer Aufgabe bzw. eines Jobs. Das Kommando wird durch einen bestimmten Zeitpunkt oder eine zeitliche Bedingung angestoßen.

Typische Beispiele für durch CronJobs gestartete Aufgaben sind:

  • Regelmäßiges Aufräumen der Datenbank - z. B. um veraltete Daten zu löschen oder DSGVO konform persönliche Daten nach einer definierten Zeit zu löschen.
  • Wöchentlicher Versand von Newslettern oder Werbung per Email
  • Nächtliche Datenbank-Backups
  • Monatliches Erstellen von Rechnungen (z.B. Telefon-Rechnung)
Das Betriebssystem Linux bietet crontab zum Erstellen von CronJobs an. Mit crontab könnten wir eine Spring Anwendung starten, die täglich neue Angebots-Daten veröffentlicht. Spring bietet uns Spring die Möglichkeit, das ganze ohne Linux und crontab zu tun. Eine ausführliche Erklärung zu CronJobs und ihre Verwendung mit Linux findet ihr hier:

Englisches Video zum Artikel

CronJobs mit Spring

Das Spring Framework bietet Entwickler eine Scheduling-Funktionalität zum Erstellen von CronJobs. Scheduling ist Teil der Spring Kern-Funktionalitäten, daher werden keine weiteren Build-Dependencies bzw. Bibliotheken benötigt. Einen einzelnen CronJob legen wir einfach in einer einzigen Klasse an und konfigurieren per Annotation:

@EnableScheduling
@Configuration
public class ScheduledJobs {
    Logger log = Logger.getLogger("ScheduledJobs");

    @Scheduled(cron = "1 * * * * *")
    private void logEvery1stSecond() {
        log.log(Level.INFO, 
            "CronJob executed every 1st second" + 
            "of any minute at any day");
    }
}

  • @EnableScheduling aktiviert die Spring Scheduling Fähigkeit für die komplette Spring Anwendung. Sie sollte zusammen mit der @Configuration Annotation gesetzt werden. Sie muss nicht zusammen mit den CronJob-Methoden in derselben Klasse sein.
  • @Scheduled markiert eine Methode, so dass sie vom Spring Scheduler entsprechend ihrer Konfiguration ausgeführt wird. Hier verwende ich eine einfache CronJob-Konfiguration, welche die Methode in der ersten Sekunden jeder beliebigen Minute ausführt.
    cron = "1 * * * * *" definiert durch die Sterne (*), dass Minute, Stunde, Tag, Monat und Wochentag beliebig sind.

Das cron-Attribut

Das cron-Attribute der Scheduled Annotation ist ähnlich dem Linux crontab aufgebaut. In Spring hat es 6 Leerzeichen-separierte Werte und somit dieses Format:
<Sekunde> <Minute> <Stunde> <Tag des Monats> <Monat> <Tag der Woche>
Weitere Beispiele für das cron-Attribut:

  • @Scheduled(cron = "1-10 */2 * * * Mon-Fri")
    • 1-10 legt fest, dass die annotierte Methode in den Sekunden 1 bis 10 aufgerufen wird. Das Minuszeichen definiert eine Spanne und legt hier fest, dass die Methode nur an den Werktagen Montag bis Freitag in einer Woche ausgeführt wird.
    • */2 legt fest, dass der CronJob nur jede 2. Minute ausgeführt wird. Würde man statt dem Stern eine Zahl festlegen, z.B. 5/2 so würde der Job ab der 5. Minute jede 2 Minute ausgeführt.
  • @Scheduled(cron = "0 0 0 25 12 ?", zone = "Europe/Berlin")
    • Dieser CronJob wird einmal im Jahr am ersten Weihnachtstag, dem 25.12. um 0:00 Uhr ausgeführt.
    • Mit dem zone Attribut kann man noch die Zeitzone festlegen, also hier die deutsche Zeitzone.
    • Der Wochentag kann durch ein Fragezeichen als irrelevant markiert werden.
  • @Scheduled(cron = "11,22,33,44,55 * * * * *")
    • Die Komma-separierte Liste definiert in welchen Sekunden dieser CronJob ausgeführt wird.
  • @Scheduled(cron = "1-10,15,30,45 * * * * ?")
    • Kombinationen sind auch möglich.

Relative Abstände zwischen CronJobs

Statt mit genauen Zeitangaben (cron-Attribut) können mit @Schedule annotierte Methoden auch mit relativen Zeitspannen zwischen den einzelnen Ausführungen gestartet werden:

    @Autowired IdGenerator idGenerator;
    @Scheduled(fixedDelay = 1000, initialDelay = 5000)
    private void startJobAfterCompletionOfPreviousSchedule() {
String jobId = idGenerator.generateId();
log.log(Level.INFO, "Job completed: " + jobId));
    }
  • Wenn CronJobs mit Spring definiert und ausgeführt werden, können sie auf den Spring IoC Container und somit alle Beans zugreifen. Hier wurde eine IdGenerator Bean mit @Autowired injiziert. Weitere Infos zu Beans und Dependency Injektion findet ihr hier: kernkonzepte-von-spring.html
  • initialDelay legt fest wie viele Millisekunden nach Start der Spring Anwendung der CronJob zum ersten Mal startet.
  • fixedDelay definiert wie viele Millisekunden gewartet wird, bevor der CronJob erneut nach Ende der vorherigen CronJobs-Ausführung startet. Im Code-Beispiel oben ist zwischen den Ausführungen des CronJobs immer 1 Sekunde bzw. 1000 Millisekunden Pause.
  • fixedRate zeige ich im Zusammenhang mit asynchroner Ausführung von CronJobs. Das Attribut fixedRate legt fest wie viele Millisekunden Abstand zwischen dem Start zweier CronJobs ist.

Asynchrone Ausführung von CronJobs

@Scheduled annotierte Methoden werden grundsätzlich im scheduling-Thread ausgeführt. In meinem Beispiel-Code verwende ich immer eine Logger-Instanz, da diese den Namen des Threads loggt. Das verdeutlicht den Unterschied zu asynchron ausgeführten CronJobs. Startet unsere Anwendung viele zeitlich überlappende CronJobs, kann es besser bzw. performanter sein die CronJobs asynchron auszuführen. Ohne zeitliche Überlappung ist die asynchrone Ausführung der CronJobs nicht notwendig, da jede CronJob-Ausführung im scheduling-Thread unabhängig vom main-Thread und der restlichen Anwendung stattfindet.

@EnableAsync
@EnableScheduling
@Configuration
public class AsyncScheduledJobs {
    Logger log = Logger.getLogger("AsyncScheduledJobs");
    
    @Scheduled(cron = "${scheduled.special_seconds}")
    @Async
    void logAtSpecialSeconds() {
     log.log(Level.INFO, "Job async executed");
    }
    @Scheduled(fixedRateString = "${scheduled.fixed-rate}",
            initialDelayString = "${scheduled.initial-delay}"
)
    @Async
    void startJobOften() {
log.log(Level.INFO, "Job async executed");
    }
}
  • Um CronJobs asynchron durch den Spring Scheduler auszuführen, muss zur @EnableScheduling Annotation auch die @EnableAsync in einer Konfiguration-Klasse gesetzt werden.
  • Jede @Scheduled annotierte Methode, die asynchron zu anderen CronJobs ausgeführt werden soll, benötigt die @Async Annotation. Danach startet sie der Spring Scheduler in einem separaten Thread.
  • Die String-Attribute (inklusive cron-Attribut) der @Scheduled Annotation, können aus den Spring Properties Dateien gelesen werden. Dazu schreibt man den Properties-Schlüssel in ${...}. Spring geht alle Property Definitionen durch, um den Wert passend zum Schlüssel zu finden. Meine application.properties Datei zu diesem Beispiel sieht so aus:
scheduled.special_seconds=11,22,33,44,55 * * * * ?
scheduled.fixed-rate=2500
scheduled.initial-delay=4000

Anmerkung: Definiert ihr mehrere CronJobs mit zeitlicher Überlappung, starten einige Ausführungen möglicherweise nicht, weil der scheduling-Thread gerade ausgelastet ist. Die asynchrone Ausführung kann dies verhindern. Startet meinen JUnit-Test ScheduledJobsTest um zu sehen, wie einige Ausführungen übersprungen werden. Die Ursache dessen ist, dass der scheduling-Thread durch die Methode startJobAfterCompletionOfPreviousSchedule phasenweise blockiert ist. 

Fazit

Die Scheduling Fähigkeit von Spring ist sehr gut ins Spring Framework integriert und einfach zu benutzen. Falls ihr Linux CronJobs kennt, versteht ihr alles schnell. 

In der Praxis verwende ich die Scheduling Fähigkeit von Spring, um alte Daten aus der Datenbank zu löschen. Dazu verwende ich einfach die vorhandene Beans, die JPA Entitäten und die Datenbank Connection innerhalb des CronJobs.

Den kompletten Code findet ihr in GitHub:
https://github.com/elmar-brauch/beans/tree/master/src/main/java/de/bsi/bean/schedule

Kommentare

Beliebte Posts aus diesem Blog

OpenID Connect mit Spring Boot 3

Authentifizierung in Web-Anwendungen mit Spring Security 6

Reaktive REST-Webservices mit Spring WebFlux