5

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 a Validator is considered when binding to a Typed Config implementing Validator. 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 and Validator, 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:
    • ctor binding triggers this method here, and it creates a "Value Object" (not sure what that means in Spring's context)
    • getter/setter, instead, triggers this method here.

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.

nevvermind
  • 3,302
  • 1
  • 36
  • 45

0 Answers0