Enterprise Java

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.
 

Petri Kainulainen

Petri 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.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button