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):
- Create a class called ScheduledJob.
- Annotate the class with the @Component annotation.
- Create a private Logger field and instantiate the created field.
- Create a public method called run() and ensure that its return type is void.
- 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).
- 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:
- Configure Maven.
- Create the properties files.
- Configure the application context.
- Modify the task class.
Let’s get started.
Configuring Maven
We can configure Maven by following these steps:
- Create profiles for both development and production environment.
- 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:
- Add a new profile to the profiles section of the POM file.
- Set the id of the created profile to ‘dev’.
- Ensure that the development profile is active by default.
- Create a property called build.profile.id and set its value to ‘dev’.
We can create the production profile by following these steps:
- Add a new profile to the profiles section of the POM file.
- Set the id of the created profile to ‘prod’.
- 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:
- 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).
- 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:
- We have to create a properties file for the development environment.
- We have to create a properties file for the production environment.
- 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:
- Create a file called config.properties to the profiles/dev directory.
- 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:
- Create a file called config.properties to the profiles/prod directory.
- 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:
- Create a file called application.properties to the src/main/resources directory.
- 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:
- Create a class called ExampleApplicationContext.
- Annotate the class with the @Configuration annotation.
- Enable scheduling by annotating the class with the @EnableScheduling annotation.
- Annotate the class with the @ComponentScan annotation and configure the scanned packages.
- 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.
- 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:
- 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.
- Use the annotation-config element of the context namespace for ensuring that the “general” annotations are detected from our bean classes.
- Use the component-scan element of the context namespace for configuring the scanned packages.
- 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.