Spring Boot 2.2.6, Spring Cloud Greenwich.RELEASE (I have since realized that I should be using the Hoxton branch with 2.2.x, which I have upgraded to now)
Following the suggestion from https://stackoverflow.com/questions/57811421/how-to-validate-request-parameters-on-feign-client/61635351#61635351, I can successfully use a validated Feign interface like this:
@Validated
@FeignClient( name = "gps-service", path = "${infrastructure.base-path.gps-service}" )
public interface GpsClient
{
@PostMapping( value = "/files/gpslogs" )
GpsLogFileResult triggerGpsLogFile( @RequestBody @Valid @NotNull GpsLogFileParameters parameters );
}
That is, this works most of the time. Very rarely, but then seemingly in bursts, the call to triggerGpsLogFile
fails due to an exception like shown below. This application runs in a Kubernetes cluster and there are usually many thousands of calls to this method per day without the exception. Then one day, there are suddenly around 100 cases of the ConstraintDeclarationException
within a time span of 10 minutes, only to be gone again afterwards.
javax.validation.ConstraintDeclarationException: HV000152: Two methods defined in parallel types must not declare parameter constraints, if they are overridden by the same method, but methods $Proxy268#triggerGpsLogFile(GpsLogFileParameters) and GpsClient#triggerGpsLogFile(GpsLogFileParameters) both define parameter constraints.
at org.hibernate.validator.internal.metadata.aggregated.rule.ParallelMethodsMustNotDefineParameterConstraints.apply(ParallelMethodsMustNotDefineParameterConstraints.java:23)
at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.assertCorrectnessOfConfiguration(ExecutableMetaData.java:461)
at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.build(ExecutableMetaData.java:377)
at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataImpl$BuilderDelegate.build(BeanMetaDataImpl.java:788)
at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataImpl$BeanMetaDataBuilder.build(BeanMetaDataImpl.java:648)
at org.hibernate.validator.internal.metadata.BeanMetaDataManager.createBeanMetaData(BeanMetaDataManager.java:204)
at org.hibernate.validator.internal.metadata.BeanMetaDataManager.getBeanMetaData(BeanMetaDataManager.java:166)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:265)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:233)
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:105)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy269.triggerGpsLogFile(Unknown Source)
at com.acme.application.service.GpsService.triggerGpsLogFile(GpsService.java:227)
GpsService.triggerGpsLogFile()
makes the call to GpsClient.triggerGpsLogFile()
.
Having read the relevant Hibernate Validator documentation and the source code of ParallelMethodsMustNotDefineParameterConstraints
, I understand that Bean Validation method parameter annotations on overridden/implementing methods may pose a problem.
And as @wilkinsona pointed out at https://github.com/spring-projects/spring-boot/issues/17000, @Validated
is realized by dynamic proxies (which can also be seen in the stacktrace above). So I kind of see why this could be a problem. But then I don't understand why the vast majority of calls work correctly, also throwing a ConstraintViolationException
(note: Violation <-> Declaration) if applicable.
I couldn't find any official documentation about using @Validated
on a @FeignClient
, maybe that's for a reason and what I'm doing is basically unsupported?
Comment From: jhoeller
There seems to be a Hibernate configuration property hibernate.validator.allow_parallel_method_parameter_constraint
which can be set to true
. I'm not sure where "parallel" methods (with a subtype method implementing two distinct method declarations from unrelated interfaces) come from here but I suppose that property would help for a start.
With respect to your scenario specifically, the JVM could potentially use different method handles for the proxy invocation, e.g. sometimes the interface method handle and sometimes the proxy class method handle. Maybe Hibernate Validator misidentifies a proxy class method handle as a parallel method hierarchy? Might be worth reporting to them as well.
Comment From: jo-ka
Thanks for your reply! I will test that configuration property and see how it goes (for some time, to be sure). I'll report back in a few weeks then. In addition, I'll raise an issue with Hibernate Validator.
Comment From: jo-ka
Sorry for asking a potentially trivial question, but how could I set that property in my application.properties
? Or would I need to use the programmatic bootstrapping for that (Hibernate Validator docs)?
Comment From: jhoeller
At the core level, we got a setValidationProperties
method on LocalValidatorFactoryBean
. @snicoll is there a straightforward way to set such validation properties in Boot, or would it simply require a custom LocalValidatorFactoryBean
definition?
Comment From: snicoll
@snicoll is there a straightforward way to set such validation properties in Boot, or would it simply require a custom LocalValidatorFactoryBean definition?
At this time, a custom LocalValidatorFactoryBean
is required. Our definition is pretty straightforward.
Comment From: jo-ka
Thanks again to both of you for helping.
I was thinking that I'd like to keep the current Boot autoconfiguration (for one, so that the application behaves the same, and also so that any later modifications to the autoconfiguration will automatically become available to the application, too).
So I first tried to inject the LocalValidatorFactoryBean
, apply the property there and return that as my own @Bean LocalValidatorFactoryBean
. But that obviously failed due to a circular reference (BeanCurrentlyInCreationException
). So instead I came up with this:
@Configuration
public class ValidatorConfig
{
@Bean
public LocalValidatorFactoryBean validatorAllowingParallelMethodParameterConstraints()
{
LocalValidatorFactoryBean defaultValidator = ValidationAutoConfiguration.defaultValidator();
Properties properties = new Properties();
properties.put( HibernateValidatorConfiguration.ALLOW_PARALLEL_METHODS_DEFINE_PARAMETER_CONSTRAINTS, "true" );
defaultValidator.setValidationProperties( properties );
return defaultValidator;
}
}
Since ValidationAutoConfiguration
is not in an "internal" package and I do want to hook into the autoconfiguration mechanism, it seems kind of okay to me to directly call ValidationAutoConfiguration.defaultValidator()
. Of course, I trade benefitting from later updates to the autoconfiguration for dependency on the API like that.
Comment From: jhoeller
A quick update: We'll try to do something about this by default.
Could you please confirm which JDK you're running on there? Is it by any chance >8 in which case it might be a regression that happens only on newer JDKs (while working reliably on 8)? Or are you seeing the effect on JDK 8 as well?
Comment From: jo-ka
Those exceptions always occured on JDK 11. Unfortunately, I can't tell if it already happens with JDK 8 because we've been using 11 for a while already. And I'm afraid that trying to reproduce it now with 8 would prove to be difficult because it only happened so rarely (and that particular application requires 11+ anyway).
Comment From: jhoeller
Thanks for the quick reply, that's very helpful for a start. We are seeing similar exceptions on our JDK 15 CI build at the moment, never seen on JDK 8 CI before, so it looks like a JDK 11+ regression to me.
Comment From: jo-ka
My last comment wasn't correct: In fact, we've already been using JDK 15 as runtime environment for a couple of weeks, too. I was under the impression that the exceptions may have occured before we made the move from 11 to 15, but actually this is already outside our log retention period of 30 days.
So at this point, I cannot say if it was introduced with 11 or 15 (let alone 8), but we're now witnessing it with 15, too.
Comment From: jhoeller
After debugging this down to the level of Class.isAssignableFrom
in HV's MethodConfigurationRule.isDefinedOnParallelType
having to misfunction in order for this to happen, it turns out that this is indeed a JVM bug that has been fixed for the yet-to-be-released JDK 15.0.2: https://bugs.openjdk.java.net/browse/JDK-8253566
From that perspective, we won't try to work around it in our code (and it seems we cannot anyway). Thanks for your help in narrowing this!
Comment From: jhoeller
BTW here's the Hibernate Validator ticket for it: https://hibernate.atlassian.net/browse/HV-1801?focusedCommentId=107317
Apparently there's a workaround: adding the JVM flags -XX:+UnlockDiagnosticVMOptions -XX:+ExpandSubTypeCheckAtParseTime
Comment From: abmjunaed
I am having the same issue with openjdk:16-jdk-alpine in my docker image.