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) bytes
  • DataUnit.MEGABYTES means 1048576 (220) bytes
  • DataUnit.GIGABYTES means 1073741824 (230) bytes
  • DataUnit.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 sizeX 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 the AppProperties 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 default DataUnit.KILOBYTES is used)
  • 4 Megabytes if you configure: app.buffer-size = 4MB
    (the default DataUnit.KILOBYTES is ignored)

Similar Posts