Looking for potential support for constructing configuration properties on types that utilise the builder pattern.
Background
Currently, Spring Boot supports binding @ConfigurationProperties via constructor injection and via getter/setter injection.
In the old Java world, this worked really well. I could use Lombok with this to set defaults for when fields are not provided in the properties and Spring would know to automatically use their default values if nothing was set, for example:
@ConfigurationProperties("database")
@lombok.Data
@Validated
public class DatabaseProperties {
@NotBlank
private String username;
@NotBlank
private String password;
// Default to 30s if unset
@NotNull
@DurationMin(seconds = 5)
private Duration timeout = Duration.ofSeconds(30);
}
In the new Java world, where we use tools such as Uber's NullAway alongside ErrorProne to validate JSpecify nullability annotations as part of compilation, this becomes a little more clunky.
This existing approach poses the following issues:
- In the above case, for null safety, we would have to either use null-unmarked on the class (which is dirty), or explicitly add
@Nullable to each attribute, due to the way this data is late-bound. This isn't great as this forces us to do useless runtime checks once we know the class has been correctly constructed, leading to wasteful boilerplate.
- When we get Project Valhalla in JDK's mainline release shortly, we'll also get null-constrained types which would likely be incompatible with this mechanism of late-binding defaults.
- Data is not immutable, so disallows enforcing consistent state as a compile-time contract.
Another option is to utilise constructor injection (either on a @lombok.Value class, or via record types. This mostly solves the issue of knowing that "late bound" values are not null. This introduces several new issues as a result:
-
Java lacks named default parameters. This makes it very difficult to have default values when parameters are not specified in the configuration property source.
-
Testing becomes clunky, as we now have a dependency on the argument order when initialising properties in unit tests via object instantiation. Likewise we have to always explicitly add the sane defaults. A mix of large numbers of properties, order dependency, and defaults being set at the use site results in hard-to-maintain code.
-
Inheritance is no longer sensible without overloading each constructor.
-
Inheritance is not possible with records due to the lack of ability to extend a class. Interface use would result in clunky code duplication.
-
I can no longer initialize configuration properties via bean methods, which has always been very useful as it allows me to separate the spring wiring concern from the model itself:
@Configuration
public class DatabaseConfig {
@Bean
@ConfigurationProperties("database")
DatabaseProperties oldWay() {
return new DatabaseProperties();
}
@Bean
@ConfigurationProperties("database")
DatabaseProperties newWay() {
return new DatabaseProperties(/* welp, i have to provide the properties here for this to compile... so not possible now */);
}
}
Proposal
Builder support in configuration properties
I would like to be able to see configuration properties get constructed from a builder type where possible. This can work in a similar way to Lombok's Jackson support whereby a sensible default is picked based on annotations.
For example, using Lombok:
@ConfigurationProperties(
name = "database",
builderMethod = "builder"
)
@lombok.Builder
@lombok.Value
@Validated
public class DatabaseProperties {
...
}
or using the Immutables annotation processor:
@ConfigurationProperties(
name = "database",
builderMethod = "builder"
)
@Value.Immutable
@Validated
public interface DatabaseProperties {
String getUserName();
String getPassword();
...
static ImmutableDatabaseProperties.Builder builder() {
return ImmutableDatabaseProperties.builder();
}
}
Bean-friendly definitions
To solve the issue of Bean methods late-initializing, we could allow returning builders with @ConfigurationProperties directly.
@Configuration
public class DatabaseConfig {
@Bean
@ConfigurationProperties("database")
public DatabaseProperties.Builder databaseProperties() {
return DatabaseProperties.builder();
}
@Bean
public SomeRepository repository(DatabaseProperties props) {
...
}
@Bean
public SomeRepository repository() {
return new SomeRepository(databaseProperties().build());
}
}
In the case of returning a builder, I'd expect Spring to wrap the type in a CGLIB proxy that rejects mutation of the state in my own code. The .build() method could be intercepted by CGLIB to cache the built value after the first call, mirroring how method proxying works already.
Another alternative for the latter could be a ConfigurationPropertiesProvider type of some description that takes the class to construct as a parameter and acts as a supplier.
@Bean
ConfigurationPropertiesProvider<DatabaseProperties> properties() {
return ConfigurationPropertiesProvider.forClass(DatabaseProperties.class);
// or
return ConfigurationPropertiesProvider.forBuilder(DatabaseProperties::builder);
}
Looking for potential support for constructing configuration properties on types that utilise the builder pattern.
Background
Currently, Spring Boot supports binding
@ConfigurationPropertiesvia constructor injection and via getter/setter injection.In the old Java world, this worked really well. I could use Lombok with this to set defaults for when fields are not provided in the properties and Spring would know to automatically use their default values if nothing was set, for example:
In the new Java world, where we use tools such as Uber's NullAway alongside ErrorProne to validate JSpecify nullability annotations as part of compilation, this becomes a little more clunky.
This existing approach poses the following issues:
@Nullableto each attribute, due to the way this data is late-bound. This isn't great as this forces us to do useless runtime checks once we know the class has been correctly constructed, leading to wasteful boilerplate.Another option is to utilise constructor injection (either on a
@lombok.Valueclass, or via record types. This mostly solves the issue of knowing that "late bound" values are not null. This introduces several new issues as a result:Java lacks named default parameters. This makes it very difficult to have default values when parameters are not specified in the configuration property source.
Testing becomes clunky, as we now have a dependency on the argument order when initialising properties in unit tests via object instantiation. Likewise we have to always explicitly add the sane defaults. A mix of large numbers of properties, order dependency, and defaults being set at the use site results in hard-to-maintain code.
Inheritance is no longer sensible without overloading each constructor.
Inheritance is not possible with records due to the lack of ability to extend a class. Interface use would result in clunky code duplication.
I can no longer initialize configuration properties via bean methods, which has always been very useful as it allows me to separate the spring wiring concern from the model itself:
Proposal
Builder support in configuration properties
I would like to be able to see configuration properties get constructed from a builder type where possible. This can work in a similar way to Lombok's Jackson support whereby a sensible default is picked based on annotations.
For example, using Lombok:
or using the Immutables annotation processor:
Bean-friendly definitions
To solve the issue of
Beanmethods late-initializing, we could allow returning builders with@ConfigurationPropertiesdirectly.In the case of returning a builder, I'd expect Spring to wrap the type in a CGLIB proxy that rejects mutation of the state in my own code. The
.build()method could be intercepted by CGLIB to cache the built value after the first call, mirroring how method proxying works already.Another alternative for the latter could be a ConfigurationPropertiesProvider type of some description that takes the class to construct as a parameter and acts as a supplier.