This was raised by Philipp Paland on Gitter when trying to use records for @ConfigurationProperties. I believe it will apply to any final @ConfigurationProperties class that has @Validated on it.
While we don't require a proxy to perform configuration property validation, MethodValidationPostProcessor finds @Validated on the bean and tries to create a proxy. When the configuration property class is final, this fails. You can work around the problem by excluding ValidationAutoConfiguration and just declaring a validator:
package com.example.recordvalidation;
import javax.validation.constraints.NotBlank;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.validation.MessageInterpolatorFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@ConfigurationPropertiesScan
@SpringBootApplication(exclude = org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration.class)
public class RecordValidationApplication {
public static void main(String[] args) {
SpringApplication.run(RecordValidationApplication.class, args);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public static LocalValidatorFactoryBean defaultValidator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
@ConfigurationProperties(prefix = "myprops")
@ConstructorBinding
@Validated
public record MyProps(@NotBlank String baseUrl) {}
}
This retains configuration property validation at the cost of method validation anywhere in the app. It would be nice if we could somehow keep both and just have the final @ConfigurationProperties class excluded from method validation.
Comment From: philipp-paland
Thanks for providing the workaround :)
Comment From: snicoll
After some brainstorming with @jhoeller, it looks like we unfortunately created an additional use-case for @Validated that overlaps with the ones of the core framework, that is:
@Validatedat class-level to trigger method validation (the post-processor is optional but auto-configured in Spring Boot)@Validatedat parameter-level to trigger validation, typically to validate input arguments in MVC
Our binder looks for @Validated at class-level and triggers validation that looks more like the second use case and definitely not the first one.
There are a few workarounds to be considered:
- Use a
@ConfigurationProperties-specific way to trigger validation (breaking change) - Auto-configure an extension of
MethodValidationPostProcessorthat overridesisEligibleand ignores@ConfigurationProperties-annotated types by default
Comment From: snicoll
We've decided to add a validated attribute to @ConfigurationProperties and recommend users to move to that. That won't be a breaking change as we'd be still looking at @Validated (perhaps in a deprecated fashion?)
Comment From: wilkinsona
My recollection is that we discussed that approach, but decided against it as it would be inconsistent with how users expect to enable validation by using @Validated. Instead, I think we decided to look at introducing an extension of MethodValidationPostProcessor that can ignore ConfigurationProperties-annotated types. To avoid the post-processor having knowledge of @ConfigurationProperties, @philwebb was keen to have come sort of pluggable filtering mechanism.
Comment From: philwebb
My recollection is the same as @wilkinsona
Comment From: Thrillpool
Noting for the convenience of people googling errors, as I just spent a couple of hours rediscovering this issue (or rather something similar) and solution, at which point I immediately found this github issue. this error may manifest as
Cause: java.lang.ClassCastException: class SomeClassOfYours$$SpringCGLIB$$0 cannot be cast to class org.springframework.cglib.proxy.Factory (SomeClassOfYours$$SpringCGLIB$$0 and org.springframework.cglib.proxy.Factory are in unnamed module of loader 'app')
And the same workaround resolves things