Control different environments with Spring profiles
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: