Properties Binding with Spring: Simplify Configuration with @ConfigurationProperties

Properties Binding with Spring: Simplify Configuration with @ConfigurationProperties

1 Comment

Last Updated on October 21, 2024 by jt

Introduction

In this article, we explained why we should externalise our application configuration data. We also provided configuration examples that use various methods supported by Spring Boot. Within these methods was the Java bean properties binding but it was less detailed. Therefore in this article, we are going to give more details about using the payment service in the previous article

Our payment service requires merchant information which consists of many fields. We are not going to use @Value annotation because it will be cumbersome work. Using @Value requires us to annotate each and every property with @Value. Our code will look untidy if we do so. A workaround is to group the merchant details together in a POJO class. This POJO class is the one referred to as Java Bean. Its properties will be bound to the configuration file data when annotated with @ConfigurationProperties.It will be easier for us to maintain these properties because they are at a single place and our code will be cleaner. Let us take a look at the comparison between @Value and @ConfigurationProperties annotations.

Features

The table below shows the features supported by each of the configuration methods provided by the annotations,  @Value and @ConfigurationProperties.

Feature@ConfigurationProperties@Value
Type-safetyYESNO
Relaxed-bindingYESNO
Meta-data supportYESNO
SpEL EvaluationNOYES

This comparison shows that the@ConfigurationProperties ticks more boxes compared to @Value. It is a better option for our use-case that involves many configuration properties.

Properties Binding

In order for us to understand how the Java Bean Properties binding works and how it is configured. We will use a step by step guide using the payments service from the previous article. The payment service shall be receiving payments made by customers for vendor services provided. This implies that we will be dealing with more than one vendor each with a unique merchant account. We must be able to identify the merchant account for each transaction request received.

Properties Names

Let us first list the individual properties of the merchant account. Let us indicate the data type of each so that it becomes easy for us to set up its configuration in a Spring Application using a POJO class and the @ConfigurationProperties annotation.

Configuration/SettingName of propertyData type of property valueProperty type
Merchant Accountmerchantaccountkey/value(Map)Object
nameString
usernameString
codeString
numberNumber
currencyString

We have identified the properties that we will use to get configuration values. Now let us create our properties file. In our case, we will use the YAML format.

application.yml

name: Maureen Sindiso Mpofu
username: momoe
code: MCA1230
number: 771222279
currency: ZWL

Properties Grouping

We now have our properties file, the next step is to bind them. To do this, first of all, we will create a Javaclass as indicated below.

public class MerchantAccount {
 private String name;
 private String username;
 private String code;
 private int number;
 private String currency;

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public String getUsername() {
  return username;
 }

 public void setUsername(String username) {
  this.username = username;
 }
 
 public String getCode() {
  return code;
 }

 public void setCode(String code) {
  this.code = code;
 }

 public int getNumber() {
  return number;
 }

 public void setNumber(int number) {
  this.number = number;
 }

 public String getCurrency() {
  return currency;
 }

 public void setCurrency(String currency) {
  this.currency = currency;
 }
}

Enabling Java Bean Properties Binding

There are many ways of binding our properties defined in our configuration file to our Java class we created in the previous section. We will annotate our Java class with @ConfigurationProperties. Inside this annotation, we will also specify the prefix of our properties so that Spring will be able to identify them in the properties file. In our case, the prefix is “merchantacccount”. The following are ways we can use to enable properties binding.

Annotating the Java bean class with @Component

@Component
@ConfigurationProperties(prefix = "merchantaccount")
public class MerchantAccount {
   private String name;
   private String userName;
   private String code;
   private int number;
   private String currency;

   //getters and setters

}

Declaring it as a bean in the Spring configuration class

This method is commonly used in scenarios where we want to bind the properties to third-party components.  This is because most of the time we have no control of these third-party components. In the example below the merchant account class can be considered as a third-party component.

@Configuration
public class PropertyConfigurations {
    
   @Bean
   @ConfigurationProperties(prefix = "merchantaccount")
   public MerchantAccount merchantAccount(){ 
      return new MerchantAccount();
   }
    //other beans

}

Using @EnableConfigurationproperties annotation

When using this method, we must also specify the configuration classes as indicated below. Let us say we had another configuration class for connection settings then we can include it as well in this same annotation as indicated below.

@SpringBootApplication 
@EnableConfigurationProperties({MerchantAccount.class, ConnectionSettings.class})
public class MyApplication {
   
}

Using @EnableConfigurationpropertiesScan annotation

This method will scan the package passed in the annotation. Any classes annotated with @ConfigurationProperties found in this package will be bound automatically.

@SpringBootApplication 
@EnableConfigurationPropertiesScan(“com.springframeworkguru”)
public class MyApplication {
}

Relaxed Binding

In our example, the property name “username” in our Java class matches the one defined in our configuration file. However, it is also possible to have different forms of property names in the configuration file for instance “username” can also be represented as below. This is known as relaxed binding.

