DEV Community

Cover image for Spring @ConfigurationProperties annotation explained
Habeeb Okunade
Habeeb Okunade

Posted on

Spring @ConfigurationProperties annotation explained

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 student*. This 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)

Collapse
 
pixelstuermer profile image
Philipp Montesano • Edited

Thanks for the post.

What I like most about the feature is the possibility to avoid setters() using @ConstructorBinding, making the properties from a YAML private final and therefore immutable. And this goes hand in hand with Lombok's @AllArgsConstructor and @Getter.

For example:

@Getter
@Validated
@AllArgsConstructor
@ConstructorBinding
@ConfigurationProperties("foo")
public class FooBarProperties {

    @NotBlank
    private final String bar;

}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
brogrammerben profile image
Ben • Edited

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?

Collapse
 
habeebcycle profile image
Habeeb Okunade

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.