Spring From the Trenches: Using Environment Specific Cron Expressions With the @Scheduled Annotation

The @Scheduled annotation offers an easy way to create scheduled tasks in Spring powered applications. We can use it to schedule our tasks by using either periodic scheduling or cron expressions.

Although period scheduling can also be useful, the cron expressions give us much more control over the invocation of the scheduled tasks. That is why they are very useful in real life applications.

However, using cron expressions has one major drawback if it is not done right.

Let's find out what that is.

Creating a Scheduled Task

Let's assume that we want to create a task which is invoked once in a second and which simply writes a message to the log.

We can create this task by following these steps (We will skip the required configuration since it is described in the second part of this post):

  1. Create a class called ScheduledJob.
  2. Annotate the class with the @Component annotation.
  3. Create a private Logger field and instantiate the created field.
  4. Create a public method called run() and ensure that its return type is void.
  5. Annotate the method with the @Scheduled annotation and set the used cron expression as the value of the cron attribute (Cron Scheduler in Spring provides a nice overview about cron expressions).
  6. Implement the method by writing a single message to the log.

The source code of the ScheduledJob class looks as follows:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ScheduledJob {

    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledJob.class);

    @Scheduled(cron = "0-59 * * * * *")
    public void run() {
        LOGGER.debug("run()");
    }
}

The problem of our implementation is that the cron expression is hardcoded. This means that it is not possible to use different configurations in different environments.

If we want to use different scheduling configuration in different environments, we have to change the configuration manually before we create the deployed binary.

This is naturally error prone. Since the consequences of using wrong scheduling configuration can be severe, we have to find a way to move our cron expressions from the code to the configuration files of our project.

Moving Cron Expressions to a Properties File

When I was looking for a solution to our problem, I ran into this thread. The solution described in this blog post is based on that discussion.

The requirements of our solution are following:

  • It must have different configurations for production and development environment.
  • When the scheduled task is run in the development environment, it must be invoked once in a second.
  • When the scheduled task is run in the production environment, it must be invoked once in a minute.

We can fulfill these requirements by following these steps:

  1. Configure Maven.
  2. Create the properties files.
  3. Configure the application context.
  4. Modify the task class.

Let's get started.

Configuring Maven

We can configure Maven by following these steps:

  1. Create profiles for both development and production environment.
  2. Configure resource filtering.

Let's move on and find out how this is done.

Creating Profiles for Development and Production Environment

As we remember, we have to create Maven profiles for both development and production environment.

We can create the profile used in the development environment by following these steps:

  1. Add a new profile to the profiles section of the POM file.
  2. Set the id of the created profile to 'dev'.
  3. Ensure that the development profile is active by default.
  4. Create a property called build.profile.id and set its value to 'dev'.

We can create the production profile by following these steps:

  1. Add a new profile to the profiles section of the POM file.
  2. Set the id of the created profile to 'prod'.
  3. Create a property called build.profile.id and set its value to 'prod'.

The profiles section of our pom.xml file looks as follows:

<profiles>
	<profile>
		<id>dev</id>
		<activation>
			<activeByDefault>true</activeByDefault>
		</activation>
		<properties>
			<build.profile.id>dev</build.profile.id>
		</properties>
	</profile>
	<profile>
		<id>prod</id>
		<properties>
			<build.profile.id>prod</build.profile.id>
		</properties>
	</profile>
</profiles>

We will use the build.profile.id property when we are configuring the resource filtering of our build. Let's see how this is done.

Configuring the Resource Filtering

We can configure the resource filtering by following these steps:

  1. Configure the location of the configuration file which contains the profile specific properties (The value of the build.profile.id property identifies the used profile).
  2. Configure the location of the resource directory and activate the resource filtering.

The relevant part of our pom.xml file looks as follows:

<filters>
    <filter>profiles/${build.profile.id}/config.properties</filter>
</filters>
<resources>
    <resource>
        <filtering>true</filtering>
        <directory>src/main/resources</directory>
    </resource>
</resources>

Creating the Properties Files

We can create the required properties files by following these steps:

  1. We have to create a properties file for the development environment.
  2. We have to create a properties file for the production environment.
  3. We have to create a properties file which is read by our application.

Let's get started.

Creating the Properties File for the Development Environment

We can create the properties file for the development environment by following these steps:

  1. Create a file called config.properties to the profiles/dev directory.
  2. Set the value of the scheduling.job.cron property to '0-59 * * * * *'. This ensures that the task is invoked once in a second.

