Are you looking for an advent calendar? You found it!

Spring from the Trenches: Using Environment Specific Cron Expressions with the @Scheduled Annotation

Staring at the tank gun

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:

If you enjoyed reading this blog post, you should follow me on Twitter:

About the Author

Petri Kainulainen is passionate about software development and continuous improvement. He is specialized in software development with the Spring Framework and is the author of Spring Data book.

About Petri Kainulainen →

10 comments… add one

  • 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

    Reply
  • 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

    Reply
    • 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 :)

      Reply
  • 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 ;-)

    Reply
    • 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).

      Reply
  • 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!

    Reply
    • 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.

      Reply
  • 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.

    Reply
    • 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.

      Reply

Leave a Comment