Step-by-Step Guide to Profiles with Spring Boot

Written by rainokolk | Published 2021/01/08
Tech Story Tags: spring-boot | spring-framework | spring-profiles | java | backend | programming | software-development | news

TLDRvia the TL;DR App

The Spring Framework is an application framework and inversion of control container for the Java platform. The framework’s core features can be used by any Java application, but there are extensions for building web applications on top of the Java EE (Enterprise Edition) platform. Although the framework does not impose any specific programming model, it has become popular in the Java community as an addition to, or even replacement, for the Enterprise JavaBeans (EJB) model. The Spring Framework is an open source.
Spring Profiles provide a way to segregate parts of your application configuration and make it available only in certain environments.

Preparation

Spring profiles are a very useful concept in the framework but there are some gotchas to catch for mastering it. The following article aims to explain how profiles are applied and how to survive in a multi-profile set-up.
Let's start from the set-up
  • Spring-boot
  • Lombok
  • Gradle
Everything starts from Application.java. This is a standard Spring boot command line application. Also introduce EnvironmentPrinter.java, this helps with print environment and properties.
Properties.java will be holding our properties that are defined into application.yml

Default profile

Now everything is set up. Let us start the application and find out what happens:
No active profile set, falling back to default profiles: default
Active profiles: 
Properties: Properties(param1=param1)
As we can see we don’t have an active profile. But application.yml is read and param1 has the value param1. Notice that because no active profiles have been set, spring falls back to default profile. Default profile is applied to all spring beans that do not explicitly use @Profile annotation.
Let's try to introduce default profile: dev
first, we create new yml for dev profile application-dev.yml
app:
  param1: dev-param1
and add the following to application.yml:
spring:
  profiles:
    default: dev
Let's run our application again.
No active profile set, falling back to default profiles: dev
Active profiles:
Properties: Properties(param1=param1)
Okay, it seems correct. We do not have an active profile, so it falls back to dev profile… Wait a minute, why doesn't our param1 have value dev-param1?

Active profile

Default profile makes our beans to be in dev profile, but it doesn’t load any additional properties files. So, instead of introducing the default profile, we should introduce the active profile. Replacing yml's relevant default profile block:
spring:
  profiles:
    active: dev
gives us an output:
The following profiles are active: dev
Active profiles: dev
Properties: Properties(param1=dev-param1)
Now it looks the way we expected as it should be.

Multiple active profiles

Spring allows us to activate multiple profiles. I will introduce 2 more profiles, let's name them dev-db and dev-mock. And I will create 2 more yml files.
#application-dev-db.yml

app:
  param1: dev-db-param1
#application-dev-mock.yml

app:
  param1: dev-mock-param1
Now I will activate the dev-db profile in application.yml file. Let us ignore the dev-mock profile right now. We will use it soon.
#application.yml

spring:
  profiles:
    active: dev,dev-db

app:
  param1: param1
We are now expected to have active profiles dev and dev-db. But which will be our param1 value? Let's run the application once again.
The following profiles are active: dev,dev-db
Active profiles: dev,dev-db
Properties: Properties(param1=dev-db-param1)
If I switch active profiles to dev-db,dev, the new output as follows.
The following profiles are active: dev-db,dev
Active profiles: dev-db,dev
Properties: Properties(param1=dev-param1)
So, we can conclude that if property is overridden in another profile, the last profile in the row wins.
Now let us try to activate one more profile but I want to do it in some active profile yml. In general, I will try to override spring.profiles.active property.
#application-dev-db.yml

spring:
  profiles:
    active: dev,dev-db,dev-mock
    
app:
  param1: dev-db-param1
and output for that change
The following profiles are active: dev,dev-db
Active profiles: dev,dev-db
Properties(param1=dev-db-param1)
Hmm... okay, it seems that it is not possible to override active profiles definition.

Include

Of course, we can add dev-mock to active profiles in application.yml but this is not always desired. Sometimes we want to have a certain profile include another profile. This can be done with include property. Lets modify dev profile yml a little bit.
The following profiles are active: dev,dev-db,dev-mock
Active profiles: dev,dev-db,dev-mock
Properties: Properties(param1=dev-mock-param1)
As we can see, a new dev-mock profile is included and applied as last in the row - so all the properties that dev-mock introduces, will be included and they will override previous profile properties with the same name.
Why to use include instead of modifying application.yml. Usually we do not want to introduce active profiles in application.yml or we will override profiles with command line argument -Dspring.profiles.active=. Command line override gives us the ability to introduce the exact profile per environment.
Sometimes one single profile yml might be too long or there are many repetitions with other environments. In this case, it is much more convenient to use single profile in the command line property and include the other profiles in that profile's yml file.
For example, local-dev might include dev,local-db,local-mock profiles to act like a dev environment but override database's and some third party API URL's.
In this case we introduce yml as follows:
#application-local-dev.yml

spring:
  profiles:
    include: dev,local-db,local-mock
And execute with command line parameter
"-Dspring.profiles.active=local-dev"
The following profiles are active: local-dev,dev,local-db,local-mock
Active profiles: local-dev,dev,local-db,local-mock
Properties: Properties(param1=dev-param1-override)
I didn't change application.yml, it still contains an active profile as dev,dev-db. But as you can see this is not applied. It is because of command line argument. So, If we use the command line argument, spring boot will ignore spring.profiles.active property in yml file.
This is a nice feature to know. This helps to configure the application so that it has a default active profile which will be used if nothing is specified. I usually use local-dev as default active profile and all other environments use command line parameter for overriding this profile.
In my experience, it is useful to keep running the app in the local environment as easy as possible because this is an environment that must be built up multiple times for different developers in different locations. Hassle-free local environment helps start coding as soon as possible.
Nobody wants to hear day after day that a professional developer must waste his/her time with setting up an environment.

