Mastodon

Stage Switch with Spring

Like other applications, my main project runs through several quality gates before being deployed on the production stage. For example, there is a developer stage, which simply is the locally running version on every developer machine, and a test stage. This test stage is similar to the production setup and can be used to test features before they are merged into the main branch of your version control system. Many projects use an additional quality stage which runs the latest stable build so the customer can test, too. The deployed system behaves differently for every stage. The simplest difference is the database used. You don’t want to use the same database for your production and test system, so the deployed code has to decide which database to use. This article describes, how we use Spring to setup different stages.

Because the deployed code is the same in every stage, we need a system property to set the stage. In this example, there are only two instances, using the following system properties:

  1. developer stage: no system property set (to keep the costs for setting up a new developer machine as low as possible)
  2. test stage: spring.profiles.active = test

As mentioned above, other stages can easily be added to this setup.

This system property has to be used by Java code. To implement clean code, the first step is to have an enum for the system properties used:

package constants;
 
public interface Constants {
 
    // active Spring profile
    String KEY_SPRING_PROFILE = "spring.profiles.active";
}

Depending on the stage the application is running on, there are different configurations to use. These are listed in properties files. For each stage, there is one property file. They have the same attributes, but different values. To keep this example simple, there are just two attributes in the files. One for the name of the application (which is the same regardless of the stage) and one string that represents the stage itself. This is the properties file for the local development stage:

# Client Properties for local environment
 
web.app.name=/MyProject/
 
stage=developer-instance

This is the properties for the test stage:

# Client Properties for test environment
 
web.app.name=/MyProject/
 
stage=test-instance

Using Java configuration, these configuration files can be mapped to a Java class:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
 
import static constants.Constants.KEY_SPRING_PROFILE;
 
/**
 * This class is a Java-mapping of a properties file. Every attribute in the file
 * is an attribute in this class. Which property-file is mapped is decided on the
 * configuration in the PropertySource-annotation. If a system property
 * "spring.profiles.active" is set, it is used to determine the path of the property
 * file. For the test stage, this would be "test/client.properties".
 * If no system property is set, "developer/client.properties" will be mapped.
 */
@PropertySource("classpath:${"+KEY_SPRING_PROFILE+":developer}/client.properties")
@Configuration
public class Properties
{
    @Value("${web.app.name}")
    private String webAppName;
 
    @Value("${stage}")
    private String stage;
 
    public String getWebAppName() {
        return webAppName;
    }
 
    public String getStage() {
        return stage;
    }
}

There are two things happening in this class. First, the annotation of the class decides which property file to map. If there is a system property “spring.profiles.active”, it is used. If there is no property, the default “developer” is used. The path of the property file is “[stage]/client.properties”.

Second, each attribute of the property file is mapped into the Java class to be used by the application.

Of course, this setup has to be tested. Here’s the test for the locally running developer stage:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import static org.junit.Assert.assertEquals;
 
/**
 * Test for the local developer stage that doesn't use any system properties.
 */
@ContextConfiguration(classes = {Config.class})
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class DeveloperInstanceTest {
 
    @Autowired
    private ApplicationContext applicationContext;
 
    @Test
    public void propertyTest() {
        Properties properties = applicationContext.getBean(Properties.class);
        assertEquals("developer-instance", properties.getStage());
    }
}

Here’s the slightly more complicated version for the test stage. ProvideSystemProperty is used to set the system property “spring.profiles.active” to “test”:

import constants.Constants;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.ProvideSystemProperty;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import static org.junit.Assert.assertEquals;
 
/**
 * Test for the test stage that uses a system property spring.profiles.active=test.
 */
@ContextConfiguration(classes = {Config.class})
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class TestInstanceTest {
 
    // This has to be a ClassRule to set the property before the Spring setup started. Rule is not sufficient here.
    @ClassRule
    public static final ProvideSystemProperty provideSystemProperty = new ProvideSystemProperty(Constants.KEY_SPRING_PROFILE, "test");
 
    @Autowired
    private ApplicationContext applicationContext;
 
    @Test
    public void propertyTest() {
        Properties properties = applicationContext.getBean(Properties.class);
        assertEquals("test-instance", properties.getStage());
    }
}

The complete code can be found here.

TL;DR

Use Java configuration and Spring to map the stage your application is running on into your code.