The content of the profiles/dev/config.properties file looks as follows:

scheduling.job.cron=0-59 * * * * *

Creating the Properties File for the Production Environment

We can create the properties file for the production environment by following these steps:

  1. Create a file called config.properties to the profiles/prod directory.
  2. Set the value of the scheduling.job.cron property to '0 0-59 * * * *'. This ensures that the task is invoked once in a minute.

The content of the profiles/prod/config.properties file looks as follows:

scheduling.job.cron=0 0-59 * * * *

Creating the Properties File of Our Application

We can create the properties file of our application by following these steps:

  1. Create a file called application.properties to the src/main/resources directory.
  2. Set the value of the scheduling.job.cron property to '${scheduling.job.cron}'. This ensures that the placeholder is replaced with the correct cron expression.

The content of the src/main/resources/application.properties file looks as follows:

scheduling.job.cron=${scheduling.job.cron}

Configuring the Application Context

We can configure the application context of our application by using either a Java configuration class or an XML configuration file.

Both of these options are described in the following.

Java Configuration

We can create the application context configuration class by following these steps:

  1. Create a class called ExampleApplicationContext.
  2. Annotate the class with the @Configuration annotation.
  3. Enable scheduling by annotating the class with the @EnableScheduling annotation.
  4. Annotate the class with the @ComponentScan annotation and configure the scanned packages.
  5. Annotate the class with the @PropertySource annotation and ensure that the properties are loaded from a properties file called application.properties which is found from the classpath.
  6. Create a new PropertySourcesPlaceHolderConfigurer bean.

The source code of our application context configuration class looks as follows:

import org.springframework.context.annotation.*;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
@ComponentScan(basePackages = {
        "net.petrikainulainen.spring.trenches.scheduling"
})
@PropertySource("classpath:application.properties")
public class ExampleApplicationContext {

    @Bean
    public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        PropertySourcesPlaceholderConfigurer properties = new PropertySourcesPlaceholderConfigurer();

        properties.setLocation(new ClassPathResource( "application.properties" ));
        properties.setIgnoreResourceNotFound(false);

        return properties;
    }
}

XML Configuration

We can create the application context configuration file by following these steps:

  1. Use the property-placeholder element of the context namespace for loading the properties from the properties file called application.properties which is found from the classpath.
  2. Use the annotation-config element of the context namespace for ensuring that the "general" annotations are detected from our bean classes.
  3. Use the component-scan element of the context namespace for configuring the scanned packages.
  4. Enable scheduling by using the annotation-driven element of the task namespace.

The source code of our application context configuration file looks as follows:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
       http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.1.xsd">

    <context:property-placeholder location="classpath:application.properties" ignore-resource-not-found="false"/>

    <context:annotation-config/>
    <context:component-scan base-package="net.petrikainulainen.spring.trenches.scheduling"/>
    <task:annotation-driven/>
</beans>

Modifying the Scheduled Task

Our last step is to modify our task class and ensure that the used cron expression is read from the application.properties file. We can do this by setting the value of the cron attribute of the @Scheduled annotation to '${scheduling.job.cron}'.

The source code of the ScheduledJob class looks as follows:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ScheduledJob {

    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledJob.class);

    @Scheduled(cron = "${scheduling.job.cron}")
    public void run() {
        LOGGER.debug("run()");
    }
}

Summary

We have now created a scheduled task which reads the used cron expression from a properties file. This blog post has taught us three things:

  • We learned that hard coding the used cron expression makes it hard to use different configuration in different environments.
  • We learned how we can use Maven for separating the profile specific configuration properties into profile specific configuration files.
  • We learned to configure the application context of our application and read the used cron expression from a properties file.

As always, the example application of this blog post is available at Github.

Micho pointed out that if you use Spring Framework 3.2 or newer, you can also use the fixedDelayString and fixedRateString attributes. This means that you can use the approach described in this blog even if you don't want to use cron expressions.

See the API documentation of Spring Framework for more details about this:

