Spring @ConfigurationProperties. Spring provides several ways of… | by Habeeb Okunade | Medium
Habeeb Okunade ・ ・ 10 min read
Medium
Spring provides several ways of injecting/looking up values from the application configuration properties file.
One of them is the @Value
annotation discussed in the Spring @Value annotation tricks write up.
Another one is the using @ConfigurationProperties
annotation on a configuration bean to inject properties values to a bean and use it in the class. Also is the using of Environment
object to lookup values, properties or profiles in the classpath.
In this write-up, I will demonstrate how to use @ConfigurationProperties
annotation to inject properties values from the application.properties
or application.yaml
files.
Let go to https://start.spring.io/ to generate and bootstrap our project.
Choose Maven Project, Java as the language, give your project the group name and artefact Id. Select Spring Web
as the only dependency for our project.
Click ‘Generate’ button to download the bootstrapped project as a zip file. Unzip the file and open it with your prefered IDE as a Maven project to download all the required dependencies and at the end of all these, your project structure should look like the image below:
Open the application.properties file and write the properties values below:
#DEV environment properties
dev.name=Development Application
dev.port=8091
dev.dbtype=Maria DB
dev.version=1.0.alpha
dev.dburl=jdbc:mariadb://localhost:3306/
dev.dbname=studentDB
dev.dbuser=root
dev.dbpassword=root
#QA environment properties
qa.name=QA Test Application
qa.port=8092
qa.dbtype=Mongo DB
qa.version=1.2.beta
qa.dburl=mongodb://mongodb0.example.com:27017/
qa.dbname=studentDB
qa.dbuser=admin
qa.dbpassword=admin
#PROD environment properties
prod.name=Production Application
prod.port=8091
prod.dbtype=MySQL
prod.version=1.0.1
prod.dburl=jdbc:mysql://localhost:3308/
prod.dbname=studentDB
prod.dbuser=admin-prod
prod.dbpassword=admin-prod
The file contains three properties values for three different environment — dev, qa and prod. Each property is prefixed with the type of environment they belong to. This is not a good practice to have all these specific environment properties on a single file. In reality, we cannot have different environment properties like this, but rather create a specific application properties file for each environment, set up a profile
and choose the active profile or profiles to use. Just bear with me with this approach to show how to use the annotation. Our aim is to load these properties into our application by creating beans and using @ConfigurationProperties
annotation.
Let’s create a package called config and create three classes DevConfigProps.java, QaConfigProps.java and ProdConfigProps.java
in this package and annotate each class with @Configuration
to tell spring that these classes should be scanned, managed and configured as spring beans during application bootup.
If you consider the application.properties
file above, you will notice that each of these properties is either prefix with dev
, qa
or prod
. These prefixes will be used to configure these classes by using the @ConfigurationProperties("prefix")
annotation.
@ConfigurationProperties("dev")
for dev properties, @ConfigurationProperties("qa")
for qa properties and @ConfigurationProperties("prod")
for prod properties.
//DevConfigProps.java
package com.habeebcycle.configurationpropertiesannotation.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties("dev")
public class DevConfigProps {
}
//QaConfigProps.java
package com.habeebcycle.configurationpropertiesannotation.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties("qa")
public class QaConfigProps {
}
//ProdConfigProps.java
package com.habeebcycle.configurationpropertiesannotation.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties("prod")
public class ProdConfigProps {
}
@ConfigurationProperties
allows us to map the entire properties or YAML files into an object easily. It also allows us to validate properties with JSR-303 bean validation. By default, the annotation reads from the application.properties
file. The properties file to be used can be changed with @PropertySource("file-name")
annotation if we don’t want to use the default properties file.
The next step is to create all the properties names as a variable with their setters and getters.
for DevConfigProps.java
package com.habeebcycle.configurationpropertiesannotation.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties("dev")
public class DevConfigProps {
private String name;
private int port;
private String dbType;
private String version;
private String dbUrl;
private String dbName;
private String dbUser;
private String dbPassword;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getDbType() {
return dbType;
}
public void setDbType(String dbType) {
this.dbType = dbType;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getDbUrl() {
return dbUrl;
}
public void setDbUrl(String dbUrl) {
this.dbUrl = dbUrl;
}
public String getDbName() {
return dbName;
}
public void setDbName(String dbName) {
this.dbName = dbName;
}
public String getDbUser() {
return dbUser;
}
public void setDbUser(String dbUser) {
this.dbUser = dbUser;
}
public String getDbPassword() {
return dbPassword;
}
public void setDbPassword(String dbPassword) {
this.dbPassword = dbPassword;
}
}
for QaConfigProps.java
package com.habeebcycle.configurationpropertiesannotation.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties("qa")
public class QaConfigProps {
private String name;
private int port;
private String dbType;
private String version;
private String dbUrl;
private String dbName;
private String dbUser;
private String dbPassword;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getDbType() {
return dbType;
}
public void setDbType(String dbType) {
this.dbType = dbType;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getDbUrl() {
return dbUrl;
}
public void setDbUrl(String dbUrl) {
this.dbUrl = dbUrl;
}
public String getDbName() {
return dbName;
}
public void setDbName(String dbName) {
this.dbName = dbName;
}
public String getDbUser() {
return dbUser;
}
public void setDbUser(String dbUser) {
this.dbUser = dbUser;
}
public String getDbPassword() {
return dbPassword;
}
public void setDbPassword(String dbPassword) {
this.dbPassword = dbPassword;
}
}
for ProdConfigProps.java
package com.habeebcycle.configurationpropertiesannotation.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties("prod")
public class ProdConfigProps {
private String name;
private int port;
private String dbType;
private String version;
private String dbUrl;
private String dbName;
private String dbUser;
private String dbPassword;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getDbType() {
return dbType;
}
public void setDbType(String dbType) {
this.dbType = dbType;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getDbUrl() {
return dbUrl;
}
public void setDbUrl(String dbUrl) {
this.dbUrl = dbUrl;
}
public String getDbName() {
return dbName;
}
public void setDbName(String dbName) {
this.dbName = dbName;
}
public String getDbUser() {
return dbUser;
}
public void setDbUser(String dbUser) {
this.dbUser = dbUser;
}
public String getDbPassword() {
return dbPassword;
}
public void setDbPassword(String dbPassword) {
this.dbPassword = dbPassword;
}
}
To test our properties, let’s create a configuration class that implements CommandLineRunner
so that we can log all the properties configured in its run
method. In this class, we will have these three configuration classes autowired since they are spring managed beans.
package com.habeebcycle.configurationpropertiesannotation;
import com.habeebcycle.configurationpropertiesannotation.config.DevConfigProps;
import com.habeebcycle.configurationpropertiesannotation.config.ProdConfigProps;
import com.habeebcycle.configurationpropertiesannotation.config.QaConfigProps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Configuration;
@Configuration
public class PropertiesConfig implements CommandLineRunner {
private final DevConfigProps devConfigProps;
private final QaConfigProps qaConfigProps;
private final ProdConfigProps prodConfigProps;
private static final Logger logger = LoggerFactory.getLogger(
ConfigurationPropertiesAnnotationApplication.class);
@Autowired
public PropertiesConfig(DevConfigProps devConfigProps,
QaConfigProps qaConfigProps, ProdConfigProps prodConfigProps) {
this.devConfigProps = devConfigProps;
this.qaConfigProps = qaConfigProps;
this.prodConfigProps = prodConfigProps;
}
@Override
public void run(String... args) throws Exception {
logger.info("-------------Dev Properties-----------------");
logger.info("Name: {}", devConfigProps.getName());
logger.info("Port: {}", devConfigProps.getPort());
logger.info("DB Type: {}", devConfigProps.getDbType());
logger.info("Version: {}", devConfigProps.getVersion());
logger.info("DB Url: {}", devConfigProps.getDbUrl());
logger.info("DB name: {}", devConfigProps.getDbName());
logger.info("DB User: {}", devConfigProps.getDbUser());
logger.info("DB Password: {}", devConfigProps.getDbPassword());
logger.info("-------------QA Properties-----------------");
logger.info("Name: {}", qaConfigProps.getName());
logger.info("Port: {}", qaConfigProps.getPort());
logger.info("DB Type: {}", qaConfigProps.getDbType());
logger.info("Version: {}", qaConfigProps.getVersion());
logger.info("DB Url: {}", qaConfigProps.getDbUrl());
logger.info("DB name: {}", qaConfigProps.getDbName());
logger.info("DB User: {}", qaConfigProps.getDbUser());
logger.info("DB Password: {}", qaConfigProps.getDbPassword());
logger.info("-------------Prod Properties---------------");
logger.info("Name: {}", prodConfigProps.getName());
logger.info("Port: {}", prodConfigProps.getPort());
logger.info("DB Type: {}", prodConfigProps.getDbType());
logger.info("Version: {}", prodConfigProps.getVersion());
logger.info("DB Url: {}", prodConfigProps.getDbUrl());
logger.info("DB name: {}", prodConfigProps.getDbName());
logger.info("DB User: {}", prodConfigProps.getDbUser());
logger.info("DB Password: {}", prodConfigProps.getDbPassword());
}
}
Running the application as a SpringBoot application will log the following into the console:
---------------------Dev Properties-------------------------
2020-03-01 17:26:25.625 [main]: Name: Development Application
2020-03-01 17:26:25.626 [main]: Port: 8091
2020-03-01 17:26:25.626 [main]: DB Type: Maria DB
2020-03-01 17:26:25.626 [main]: Version: 1.0.alpha
2020-03-01 17:26:25.626 [main]: DB Url: jdbc:mariadb://localhost:3306/
2020-03-01 17:26:25.626 [main]: DB name: studentDB
2020-03-01 17:26:25.626 [main]: DB User: root
2020-03-01 17:26:25.627 [main]: DB Password: root
---------------------QA Properties-------------------------
2020-03-01 17:26:25.627 [main]: Name: QA Test Application
2020-03-01 17:26:25.627 [main]: Port: 8092
2020-03-01 17:26:25.627 [main]: DB Type: Mongo DB
2020-03-01 17:26:25.627 [main]: Version: 1.2.beta
2020-03-01 17:26:25.627 [main]: DB Url: mongodb://mongodb0.example.com:27017/
2020-03-01 17:26:25.627 [main]: DB name: studentDB
2020-03-01 17:26:25.627 [main]: DB User: admin
2020-03-01 17:26:25.627 [main]: DB Password: admin
---------------------Prod Properties-------------------------
2020-03-01 17:26:25.627 [main]: Name: Production Application
2020-03-01 17:26:25.627 [main]: Port: 8091
2020-03-01 17:26:25.627 [main]: DB Type: MySQL
2020-03-01 17:26:25.628 [main]: Version: 1.0.1
2020-03-01 17:26:25.628 [main]: DB Url: jdbc:mysql://localhost:3308/
2020-03-01 17:26:25.628 [main]: DB name: studentDB
2020-03-01 17:26:25.628 [main]: DB User: admin-prod
2020-03-01 17:26:25.628 [main]: DB Password: admin-prod
Using @ConfigurationProperties
to inject List properties.
We can also use @ConfigurationProperties
to read List values from a properties file.
#application.properties
dev.mylist=SUN,MON,TUE,WED,THU,FRI,SAT
and use @ConfigurationProperties
to inject the list as follows
package com.habeebcycle.configurationpropertiesannotation.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
@ConfigurationProperties("dev")
public class DevConfigProps {
private List<String> myList;
public List<String> getMyList() {
return myList;
}
public void setMyList(List<String> myList) {
this.myList = myList;
}
}
Logging the property gives
[SUN, MON, TUE, WED, THU, FRI, SAT]
Using @ConfigurationProperties
to inject Map (key-value pairs) properties
key-value pair properties can be read or injected with @ConfigurationProperties
annotation as follows:
#application.properties
dev.db.url=jdbc:mysql://localhost:3308/
dev.db.name=student_marks
dev.db.user=root
dev.db.password=root
From the properties file above, we can see that after the prefix dev
, we have db
. This db
will be the variable to use to inject the values as a key-value map.
package com.habeebcycle.configurationpropertiesannotation.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
@Configuration
@ConfigurationProperties("dev")
public class DevConfigProps {
private Map<String, String> db;
public Map<String, String> getDb() {
return db;
}
public void setDb(Map<String, String> db) {
this.db = db;
}
}
Spring will automatically inject the nested properties into the db
variables as Map variable.
Logging out the property value of devConfigProps.getDb()
, will output the following:
{
"url": "jdbc:mysql://localhost:3308/",
"name": "student_marks",
"user": "root",
"password": "root"
}
Using @ConfigurationProperties
to inject a class (Object) from a properties file
Let’s create a class called Student.java
package com.habeebcycle.configurationpropertiesannotation.model;
public class Student {
private String id;
private String name;
private int age;
private String level;
private double mark;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public double getMark() {
return mark;
}
public void setMark(double mark) {
this.mark = mark;
}
}
If we have a properties file as follows
#application.properties
dev.student.id=ABC123XYZ
dev.student.name=Habeeb Okunade
dev.student.age=34
dev.student.level=700J
dev.student.mark=99.4
From the properties file above, we can see that after the prefix dev, we have
. This student*
student
will be the variable to use to inject the values as a Student*
object.
package com.habeebcycle.configurationpropertiesannotation.config;
import com.habeebcycle.configurationpropertiesannotation.model.Student;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties("dev")
public class DevConfigProps {
private Student student;
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
}
Spring will automatically inject the nested properties into the student
object as Student variable.
Logging out the property value of devConfigProps.getStudent()
, will output the following:
{
"id": "ABC123XYZ",
"name": "Habeeb Okunade",
"age": 34,
"level": "700J",
"mark": 99.4
}
Using @ConfigurationProperties
on a @Bean
method
We can also use @ConfigurationProperties
annotation on @Bean
-annotated methods.
This approach may be particularly useful when we want to bind properties to a third-party component that’s outside of our control.
Let’s create a simple School
class to show how to use this:
package com.habeebcycle.configurationpropertiesannotation.model;
public class School{
private String id;
private String name;
private int size;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
}
Now, let’s see how we can use @ConfigurationProperties
on a @Bean
method to bind externalized properties to the School
instance:
package com.habeebcycle.configurationpropertiesannotation.config;
@Configuration
public class ExternalConfigProperties {
@Bean
@ConfigurationProperties("school")
public School school() {
return new School();
}
}
By this, any school
-prefixed property will be mapped to the **School
instance managed by the Spring context.
Using @ConfigurationProperties
for property validation
@ConfigurationProperties
provides validation of properties using the JSR-303 format. This allows our properties to come out neatly. Consider the following class:
package com.habeebcycle.configurationpropertiesannotation.config;
import org.hibernate.validator.constraints.Length;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
@Configuration
@ConfigurationProperties("validate")
@Validated
public class ConfigValidation {
@NotNull
private String dbUrl;
@Length(min = 4, max = 10)
private String dbUser;
@Min(8080)
@Max(8100)
private int dbPort;
@Pattern(regexp = "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,6}$")
private String adminEmail;
public String getDbUrl() {
return dbUrl;
}
public void setDbUrl(String dbUrl) {
this.dbUrl = dbUrl;
}
public String getDbUser() {
return dbUser;
}
public void setDbUser(String dbUser) {
this.dbUser = dbUser;
}
public int getDbPort() {
return dbPort;
}
public void setDbPort(int dbPort) {
this.dbPort = dbPort;
}
public String getAdminEmail() {
return adminEmail;
}
public void setAdminEmail(String adminEmail) {
this.adminEmail = adminEmail;
}
}
This class has some validation rules for the @ConfigurationProperties
annotation to work.
The @NotNull
annotation tells the Spring that the dbUrl
field is mandatory and throws an error if not found.
The @Length(min=4, max=6)
annotation validate dbUser
to only inject property of a minimum of 4 characters and 6 characters long.
The @Min
and @Max
annotation to inject port
value in the range of 8080 to 8100.
lastly, the adminEmail
property must match the pattern provided as @Pattern(regexp = "^[a-z0–9._%+-]+@[a-z0–9.-]+\.[a-z]{2,6}$")
This helps us reduce a lot of if-else conditions in our code and makes it look much cleaner and concise.
If any of these validations fail, then the main application would fail to start with an IllegalStateException
.
In this write-up, I have shown how to use @ConfigurationProperties
to read /inject configuration properties from a properties file and explained some of the handy features it provides like property mapping, binding and validation.
Top comments (3)
Thanks for the post.
What I like most about the feature is the possibility to avoid
setters()
using@ConstructorBinding
, making the properties from a YAMLprivate final
and therefore immutable. And this goes hand in hand with Lombok's@AllArgsConstructor
and@Getter
.For example:
This makes sense from a perspective of each environment having its own configuration but this isn’t what you actually want in the end. You want the configuration to be driven by the environment so that the code is completely environment agnostic. In this example, how would you use the QA config specifically in the QA environment?
Hi Ben,
This article is not meant to explain how to configure properties environment but rather on how to use @ConfigurationProperties annotation. There are so many technologies out there to use in QA & Prod environments like docker config/env file, K8s configMap/Secrets, Hashicorp vault, Spring config server etc.
I will try to write something about it in the next article. This one is just to explain the usefulness of @ConfigurationProperties annotation in a simple spring application.