merchantaccount:
 username: momoe        //exact match
 userName: momoe        //standard camel case
 user-name: momoe       //kebab case recommended for use in .properties or .yml files
 user_name: momoe       //underscore notation an alternative to kebab notation
 USER_NAME: momoe   //uppercase format recommended when using system environment variables

Constructor Binding

Take a look at this article for more details.

Properties Conversion

When binding external properties to the @ConfigurationProperty annotated Java Beans, Spring Boot attempts to match them to the target type. However, it is also possible to provide a custom type converter. There are various ways of providing a custom converter. Let us look at them in the following sections.

@ConfigurationPropertiesBinding annotation

First of all, we need to specify a property to our Java bean that has no default converter. Let us add a property of type LocalDateTime.

@ConfigurationProperties(prefix = "merchantaccount")
public class MerchantAccount {
   private String name;
   private String username;
   private String code;
   private int number;
   private String currency;
   private final LocalDateTime localDateTime;
   
   public LocalDateTime getLocalDateTime() {
     return localDateTime;
    }
   public void setLocalDateTime(LocalDateTime localDateTime) {
    this.localDateTime = localDateTime;
    }
//other getters and setters
}

Then configure its value in our external configuration file.

merchantaccount:
 name: Maureen Sindiso Mpofu
 username: momoe
 code: MCA1230
 number: 771222279
 currency: ZWL
 localDateTime: 2011-12-03T10:15:30

We must provide a custom converter so that we do not get a binding exception during application startup. We will use the annotation @ConfigurationPropertiesBinding as shown below. This custom converter will convert the String input type in our configuration file to a LocalDateTime . Take note that we must register this converter as a bean indicated by @Component annotation.

@Component
@ConfigurationPropertiesBinding
public class LocalDateTimeConverter implements Converter<String,LocalDateTime> {

   @Override
   public LocalDateTime convert(String s) {
       return LocalDateTime.parse(s, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
   }
}

Conversion Service bean

We can use the ConversionService bean instead of the @ConfigurationPropertiesBinding annotation. Spring picks up a ConversionService and uses it whenever type conversion needs to be performed. This conversion service is like any other bean thus can be injected into other beans and invoked directly. The default ConversionService can convert between strings, numbers, enums, collections, maps, and other common types.

However, there are other converters that are not provided by default for instance conversion to LocalDateTime. We can supplement the default converter with our own custom converter by defining a conversion service bean as indicated below. We only added our custom converter through the factory bean.

@Bean
public  ConversionServiceFactoryBean conversionService(){
  ConversionServiceFactoryBean conversionServiceFactoryBean= new ConversionServiceFactoryBean();
  Set<Converter> converters = new HashSet<>();
  converters.add(new CustomLocalDateTimeConverter());
   conversionServiceFactoryBean.setConverters(converters);
  return conversionServiceFactoryBean;
}

After we have defined our conversion service bean, Spring will be able to bind the value of LocalDateTime provided in our properties configuration file.

CustomEditorConfigurer

If we had declared a field of type java.util.Date then we must tell Spring how it will bind the Date value specified in the property configuration file to this type. We can do this by  configuring Spring’s built-in  CustomDateEditor class as indicated below.

public class CustomLocalDateTimeEditorRegistrar implements PropertyEditorRegistrar {
   @Override
   public void registerCustomEditors(PropertyEditorRegistry propertyEditorRegistry) {
       propertyEditorRegistry.registerCustomEditor(Date.class,
               new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"),false));
   }
}

We then register it through the CustomeditorConfigurer  bean factory class as indicated below.

@Bean
public CustomEditorConfigurer customEditorConfigurer(){
   CustomEditorConfigurer customEditorConfigurer = new CustomEditorConfigurer();
   PropertyEditorRegistrar[] registrars = {new CustomLocalDateTimeEditorRegistrar()};
   customEditorConfigurer.setPropertyEditorRegistrars(registrars);
   return customEditorConfigurer;
}

Duration Conversion

Spring supports duration expressions. Let us add two more properties to our Java-bean class that are of type java.time.Duration  i.e session timeout and read timeout.

@ConfigurationProperties(prefix = "merchantaccount")
public class MerchantAccount {
    private final Duration sessionTimeout;
    private final Duration readTimeout;
    //other properties
    public Duration getSessionTimeout() {
      return sessionTimeout;
    }
   public void setSessionTimeout(Duration sessionTimeout) {
     this.sessionTimeout = sessionTimeout;
   }
   public Duration getReadTimeout() {
   return readTimeout;
  }
  public void setReadTimeout(Duration readTimeout) {
   this.readTimeout = readTimeout;
  } 
   // setters and getters of other fields
}

Then in our properties file let us specify their values as indicated below.

merchantaccount:
    sessionTimeout: 15
    readTimeout: 10

When we execute our application, the default unit for both session timeout and read timeout in milliseconds. This default unit can be overridden using either of the methods defined below.

@DurationUnit annotation

We can use the @DurationUnit  annotation directly on the field. We noticed that in some Spring boot versions this annotation does not work with constructor binding,  the default unit takes precedence. A workaround is to use setters.

@ConfigurationProperties(prefix = "merchantaccount")
public class MerchantAccount {
    @DurationUnit(ChronoUnit.SECONDS)
    private Duration readTimeout;
    //other fields
}

Coupling value and unit

This is more readable. In the properties file, we can append the unit symbols to the value. Let us reset our read timeout and session timeout values to seconds. We do this by appending ‘s’ to their values in our configuration file as indicated below.

merchantaccount:
    sessionTimeout: 15s
    readTimeout: 10s

The supported units are:

