Spring Boot: configuring data size values
In Spring Boot applications, it’s not uncommon to configure a value representing a size in bytes, for example, a buffer size, a file size limit, etc. In this article, I will describe how to use the DataSize
class provided by Spring Framework to configure and handle data size values.
Introduction
Spring Framework currently provides two types related to data sizes in bytes:
org.springframework.util.unit.DataUnit
(an enumerated type)org.springframework.util.unit.DataSize
(a final class)
These types are part of the spring-core
artifact and were introduced in Spring Framework 5.1. Consequently, they are also available since Spring Boot 2.1.0 (but not in 2.0.x versions). You can read about this new feature in the issue Introduce DataSize type [SPR-17154] #21691.
Specifically for Spring Boot, since version 2.1.0, there is also the type org.springframework.boot.convert.DataSizeUnit
(an annotation), which I will cover later in this article.
Before continuing, it should be noted that this article mainly focuses on Spring Boot, not Spring Framework, even if some concepts belong to the “pure” Spring Framework.
The DataUnit enumerated type
DataUnit
is a simple enum
that declares five data size units: BYTES
, KILOBYTES
, MEGABYTES
, GIGABYTES
, and TERABYTES
. All these units represent byte sizes in terms of powers of two. In practice:
DataUnit.KILOBYTES
means 1024 (210) bytesDataUnit.MEGABYTES
means 1048576 (220) bytesDataUnit.GIGABYTES
means 1073741824 (230) bytesDataUnit.TERABYTES
means 1099511627776 (240) bytes
There is, unfortunately, very little that you can do with DataUnit
. You can use the static fromSuffix
method to obtain the DataUnit
instance from a suffix string (one of "B"
, "KB"
, "MB"
, "GB"
, and "TB"
). The string matching is case-sensitive. Thus, for example:
DataUnit unit = DataUnit.fromSuffix("MB"); // unit == DataUnit.MEGABYTES
Any unknown suffix causes a java.lang.IllegalArgumentException
at runtime.
The DataSize class
DataSize
is a simple “value” class containing a single primitive long
value representing a data size in bytes. This class is final
and implements Comparable<DataSize>
and Serializable
. In addition, DataSize
objects are immutable and thus intrinsically thread-safe.
Since there is no publicly accessible constructor, the only way to obtain a DataSize
object is through the “of” and “parse” static methods. For example:
DataSize size1 = DataSize.ofBytes(4194304);
DataSize size2 = DataSize.ofKilobytes(4096);
DataSize size3 = DataSize.ofMegabytes(4);
DataSize size4 = DataSize.of(4, DataUnit.MEGABYTES);
DataSize size5 = DataSize.parse("4MB");
All the above size
X
objects represent a data size of 4194304 bytes and are all equal in terms of the equals(Object)
method.
Parsing with parse methods
DataSize
provides two static parse
methods:
public static DataSize parse(CharSequence text)
public static DataSize parse(CharSequence text, DataUnit defaultUnit)
The second version accepts a default DataUnit
, which can be null
. Remember that the default unit is only used when the input string does not contain an explicit unit. The following examples should clarify the usage.
DataSize size1 = DataSize.parse("4"); // 4 bytes
DataSize size2 = DataSize.parse("4KB"); // 4096 bytes
DataSize size3 = DataSize.parse("4", DataUnit.KILOBYTES); // 4096 bytes
DataSize size4 = DataSize.parse("4MB", DataUnit.KILOBYTES); // 4194304 bytes (default unit ignored!)
DataSize
performs the parsing in the following way: firstly, the numeric part is parsed as a primitive long
value; the unit (if present) must be precisely one of "B"
, "KB"
, "MB"
, "GB"
, "TB"
, and it is case-sensitive. The following examples clarify what is accepted and what is not.
DataSize.parse("128") // ✔️ OK
DataSize.parse("128KB") // ✔️ OK
DataSize.parse("128Kb") // ❌ IllegalArgumentException
DataSize.parse("128.0KB") // ❌ IllegalArgumentException
DataSize.parse("128PB") // ❌ IllegalArgumentException
NOTE:
Starting from Spring Framework 5.3.22 (Spring Boot ≥ 2.7.2), the parsing is more “lenient” regarding whitespaces (see the v5.3.22 Release Notes). With this change, the
parse
method ignores leading, trailing, and inner whitespaces. It means that the following cases are also accepted:DataSize.parse("128 KB") // ✔️ OK DataSize.parse(" 128KB ") // ✔️ OK DataSize.parse(" 128 KB ") // ✔️ OK
Handling negative data sizes
Negative data sizes (values < 0) are rarely helpful, but despite this, they are supported by the DataSize
class. So, the following are technically valid:
DataSize size1 = DataSize.ofKilobytes(-128);
DataSize size2 = DataSize.parse("-128KB");
You can use the isNegative()
method to check if a DataSize
object represents a negative value.
Binding data size configuration values
The real usefulness of DataSize
becomes more evident when you need to configure a data size property in your application. Spring Boot provides, by default, two converters named NumberToDataSizeConverter
and StringToDataSizeConverter
. These converters (especially the second) are used when you need to inject a data size property using two common techniques in Spring Boot, the @Value
and the @ConfigurationProperties
annotations. I am going to show both cases in the following sections.
Injecting a data size property with @Value
@Value
is a Spring annotation you can apply to fields or method/constructor parameters to inject a value using Spring Expression Language (#{……}
) or property placeholders (${……}
). Consider, for example, the following simple service:
package your.pkgname;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.unit.DataSize;
@Service
public class MyService {
@Value("${app.buffer-size}")
DataSize bufferSize;
// .......
}
Then in your application.properties
(or YAML equivalent), you can place the following:
app.buffer-size = 64KB
And the bufferSize
field will contain a DataSize
object representing the value of 65536 bytes.
The property placeholder expression can also specify a default value that is used when the property is missing, for example:
@Value("${app.buffer-size:16KB}")
DataSize bufferSize;
Note that if the value is blank in the configuration file, e.g.:
app.buffer-size =
Then a null
is injected into the bufferSize
field, even if there is a default value in the @Value annotation.
Using the DataSizeUnit annotation
You can also place another helpful annotation on the field (or parameter), the @DataSizeUnit
annotation. It provides the default unit, precisely like the defaultUnit
parameter of the DataSize.parse
method. For example:
@Value("${app.buffer-size}")
@DataSizeUnit(DataUnit.KILOBYTES)
DataSize bufferSize;
Then you can configure:
# means 4096 bytes
app.buffer-size = 4
or
# means 4194304 bytes (DataUnit.KILOBYTES is ignored)
app.buffer-size = 4MB
Injecting a data size property with @ConfigurationProperties
Spring Boot provides an alternative way to capture externalized configuration values using the @ConfigurationProperties
annotation. However, beware that some differences exist from the @Value
annotation, as described in the official documentation @ConfigurationProperties vs. @Value.
First, when you want to use @ConfigurationProperties
, you create a “bean” class containing one or more properties matching the configured property names. For example:
package your.pkgname;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.unit.DataSize;
@ConfigurationProperties("app")
public class AppProperties {
private DataSize bufferSize;
public DataSize getBufferSize() {
return bufferSize;
}
public void setBufferSize(DataSize bufferSize) {
this.bufferSize = bufferSize;
}
}
To make AppProperties
a Spring “bean”, there are two main options:
- applying the
@Component
annotation on theAppProperties
class - applying the
@EnableConfigurationProperties({AppProperties.class})
on a@Configuration
class or the main class with the@SpringBootApplication
annotation
Then AppProperties
becomes a Spring “bean”, and you can inject it like any other bean using, for example, the @Autowired
annotation.
The field can also use the @DataSizeUnit
annotation previously described; you can even apply a default value by initializing the field. Thus, if you use, for example:
@ConfigurationProperties("app")
public class AppProperties {
@DataSizeUnit(DataUnit.KILOBYTES)
private DataSize bufferSize = DataSize.ofKilobytes(16);
// ....
}
You will have, for example, the following results:
- 16 Kilobytes if the
app.buffer-size
property is missing - 4 Kilobytes if you configure:
app.buffer-size = 4
(the defaultDataUnit.KILOBYTES
is used) - 4 Megabytes if you configure:
app.buffer-size = 4MB
(the defaultDataUnit.KILOBYTES
is ignored)