I'm using spring-boot-starter-parent
version 2.4.4
.
I'm experimenting with validation on Typed Configuration using @ConstructorBinding
and without.
Here's a Typed Config that gets validated and correctly stops the app from running. It's not using @ConstructorBinding
, but the getter/setter style:
@ConfigurationProperties("app")
@Validated
@Getter
@Setter
public static class AppProperties implements org.springframework.validation.Validator {
private int someProp = 10;
@Override
public boolean supports(Class<?> aClass) {
return aClass.isAssignableFrom(getClass());
}
@Override
public void validate(Object o, Errors errors) {
AppProperties appProperties = (AppProperties) o;
if (appProperties.getSomeProp() < 100) {
errors.rejectValue("someProp", "code", "Number should be greater than 100!");
}
}
}
The webapp is set with @ConfigurationPropertiesScan
so it all works. When running, as expected, I get this:
Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'app' to com.example.demo.DemoApplication$AppProperties failed:
Property: app.someProp
Value: 10
Reason: Number should be greater than 100!
Great.
But I'm a fan of constructor injection, and I wanted to refactor this into using @ConstructorBinding
. Let's do it:
@ConfigurationProperties("app")
@ConstructorBinding // !!!
@Validated
@Getter
// @Setter - no setters!
public static class AppProperties implements org.springframework.validation.Validator {
private int someProp = 10;
// constructor!
public AppProperties(int someProp) {
this.someProp = someProp;
}
// same validation as above
}
This time, the app starts. No errors. I was expecting the app NOT to run, and for me to get the same startup error, because the injected someProp
is 0
(I haven't declared it anywhere), so clearly invalid according to my validation. If I debug the AppProperties
, the someProp
member is indeed 0
.
I did some research, but I'm too big a Spring noob to figure out why. Here's some of my findings:
- although I agree with the Spring devs that a
org.springframework.validation.Validator
should be a standalone object, meant to validate many objects, the matter of fact is that aValidator
is considered when binding to a Typed Config implementingValidator
. See here: GitHub link. That code says "If your class implements Validator, I'm going to use a special binder to when creating your bean, which will validate it". - using the getter/setter style, you get two validators from
org.springframework.boot.context.properties.ConfigurationPropertiesBinder#getValidators()
- JSR-303 andValidator
, while using@ConstructorBinding
gets you one - only JSR-303. Why is that? Well, because the target - my Typed Config - has no value yet when using the constructor binding: see here the condition. - related to the above, on ctor binding, this condition is false, thus bypassed: condition bypassed.
- depending on strategy, the bean definitely gets created differently:
This probably relates to the creation-time of the bean - ctor is early, setter is later. Still, at this point, this is just curiosity.
Any ideas how can I make the a constructor-bound typed configuration to be validated using the Validator
? How would you do it instead, if this isn't the way to go?
Thanks for reading.