Hello and welcome to my blog🙂 If you’ve been working with @ConfigurationProperties and environment variables in Spring Boot, then you probably saw that by default, Spring Boot does not fail when the value is missing.
In this article, I will show you four ideas on how to solve this issue.
Please keep in mind that all of them are workarounds, and if you came up with something even better, then do not forget to share in the comments🙂
Video Tutorial
If you prefer video content, then check out my video:
If you find this content useful, please leave a subscription 😉
Problem
To be on the same page, let’s take a quick look at a simple Spring Boot configuration with @ConfigurationProperties.
Firstly, let’s navigate to the application.yaml
file:
example: some-property: ${SOME_VARIABLE}
As we can see, we defined a new property inside our config file for which the value should be set from the environment variable named SOME_VARIABLE
.
Following, let’s introduce the @ConfigurationProperties class:
@ConfigurationProperties(prefix = "example") data class ExampleConfigurationProperties( val someProperty: String )
With this code, we expect that Spring Framework will read all properties defined in the configuration file and inject values into the matching properties.
Note: Spring recognizes the kebab case (some-property) and matches it with the property using camel case (someProperty)
After that, we need to explicitly inform Spring about our configuration properties class:
@Configuration @EnableConfigurationProperties(ExampleConfigurationProperties::class) class Config
Lastly, we must add a class, which injects the ExampleConfigurationProperties and prints out the value of someProperty:
@Component class SomeComponent( private val properties: ExampleConfigurationProperties ) { @PostConstruct fun init() { println(properties.someProperty) } }
Finally, when we run our application without the SOME_VARIABLE environment variable missing, we will see the following output:
${SOME_VARIABLE}
A bit counterintuitive, right?
It would seem pretty obvious, that if we declare someProperty as a non-nullable String and environment variable is missing, our Spring Boot app should fail to start.
Unfortunately, that’s not the case when using @ConfigurationProperties and Spring will default the String value to the name of placeholder.
Moreover, at the moment of writing, there is no built-in way we could enforce that.
Solution With Spring Validation
As a first solution (or rather workaround), let’s add the spring-boot-starter-validation to our project:
implementation("org.springframework.boot:spring-boot-starter-validation")
Nextly, let’s instruct Spring that it should default to the blank String value whenever our environment variable is missing:
example: some-property: ${SOME_VARIABLE:}
After that let’s modify our configuration properties class:
import jakarta.validation.constraints.NotBlank import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.validation.annotation.Validated @ConfigurationProperties(prefix = "example") @Validated data class ExampleConfigurationProperties( @field:NotBlank val someProperty: String )
As we can see, with a combination of @Validated and @NotBlank, we ask Spring Framework to check if the value is not null and contains at least one non-whitespace character.
Note: in Kotlin, we must use the @field: to annotate Java field.
If you would like to learn this, and many more then check out my Kotlin course.
Finally, when we try to run our application, we will see the following:
*************************** APPLICATION FAILED TO START *************************** Description: Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'example' to com.codersee.properties.ExampleConfigurationProperties failed: Property: example.someProperty Value: "" Origin: class path resource [application.yaml] - 2:18 Reason: must not be blank Action: Update your application's configuration Process finished with exit code 1
And maybe this solution is nott ideal, but it does its job.
And it is neat. Not only does the application not start, but also whenever the environment variable for our @ConfigurationProperties is missing, we will see a meaningful message that will speed up the debugging process.
Custom ApplicationRunner Idea
Another solution for our problem does not require any additional dependencies added to our project.
Let’s take a look at the following implementation of ApplicationRunner:
import org.springframework.boot.ApplicationArguments import org.springframework.boot.ApplicationRunner import org.springframework.stereotype.Component @Component class MyApplicationRunner : ApplicationRunner { companion object { private val requiredVariables = setOf( "SOME_VARIABLE" ) } override fun run(args: ApplicationArguments) { requiredVariables.forEach { System.getenv(it) ?: throw MissingEnvVariableException(it) } } } class MissingEnvVariableException(variableName: String) : RuntimeException("Application failed to start. Missing environment variable: $variableName")
This time, we introduce our set of required environment variables and check if they are present.
Whenever it’s missing, we throw the MissingEnvVariableException and prevent the app from starting:
java.lang.IllegalStateException: Failed to execute ApplicationRunner at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:765) ~[spring-boot-3.1.3.jar:3.1.3] at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:752) ~[spring-boot-3.1.3.jar:3.1.3] at org.springframework.boot.SpringApplication.run(SpringApplication.java:319) ~[spring-boot-3.1.3.jar:3.1.3] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-3.1.3.jar:3.1.3] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) ~[spring-boot-3.1.3.jar:3.1.3] at com.codersee.properties.PropertiesApplicationKt.main(PropertiesApplication.kt:13) ~[main/:na] Caused by: com.codersee.properties.MissingEnvVariableException: Application failed to start. Missing environment variable: SOME_VARIABLE at com.codersee.properties.MyApplicationRunner.run(MyApplicationRunner.kt:19) ~[main/:na] at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:762) ~[spring-boot-3.1.3.jar:3.1.3] ... 5 common frames omitted Process finished with exit code 1
This solution gives us a bit more control and allows us to customize the behavior better.
Would we like to log an error, or maybe send an alert request? No problem. With a few lines of code, we can easily achieve that.
Nevertheless, with this approach, we introduce a new, separate place in the code we need to remember every time we want to add a new environment variable. And this might be easily forgotten in bigger projects.
Fix With @Conditional
As the next step, let’s take a look at the approach with @Conditional annotation, which slightly reduces the chance of forgetting about additional checks.
Firstly, let’s add a new class called RequiredEnvVariablesCondition:
class RequiredEnvVariablesCondition : Condition { private val logger: Logger = LogManager.getLogger(this::class.java) companion object { private val requiredVariables = setOf( "SOME_VARIABLE" ) } override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean { val isEnvVariableMissing = requiredVariables .any { envVariableName -> val isMissing = System.getenv(envVariableName) == null if (isMissing) logger.error("Variable $envVariableName is missing!") isMissing } return !isEnvVariableMissing } }
As we can see, this logic is pretty similar to what we’ve done previously.
Every time the environment variable is missing, we log an error message and return false from the overridden matches function.
So with that done, we can annotate our config class with @Conditional:
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Conditional import org.springframework.context.annotation.Configuration @Configuration @EnableConfigurationProperties(ExampleConfigurationProperties::class) @Conditional(RequiredEnvVariablesCondition::class) class Config
This time, we will get the following output if we try to run the app without passing the environment variable:
2023-09-14T07:20:58.813+02:00 ERROR 16188 --- [ main] c.c.p.RequiredEnvVariablesCondition : Variable SOME_VARIABLE is missing! *************************** APPLICATION FAILED TO START *************************** Description: Parameter 0 of constructor in com.codersee.properties.SomeComponent required a bean of type 'com.codersee.properties.ExampleConfigurationProperties' that could not be found. Action: Consider defining a bean of type 'com.codersee.properties.ExampleConfigurationProperties' in your configuration. Process finished with exit code 1
(Bonus) Use @Value Instead
There’s no doubt that @ConfigurationProperties helps us write cleaner code and better organize configuration in our codebase. Moreover, we can effortlessly add new values to the project with this approach.
However, in some cases, we should consider using @Value annotation instead:
package com.codersee.properties import jakarta.annotation.PostConstruct import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component @Component class SomeComponent( @Value("\${example.some-property}") private val someProperty: String ) { @PostConstruct fun init() { println(someProperty) } }
This time, we can completely get rid of @Configuration and @ConfigurationProperties classes.
Instead of injecting a whole configuration object, we focus on the desired config value.
And whenever it’s missing, we get the following output out of the box:
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. 2023-09-14T07:16:43.532+02:00 ERROR 18412 --- [ main] o.s.boot.SpringApplication : Application run failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'someComponent' defined in file: Unexpected exception during bean creation ... Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'SOME_VARIABLE' in value "${SOME_VARIABLE}" ... Process finished with exit code 1
As we can see, our Spring Boot app won’t start if the environment variable is missing and we will get a meaningful message.
But again, we should use @Value wisely and most of the time @ConfigurationProperties can be a better approach.
Summary
And that’s all for this article on how to fail a Spring Boot app on missing environment variables.
As I mentioned in the beginning- these are just a few ideas that came to my mind. If you figure out some other way, then please share them in the comments section below.
As always, you can find the source code in this GitHub repository.
Thank you for reading, and see you next time! 🙂