Overrides

So far, I played with profiles and their inclusions. I already showed how profiles override each other's properties. Now let us dig deeper.
Spring Boot uses a very particular PropertySource order that is designed to allow sensible overriding of values. Properties are considered in the following order (with values from lower items overriding earlier ones)[https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config]:
  1. Default properties (specified by setting SpringApplication.setDefaultProperties).
  2. @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. It is too late to configure certain properties such as logging.* and spring.main.* which are read before refreshing begins.
  3. Config data (such as application.properties files)
  4. A RandomValuePropertySource that has properties only in random.*.
  5. OS environment variables.
  6. Java System properties (System.getProperties()).
  7. JNDI attributes from java:comp/env.
  8. ServletContext init parameters.
  9. ServletConfig init parameters.
  10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
  11. Command line arguments.
  12. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
  13. @TestPropertySource annotations on your tests.
  14. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.
As we can see the config data is number three in the list. So it's quite low in priority. In other words, we have plenty of options to override application properties.
I will not get into these because I want to keep the focus on yml files. After all, this is an article about profiles. So let's concentrate on the third bullet point.
Config data files are considered in the following order:
  1. Application properties packaged inside your jar (application.properties and YAML variants).
  2. Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants).
  3. Application properties outside of your packaged jar (application.properties and YAML variants).
  4. Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants).

Externalizing yml

Right now, our yml files were in classpath. Let's assume that we have an application as jar and we want to override something without modifying our jar package. It’s quite simple, just add yml file to the same folder than jar.
I will demonstrate it with local-dev profile.

First, let's modify classpath application-local-dev.yml. I would like to keep it simple, so I cleaned it up and added only the param1 value.
#application-local-dev.yml

app:
  param1: local-dev-param1
And our external local-dev yml (the file that I added to the same level as jar or out of the source directory, into the runtime directory)
#external application-local-dev.yml

app:
  param1: local-dev-param1-override
Let's run the application now.
The following profiles are active: local-dev
Active profiles: local-dev
Properties: Properties(param1=local-dev-param1-override)
Everything is the way we expected and like spring documentation stated. External yml file property will override the same property in classpath file.
Let's restore the include part in classpath yml.
#application-local-dev.yml
spring:
  profiles:
    include: dev, local-db,local-mock

app:
  param1: local-dev-param1
And now the output is follows:
The following profiles are active: local-dev,dev,local-db,local-mock
Active profiles: local-dev,dev,local-db,local-mock
Properties: Properties(param1=dev-param1)
We can see now that param1 is holding the value dev-param1. This might seem confusing, but it is logical if we see the list of active profiles. Even though the local-dev property was overridden in external yml file, the profile precedence is local-dev -> dev -> local-db -> local-mock. It means that dev is more important than local-dev and because of that it overrides external property.
Let's try to override spring profiles include. As I stated before, it might be that we cannot change jar file. So, we might want to change properties with an external yml file.
I added include property with some overridden profiles to yml
#external application-local-dev.yml

spring:
  profiles:
    include: nodev,local-mock,local-db

app:
  param1: local-dev-param1-override
First, I changed dev to nodev profile. Secondly, I swapped local-mock and local-db. Lets see our output
The following profiles are active: local-dev,dev,local-db,local-mock,nodev
Active profiles: local-dev,dev,local-db,local-mock,nodev
Properties: Properties(param1=dev-param1)
Now it's interesting. First it seems that spring.profiles.include cannot be overridden. Active profiles are still local-dev -> dev -> local-db -> local-mock. It is clear that this order is the same as in classpath. But then there is nodev profile as our last and most relevant profile. It seems that we can add new profiles with include but we can't change the already declared profiles. This is important to know. Nothing hints that nodev should be with highest priority and it is easy to make mistakes with overridden properties. Although I wanted to declare that nodev must be lower than local-db or local-mock, then in reality it got higher precedence.
It might very well be that the person who must run the application and is responsible for overriding profiles with a external config might not know all the profiles, or the developer changes profiles in classpath without notice. Such misconfiguration might mess up the properties values and it might be very hard to in the end to debug errors in production.

Conclusion

As we could see, using spring-boot profiles is a very useful way to mitigate different properties values in different environments. Adding all the possible ways to override properties will give us an extremely dynamic tool to change their values. But it can get very messy quite easily. Because of this, I think that overriding properties should be avoided or used as little as possible. Try to arrange profiles so that new profiles introduce new properties but do not override them.
Some recommendations that might help you as they helped me:
  1. Do not use spring.profiles.include in application.yml. As we could see, include property is merged not overridden. And application.yml is always used in property value resolving algorithm.
  2. You can use spring.profiles.active in application.yml. This can be easily overridden by system property. But do not use it in other profiles.
  3. Try to minimize overriding properties with profile specific yml files. Introduce new properties instead with profile specific yml files. For example, do not override database properties but introduce them in specific ...-db profiles. So, you can include only a specific profile for environment. For example, include profile prod-db for production and dev-db for development. This way you can avoid wrong values in wrong environment.
  4. Always keep an eye on your log output. Spring log output gives you a hint about active profiles and their precedence.
Powerful tools deserve respect!

Published by HackerNoon on 2021/01/08