Loading Spring properties into Immutables and corresponding problems
Introduction
In Spring Boot, handling configuration properties is something that you do all the time. While Spring makes it easy to bind properties to Java classes, getting it to work with Immutables (immutables.github.io) isn’t straightforward and well-documented. There’s not so much information out there, and some of what you can find is outdated.
In this article I would like to give you some simple examples of loading Spring properties into Immutables-based classes and show what problems you can encounter.
I would like to jump ahead a bit and say that I wouldn’t recommend using Immutables for loading and storing Spring properties, especially if your goal is to have an immutable configuration class. Java Records or Lombok are much better suited for achieving this (if you are targeting to reduce boilerplate code).
NOTE: In the examples, Spring version is
3.4.3
, Immutables version is2.10.1
and Google Guava version is33.4.0-jre
(I will highlight cases, where Guava is present in the classpath)
Main issues and blockers
We will use this yaml properties file for all the examples in the section:
list-property:
- item1
- item2
- item3
Problem 1
By default, generated private constructors for @Value.Immutable
don’t have any nullability checks or immutability guarantees.
Therefore, for the next class:
@Value.Immutable
@ConfigurationProperties
public interface ConfigPropertiesExample1 {
List<String> listProperty();
}
The generated constructor will be:
private ImmutableConfigPropertiesExample1(List<String> listProperty) {
this.listProperty = listProperty;
}
And this constructor is used by Spring to inject properties. Spring injects ArrayList
which is
mutable, so you can change the content after you obtain the objects using the corresponding getter method.
Problem 2
If we enforce Immutables to create a constructor with nullability checks and immutability guarantees, then List
or Set
fields are backed by Iterable
, so for the next class:
@Value.Style(of = "new", allParameters = true)
@Value.Immutable
@ConfigurationProperties
public interface ConfigPropertiesExample2 {
List<String> listProperty();
}
The generated constructor will be:
public ImmutableConfigPropertiesExample2(Iterable<String> listProperty) {
this.listProperty = createUnmodifiableList(false, createSafeList(listProperty, true, false));
}
But Spring can’t inject properties into Iterable
parameters, so the application startup fails with:
Failed to bind properties under '' to denshchikov.dmitry.app.ImmutableConfigPropertiesExample2:
Reason: java.lang.NullPointerException: Cannot invoke "java.lang.Iterable.iterator()" because "iterable" is null
Problem 3
If we enforce Immutables to use List
instead of Iterable
by utilizing @Value.Default
and
strictBuilder = true
, then the generated constructor again doesn’t have immutability guarantees.
For the next class:
@Value.Immutable
@Value.Style(of = "new", allParameters = true, strictBuilder = true)
@ConfigurationProperties
public interface ConfigPropertiesExample3 {
@Value.Default
@SuppressWarnings("immutables:untype")
default List<String> listProperty() {
return List.of();
}
}
The generated constructor is:
public ImmutableConfigPropertiesExample3(List<String> listProperty) {
this.listProperty = Objects.requireNonNull(listProperty, "listProperty");
}
And we again can change the content of the list that will be injected by Spring.
Problem 4
If we enforce Immutables to use List
instead of Iterable
by utilizing
builtinContainerAttributes = false
, then the generated constructor again doesn’t have immutability guarantees.
Original class:
@Value.Immutable
@Value.Style(of = "new", allParameters = true, builtinContainerAttributes = false)
@ConfigurationProperties
public interface ConfigPropertiesExample4 {
List<String> listProperty();
}
Generated constructor:
public ImmutableConfigPropertiesExample4(List<String> listProperty) {
this.listProperty = Objects.requireNonNull(listProperty, "listProperty");
}
And we again can change the content of the list that will be injected by Spring.
Problem 5
If we use Google Guava and thereby enforce Immutables to use ImmutableList
, then the generated constructor
guarantees immutability.
So, for the next class:
@Value.Immutable
@ConfigurationProperties
public interface ConfigPropertiesExample5 {
List<String> listProperty();
}
The generated constructor is:
private ImmutableConfigPropertiesExample5(ImmutableList<String> listProperty) {
this.listProperty = listProperty;
}
But Spring can’t inject properties into Guava’s immutable collections, so the application startup fails with:
Failed to bind properties under 'list-property' to com.google.common.collect.ImmutableList<java.lang.String>:
Property: list-property[2]
Value: "item3"
Origin: class path resource [application.yaml] - 7:5
Reason: failed to convert java.util.ArrayList<?> to com.google.common.collect.ImmutableList<java.lang.String> (caused by java.lang.InstantiationException)
Key takeaways so far
With Immutables it’s impossible to get an immutable configuration class without some workarounds if you at some point want to have
List
or Set
fields.
You can perform a small workaround and use array as a type for injected field and then utilize
@Value.Derived
to eagerly compute a field with a desired type during construction:
@Value.Immutable
@ConfigurationProperties
public interface ConfigPropertiesExample6 {
String[] listPropertyAsArray();
@Value.Derived
default List<String> listProperty() {
return List.of(listPropertyAsArray());
}
}
It’s a bit awkward, but it works. The properties file in this case will be:
list-property-as-array:
- item1
- item2
- item3
You may also want to utilize @Value.Redacted
and @Value.Auxiliary
to hide the unnecessary attribute.
Examples of successful configuration classes and corresponding properties files
Below I will provide two examples of configuration classes that contain various different fields along with their corresponding yaml files. The first one will be a fully mutable configuration class. And the second one is immutable (but of course, with some workarounds that we discussed).
Mutable configuration class
Below we utilize @Value.Modifiable
, so the generated class will have setters that will be used by Spring for
properties binding. In comparison with approach from Problem 1 where we used @Value.Immutable
, here
you can change all the fields since you have all the setters.
@Value.Modifiable
@Value.Style(strictBuilder = true)
@ConfigurationProperties
public interface ConfigPropertiesExample7 {
Integer integerProperty();
String stringProperty();
@Value.Default
default List<String> listProperty() {
return new ArrayList<>();
}
@Value.Default
default Set<String> setProperty() {
return new HashSet<>();
}
Map<String, String> mapProperty();
Map<String, List<String>> multiMapProperty();
}
integer-property: 192
string-property: "abc"
list-property:
- item1
- item2
- item3
set-property:
- item1
- item2
- item3
map-property:
key1: value1
key2: value2
multimap-property:
key1:
- value1
- value2
key2:
- value3
- value4
Immutable configuration class
Here I would like to highlight that you don’t need a special workaround to parse Map<String, List<String>>
because the
generated constructor doesn’t utilize Iterable
in this case and Spring can successfully inject properties.
@Value.Immutable
@ConfigurationProperties
public interface ConfigPropertiesExample8 {
Integer integerProperty();
String stringProperty();
String[] listPropertyAsArray();
String[] setPropertyAsArray();
@Value.Derived
default List<String> listProperty() {
return List.of(listPropertyAsArray());
}
@Value.Derived
default Set<String> setProperty() {
return Set.of(setPropertyAsArray());
}
Map<String, String> mapProperty();
Map<String, List<String>> multiMapProperty();
}
integer-property: 192
string-property: "abc"
list-property-as-array:
- item1
- item2
- item3
set-property-as-array:
- item1
- item2
- item3
map-property:
key1: value1
key2: value2
multimap-property:
key1:
- value1
- value2
key2:
- value3
- value4
Conclusion
Using Immutables-based classes for binding Spring properties is probably not the best choice due to the limitations
that we discussed in Main issues and blockers. But it’s still possible in case if you want
to follow a project style. Especially if you can avoid using List
or Set
fields. Or if it’s fine for you to use
the array type as a mediator.
There are plenty of ways to arrange Immutables’ annotation attributes, and you can play a bit more to find the one that suits you best. But the main limitations are still there, and you unfortunately can’t avoid them by just using the annotation attributes.
You can find the full project in my repository: https://github.com/DmitryDenshchikov/spring-properties-to-immutables-demo
It contains all the configuration classes and the properties file. You can enable them by uncommenting necessary ones
in the @EnableConfigurationProperties
class and uncommenting them in the main method (for being able to see the
loaded content).