Control different environments with Spring profiles

René
#jvm  #java  #kotlin  #spring  #spring boot  #profile 

JVM Spring applications are very flexible in handling different environments. You can handle multiple brands like subsidiary companies or different target environments like production or development in a very simple way. This could be necessary when environments behave differently from each other or have to connect subsystems in other ways. Generally, you do not want to use If-Else constructs in such cases. To handle these issues, the Spring Framework has an elegant solution called profiles. With these Spring profiles you can run the same build of an application on various environments with just a few additional JVM parameters.

Spring Profiles

In order to use Spring profiles in your application you just have to add the JVM parameter spring.profiles.active. If you want to start a fat jar - for example the one from our example project - with the karriereat profile you can just call it with the following command:

$ JAVA_OPTS=-Dspring.profiles.active=karriereat ./configshowcase.jar

It is also possible to use multiple profiles at the same time by just separating them with commas. The Spring Framework will use these profiles from left to right and override the different configurations in that exact order. That means if, for example, you use the profiles karriereat,prod with a YAML configuration, Spring will load all default configurations of the resources/application.yml file and overwrite these settings with values of the resources/application-karriereat.yml file and then with values of the resources/application-prod.yml file.

Different Configuration classes

There are two brand configuration classes in our example project. One for the brand “karriere.at” (profile name: karriereat) and one for the brand “jobs.at” (profile name: jobsat). In our example we use a different jackson object mapper for these two brands. The “karriere.at” object mapper uses the kebab case property naming strategy, and the “jobs.at” one uses the lower camel case property naming strategy.

You can annotate Spring components like @Configuration, @Component or @Service classes with the @Profile annotation. If you do that, the Spring context checks the condition of the @Profile annotation and only loads the bean if the condition is true. For example, here is the “KarriereAtConfig” Configuration class:

@Profile(BrandProfiles.KARRIERE_AT)
@Configuration
class KarriereAtConfig : InitializingBean {

    companion object {
        private val LOGGER = LoggerFactory.getLogger(KarriereAtConfig::class.java)
    }

    @Bean
    fun objectMapper(): ObjectMapper = Jackson2ObjectMapperBuilder.json()
            .propertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE)
            .featuresToEnable(SerializationFeature.INDENT_OUTPUT)
            .featuresToDisable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .simpleDateFormat("yyyy-MM-dd HH:mm:ss")
            .build()

    override fun afterPropertiesSet() {
        LOGGER.info("Brand configuration '{}' loaded.", BrandProfiles.KARRIERE_AT)
    }
}

Per default, Spring logs the current active profiles at startup which for our example looks like this:

INFO  a.k.e.c.WebApplicationKt - The following profiles are active: default,karriereat,prod

In our example project we want to force that at least one brand configuration is active at all times. To ensure this, you can inject the environment and check for it in your SpringBootApplication class:

@SpringBootApplication
class WebApplication(private val env: ConfigurableEnvironment) : InitializingBean {
    override fun afterPropertiesSet() {
        if (env.activeProfiles.all { it != BrandProfiles.KARRIERE_AT && it != BrandProfiles.JOBS_AT }) {
            throw RuntimeException("Cannot find brand profile!")
        }
    }
}

fun main(args: Array<String>) {
    runApplication<WebApplication>(*args)
}

Optional Dependency Injection

Sometimes you do not need all services or components in all environments, that means you can annotate those components with @Profile. In our example project there is a service called “KarriereAtTimeService”:

@Profile(BrandProfiles.KARRIERE_AT)
@Service
class KarriereAtTimeService {

    companion object {
        private val LOGGER = LoggerFactory.getLogger(KarriereAtTimeService::class.java)
    }

    fun getCurrentTime(): LocalDateTime = LocalDateTime.now().also {
        LOGGER.info("Service '{}' called at '{}'.", javaClass.simpleName, it)
    }
}

If you use this service in other components via dependency injection without the profile karriereat it is null and therefore your application will not start. There are some options to solve this problem.

  • use java.util.Optional<?>
  • use org.springframework.lang.Nullable (or any other JSR-305 nullable annotation)
  • use org.springframework.beans.factory.annotation.Autowired(required = false)
  • use Kotlins nullable types (with ?)
@RestController
@RequestMapping("/time", produces = [MediaType.APPLICATION_JSON_VALUE])
class TimeController(private val karriereAtTimeService: KarriereAtTimeService?) {

    @GetMapping
    fun showTime(): TimeResponse = TimeResponse(karriereAtTimeService?.getCurrentTime() ?: LocalDateTime.now())
}

Our example project

Our example project (configshowcase) is a gradle/kotlin/spring project containing a gradle wrapper. This means you can build it with:

$ gradlew build

To start the application you can use:

$ JAVA_OPTS=-Dspring.profiles.active=karriereat ./configshowcase.jar

Of course, you can also open the project in your favourite IDE, just open the build.gradle.kts file and import it as a project. All code snippets used in this article can be found in the runnable project for further inspection.

The project is a little JSON REST application with two controllers and two endpoints:

  • http://localhost:8080/message (MessageController)
  • http://localhost:8080/time (TimeController)

When you start the application with the karriereat profile, you can see that the kebab property naming strategy is used as well as that the response looks like the following:

{
  "message" : "Hello, welcome to the karriere.at universe",
  "current-profiles" : "default,karriereat,prod",
  "current-date" : "2020-06-04T08:29:34.383135"
}

If you prefer the lower camel case property naming strategy, you can use the jobsat profile to get another response:

{
  "message" : "Hi at jobs.at",
  "currentProfiles" : "default,jobsat,prod",
  "currentDate" : "2020-06-04T08:42:27.88963"
}

As you can see, the message is different when you change the brand profile, because Spring overrides the properties like mentioned before.

Variable Logging Configurations

Additionally, profiles can also be used for distinct logging configurations. In some environments you want file logging and in others you do not. In our example project, we defined an additional logback file logger for the profiles test and prod. The corresponding configuration looks as follows:

<springProfile name="test,prod">
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <file>${LOG_PATH}/${LOG_FILENAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${LOG_FILENAME}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxHistory>7</maxHistory>
            <maxFileSize>100MB</maxFileSize>
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <root level="INFO">
        <appender-ref ref="FILE"/>
    </root>
</springProfile>

Appendix

You can view and download the project on github: blog-config-showcase

Sources:


René

René is a JVM software developer with a passion. The Spring Framework is one of his favourite frameworks and he has been working with it for a very long time. To solve complex problems in an elegant way is his thing. In his spare time he also likes to work on IT projects but without neglecting his friends and family.


We're a team of makers, thinkers, organisers and digital explorers and we're always on the lookout for talented people.

Are you a good fit?