25 comments… add one
  • Fran Serrano Jun 21, 2013 @ 10:01

    Very interesting point!

    This way, apart from being able to use different scheduling configurations depending on environment you are able (although not desirable) to change application behaviour just by modifying the properties file and restarting the application.

    Would it be possible to do it with fixedRate and fixedDelay instead of cron, as these are not Strings?

    Thank you very much for spotting this.

    Fran

  • MIcho Mar 13, 2014 @ 6:33

    Very interesting article and very helpful.

    Regarding your comment about only being possible to use it with the cron attribute, I have to say it's also valid for the fixedRate and fixedDelay attributes. The only difference is you need to use fixedRateString and fixedDelayString instead.

    Micho

    • Petri Mar 14, 2014 @ 10:17

      Hi Micho,

      Thank you for your kind words. I really appreciate them. Also, thank you for letting me know about the fixedRateString and fixedDelayString attributes. I will update the blog post and give kudos to you :)

  • Franz van Betteraey Mar 17, 2014 @ 22:49

    Hi Petri,

    nice post. I had a similar problem and decided to handle it the following way:

    (1) Set defaults, like

    @Scheduled(cron = "${scheduling.job.cron:0 0-59 * * * *}")

    (2) place a property file in src/main/resources (e.g. application.properties) with all properties mentioned (for documentation purpose) but commented out (so that the defaults will be choosen), e.g.:

    # Cron expression to schedule job
    # Default: 0 0-59 * * * *
    #scheduling.job.cron=

    (3) Put the property file as default to the configuration but allow to choose onther file by configuration:

    @PropertySource("${propertyFile:classpath:application.properties}")

    This has the following advanteges

    (1) System will start with proper default in case of no configuration
    (2) Properties can be easily overwritten in another property file (possibly provided on different environments by the operation department), specified with -DpropertyFile=file:c:/someFile.properties
    (3) Properties can be overwritten by using system properties, e.g.: -Dscheduling.job.cron="0 */1 * * * *" (also easily done by the operation department on the target environments)

    No need to prepare classpath system property files for different environments - and no need to modify the maven pom. On a "mvn deploy" you even have to choose a profile with your solution and than the artefact is considered to be final. Instead, just set the appropriate configuration value in your environment by e.g. overwriting the default with a system property (PropertySourcesPlaceholderConfigurer will resolve these if it can not find the property in the property file) and things will work fine ;-)

    Kind regards,
    Franz

    PS: By the way, you declare a lot of spring artefacts in your pom.xml. I think the springframework people did well to mention just the "spring-context" artefact as necessary to get the most needed dependencies (including aop, beans, core, spring-expression) so you propably only have to add "spring-context", "spring-web" (and "context-support") to your pom.

    PSS: I love to leave my email to you just to put a comment to your post ;-)

    • Petri Mar 17, 2014 @ 23:04

      Hi Franz,

      That is a great tip! I had no idea that you can specify a default configuration and override it if necessary. I think that it is a really useful feature. Also, I will check out the dependency issue as well because I would love to get rid of "unnecessary" dependencies.

      By the way, from now on you can leave comments without entering your email address. I don't really need it (or want it).

  • isaac May 13, 2014 @ 7:40

    Hey,
    Great post! thank you!

    How can i disabled the scheduling mechanism when running unit-tests?
    Since I've added a scheduled job - every time I run the project's unit tests
    it also starts the scheduled job...

    I'm using @Scheduled with a fixedDelay..
    Thanks!

    • Petri May 13, 2014 @ 18:41

      Hi,

      I don't have to disable scheduled jobs in my unit tests because I use application context based configuration only when I write unit tests for my controllers, and I don't configure the web layer and scheduling in the same configuration class.

      However, I have to disable them when I run my integration tests. You can use Spring profiles for this purpose.

  • Esteban Oct 21, 2014 @ 16:10

    Hi Petri, thank you for your post, like always it's very nice and well explained.

    I've a question regarding @Scheduled. It's possible to put the cron expression in a database? We want to give the user the possibility to modify when the cron jobs are going to execute.

    • Petri Oct 21, 2014 @ 17:56

      Hi Esteban,

      Thank you for your kind words. I really appreciate them.

      I haven't done this myself, but you could take a look at this StackOverflow answer. It provides links to articles that might be useful to you.

      I will take a closer look at this and publish a new tutorial when I have got time to do it.

  • farziny Feb 6, 2015 @ 22:25

    Hi Petri,
    i'm really confused ! where was gone pool-size option on xml side in annotation configuration !
    i write a sample test using 3 @Scheduled but it seems they are called from a single Thread
    even when @Async and @EnableAsync used !
    do you have any idea ?
    thank you,

    • Petri Feb 7, 2015 @ 10:16

      Hi Farziny,

      Read the Javadoc of the @EnableScheduling annotation. It describes how you can implement the SchedulingConfigurer interface and configure the pool size when you use Java configuration.

      • farziny Feb 8, 2015 @ 8:55

        really good, thank you
        why they didn't put something like pool size on @EnableScheduling annotation to handle some thing like this ?

        • Petri Feb 8, 2015 @ 21:30

          You are welcome!

          I have no idea why they didn't add this to the @EnableScheduling annotation. One possible reason for this is that they want to provide a consistent way of configuring Spring applications, and providing these "configurer" interfaces seems to be their way of doing it.

  • Hemal Apr 30, 2017 @ 22:53

    Hello Petri,
    Thanks for such a great article, its the best one so far I found. I am in a similar situation where I want to load cron from properties file. I am trying to parameterize cron expression to a properties file using method 3 explained above. I am getting following exception "WARNING: Exception encountered during context initialization - cancelling refresh attempt:

    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springScheduleCronExample': Initialization of bean failed; nested exception is java.lang.IllegalStateException: Encountered invalid @Scheduled method 'cronJob': Cron expression must consist of 6 fields (found 1 in "${cron.expression}")"

    Then I found this post http://stackoverflow.com/questions/36418882/scheduled-throwing-exception-for-cron-expression, and made changes accordingly and it works when I define AnnotationConfigApplicationContext in my main method but when I remove this code my cron stops working and I don't see any errors. Can you please help me with this. Eventually I want to move this working code into my actual ServiceImpl code.

    My application-context.xml

    My SpringScheduleCronExample.java

    package com.hemal.spring;

    import java.util.Date;
    import java.util.concurrent.atomic.AtomicInteger;

    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.Scheduled;

    @Configuration
    @EnableScheduling
    @PropertySource("classpath:application.properties")
    public class SpringScheduleCronExample {
    private AtomicInteger counter = new AtomicInteger(0);

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {

    return new PropertySourcesPlaceholderConfigurer();
    }

    @Scheduled(cron = "${cron.expression}")
    public void cronJob() {
    int jobId = counter.incrementAndGet();
    System.out.println("Job @ cron " + new Date() + ", jobId: " + jobId);
    }

    public static void main(String[] args) {
    //If I remove this line of code I don't see print statement. End goal it make it work with application Server.
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
    SpringScheduleCronExample.class);
    try {
    Thread.sleep(24000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    context.close();
    }
    }
    }
    And my application.properties is
    cron.expression=*/5 * * * * ?

    Any help is appreciated.

    • Hemal Apr 30, 2017 @ 22:55

      Missed my application context.xml

      • Hemal Apr 30, 2017 @ 23:42

        I think I am not allowed to post an xml
        "

        "

        • Petri May 1, 2017 @ 20:09

          Wordpress doesn't allow anyone (expect me) to enter XML to the cormment form. In any case, it is not needed because I think that I know what your problem is.

          Have you configured the PropertySourcesPlaceholderConfigurer bean in your application context configuration file? If you haven't configured it, you need to add this bean to your application configuration file.

          • Hemal May 2, 2017 @ 22:07

            Thanks, you were correct I had issues loading application context and was able to fix it. I want to externalize my cron to properties file so that I don't want to rebuild my EAR file.

  • Dinesh May 28, 2017 @ 16:58

    Thanks Really helpful blog for spring batch,Do you have any information regarding how spring boot and batch are integrated!

    Really Thanks for giving internals about spring batch!

  • SaBamPa Jan 2, 2018 @ 20:30

    Very good and understandable. Thanks

    • Petri Jan 7, 2018 @ 20:38

      Thank you for your kind words. I really appreciate them. Also, it was great to hear that this blog post was useful to you.

  • Atti Sep 14, 2022 @ 19:11

    My application is running in 3 different nodes in the cloud for performance purposes. The problem is that the report scheduled is generated 3x. Is there a way to get around this?

    • Petri Sep 18, 2022 @ 22:50

      Hi,

      You have basically two options:

      • You can create a new node which runs all scheduled jobs. The downside of this approach is that it can require "a lot of work".
      • You can add a configuration property which enables scheduled jobs. After you have done this, you can simple select the node which runs the scheduled jobs. The downside of this approach is that you have to change to code of the classes which start your scheduled jobs. The new implementation of these classes must run your scheduled jobs only if the value of your new configuration property is true.

      If you have any additional questions, don't hesitate to ask them.

Leave a Reply