  • ns for nanoseconds
  • us for microseconds
  • ms for milliseconds
  • s for seconds
  • m for minutes
  • h for hours
  • d for days

Data size Conversion

Spring also supports DataSize property binding. The default unit type of byte. However, we can override this default as we did in the duration data type by using either @DataSizeUnit or couple the value and its unit on the configuration file. Let us define data size properties as indicated below.

@ConfigurationProperties(prefix = "merchantaccount")
public class MerchantAccount {
  private  DataSize bufferSize;
  private DataSize threshold;
   
  public DataSize getBufferSize() {
   return bufferSize;
  }

  public void setBufferSize(DataSize bufferSize) {
   this.bufferSize = bufferSize;
  }

  public DataSize getThreshold() {
   return threshold;
  }

  public void setThreshold(DataSize threshold) {
   this.threshold = threshold;
  }
   //  setters and getter of other fields
}

We then specify the values in the configuration file.

merchantaccount:
  bufferSize: 1
 threshold: 200

When our application is executed, the buffer size and threshold size will be 1 byte and 200 bytes respectively. Now lets us override this default unit type and ensure that the buffer size is set to 1 gigabyte.

@DataSizeUnit annotation

We apply this annotation directly to the property field as indicated below.

@ConfigurationProperties(prefix = "merchantaccount")
public class MerchantAccount {

   @DataSizeUnit(DataUnit.GIGABYTES)
    private  DataSize bufferSize;
  
    //getters and setters
}

Now our buffer size will be set to 1 Gigabyte (1GB).

Coupling value and unit

We can append the unit type on the value specified in our configuration file. The unit types supported include:

  • B for bytes
  • KB for kilobytes
  • MB for megabytes
  • GB for gigabytes
  • TB for terabytes

In our configuration file let us append the unit type GB so that the buffer size becomes 1GB.

merchantaccount:
  bufferSize: 1GB
  threshold: 200

Properties Validation

Spring validates the @Configuration classes when annotated with JSR-303 javax.validation  constraint annotations. To ensure that this validation works we must add a JSR-303 implementation on our classpath. We then add the constraint annotations to our fields as indicated below.

@ConfigurationProperties(prefix = "merchantaccount")
@Validated   
public class MerchantAccount {

   @NotNull
   private final String name;
   //other property fields
   //getters and setters

}

The @Validated annotation is mandatory. Below are options we can choose from to enable validation using this annotation.

  • At the class level on the annotated @ConfigurationProperties class.
  • On the bean method that instantiates the configuration properties class.

We can apply this validation if some of our configuration properties need to be verified and validated before using them. Our application will fail at start-up if we do not declare the merchant name in our configuration file.

Nested Properties

Our nested properties are also validated. It is recommended and also a good practice to also annotate them with @Valid. This will trigger the validation even if there are no nested properties found. 

@ConfigurationProperties(prefix = "merchantaccount")
@Validated   
public class MerchantAccount {
   @NotNull
   private String name;
   @Valid
   private ApiKey apiKey;
   public static class ApiKey{
      @NotEmpty
      public String name;
   }
   //getters and setters
}

Usage of the Java Bean

To work with @ConfigurationProperties beans, you just need to inject them the same way as any other bean. See the example below.

@Component
@Slf4j
public class PaymentServiceImpl implements PaymentService {

   private final MerchantAccount merchantAccount;

   public PaymentServiceImpl(MerchantAccount merchantAccount) {
       this.merchantAccount = merchantAccount;
   }
}

Management

In our application, we can include the spring-boot-actuator dependency in order to view all our @ConfigurationProperties beans. The spring-boot-actuator has an endpoint that exposes these beans and its URL path is ‘/actuator/configprops’.

Conclusion

It recommended to externalise our configuration and if there are many configuration properties. We can group them into a simple Java class and use the @ConfigurationProperties annotation to structure our configuration and make it type-safe.

However, the biggest challenge with externalising configuration is on the part of ensuring that the deployed application runs with the correct configuration. It is important to be careful when setting up an application that uses different property sources for different environments. The sample codes provided in this article are found here.

About SFG Contributor

Staff writer account for Spring Framework Guru

    You May Also Like

    One comment

    1. December 28, 2020 at 7:30 am

      I think it so good

      Reply

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.