Конфигурация @ConstructorBinding не запускает проверку при реализации интерфейса валидатора

#spring-boot #spring-validator #spring-properties

Вопрос:

Я использую spring-boot-starter-parent версию 2.4.4 .

Я экспериментирую с проверкой типизированной конфигурации с использованием @ConstructorBinding и без.

Вот типизированная конфигурация, которая проверяется и правильно останавливает запуск приложения. Это не использование @ConstructorBinding , а стиль геттера/сеттера:

   @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!");
      }
    }
  }
 

Веб-приложение настроено @ConfigurationPropertiesScan так, чтобы все работало. При запуске, как и ожидалось, я получаю это:

 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!
 

Здорово.
Но я фанат инъекции конструктора, и я хотел переработать это в использование @ConstructorBinding . Давайте сделаем это:

   @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.

Это, вероятно, связано с временем создания компонента- ctor рано, сеттер позже. Тем не менее, на данный момент это просто любопытство.

Любые идеи, как я могу сделать типизированную конфигурацию, привязанную к конструктору, для проверки с помощью Validator ? Как бы вы поступили вместо этого, если бы это был не тот путь?

Спасибо, что прочитали.