Properties Binding with Spring: Simplify Configuration with @ConfigurationProperties
1 CommentLast 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-safety | YES | NO |
Relaxed-binding | YES | NO |
Meta-data support | YES | NO |
SpEL Evaluation | NO | YES |
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/Setting | Name of property | Data type of property value | Property type |
Merchant Account | merchantaccount | key/value(Map) | Object |
name | String | ||
username | String | ||
code | String | ||
number | Number | ||
currency | String |
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 Java
class 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.
Nahom
I think it so good