spring-projects / spring-boot

Spring Boot

Home Page:https://spring.io/projects/spring-boot

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Failure analysis for a BindException with an unexpected root cause (such as ArrayIndexOutOfBoundsException) is unhelpful

sbueringer opened this issue · comments

Hi,

I'm using Spring Boot 2.0.2 with Kotlin 1.2.41. I'm trying to use an enum as ConfigurationProperty. I wrote a minimal example application to reproduce the issue:
https://github.com/sbueringer/springboot-kotlin-enum/

I have the following ConfigurationProperties:

@Configuration
@ConfigurationProperties(prefix = "custom")
@Validated
class Properties {
    var test = TestEnum.VALUE1
}
enum class TestEnum {VALUE1, VALUE2}

In combination with the following application.yaml:

custom:
  test: VALUE2

I get the following exception on startup:

2018-05-25 06:45:21.848  WARN 22061 --- [  restartedMain] onfigReactiveWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'application': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.boot.context.properties.ConfigurationPropertiesBindException: Error creating bean with name 'properties': Could not bind properties to 'Properties' : prefix=custom, ignoreInvalidFields=false, ignoreUnknownFields=true; nested exception is org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'custom.test' to com.example.TestEnum

To reproduce, just clone the repository and startup the SpringBootApplication.

Edit: It seems to work, if I remove the @Validated annotation. Is that intended?

This was closed too fast. After changing the configuration property to lateinit var the error is different. I just came upon this issue while migrating an application to Spring Boot 2.

@Configuration
@ConfigurationProperties(prefix = "custom")
@Validated
class Properties {
    lateinit var test: TestEnum
}

Error:

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to bind properties under 'custom.test' to com.example.TestEnum:

    Property: custom.test
    Value: VALUE2
    Origin: class path resource [application.yaml]:3:9
    Reason: 0

Here's my version of the same repo with this change: https://github.com/crypticmind/springboot-kotlin-enum

@snicoll Please take a look and let me know if I should open a new issue instead.

Running with debug gives:

org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'custom.test' to com.example.TestEnum
        at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:250) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:226) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder.lambda$bindBean$4(Binder.java:334) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:73) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:62) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:54) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder.lambda$null$5(Binder.java:342) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) ~[na:1.8.0_144]
        at java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1351) ~[na:1.8.0_144]
        at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126) ~[na:1.8.0_144]
        at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:498) ~[na:1.8.0_144]
        at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485) ~[na:1.8.0_144]
        at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) ~[na:1.8.0_144]
        at java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:152) ~[na:1.8.0_144]
        at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:1.8.0_144]
        at java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:464) ~[na:1.8.0_144]
        at org.springframework.boot.context.properties.bind.Binder.lambda$bindBean$6(Binder.java:343) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:442) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder$Context.withBean(Binder.java:428) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder$Context.access$400(Binder.java:382) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder.bindBean(Binder.java:340) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:279) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:221) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:210) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:192) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bind(ConfigurationPropertiesBinder.java:82) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:107) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:93) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:422) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1698) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:579) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:501) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:251) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1138) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1065) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:815) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:721) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:192) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1274) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1131) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:541) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:501) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:760) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:869) ~[spring-context-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) ~[spring-context-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:61) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:395) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:327) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at com.example.ApplicationKt.main(Application.kt:13) [main/:na]
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_144]
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_144]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_144]
        at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_144]
        at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-2.0.2.RELEASE.jar:2.0.2.RELEASE]
Caused by: java.lang.ArrayIndexOutOfBoundsException: 0
        at java.util.Arrays$ArrayList.get(Arrays.java:3841) ~[na:1.8.0_144]
        at org.hibernate.validator.internal.metadata.aggregated.ParameterMetaData$Builder.build(ParameterMetaData.java:169) ~[hibernate-validator-6.0.9.Final.jar:6.0.9.Final]
        at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.findParameterMetaData(ExecutableMetaData.java:435) ~[hibernate-validator-6.0.9.Final.jar:6.0.9.Final]
        at org.hibernate.validator.internal.metadata.aggregated.ExecutableMetaData$Builder.build(ExecutableMetaData.java:388) ~[hibernate-validator-6.0.9.Final.jar:6.0.9.Final]
        at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataImpl$BuilderDelegate.build(BeanMetaDataImpl.java:788) ~[hibernate-validator-6.0.9.Final.jar:6.0.9.Final]
        at org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataImpl$BeanMetaDataBuilder.build(BeanMetaDataImpl.java:648) ~[hibernate-validator-6.0.9.Final.jar:6.0.9.Final]
        at org.hibernate.validator.internal.metadata.BeanMetaDataManager.createBeanMetaData(BeanMetaDataManager.java:192) ~[hibernate-validator-6.0.9.Final.jar:6.0.9.Final]
        at org.hibernate.validator.internal.metadata.BeanMetaDataManager.lambda$getBeanMetaData$0(BeanMetaDataManager.java:160) ~[hibernate-validator-6.0.9.Final.jar:6.0.9.Final]
        at java.util.concurrent.ConcurrentMap.computeIfAbsent(ConcurrentMap.java:324) ~[na:1.8.0_144]
        at org.hibernate.validator.internal.metadata.BeanMetaDataManager.getBeanMetaData(BeanMetaDataManager.java:159) ~[hibernate-validator-6.0.9.Final.jar:6.0.9.Final]
        at org.hibernate.validator.internal.engine.ValidationContext$ValidationContextBuilder.forValidate(ValidationContext.java:566) ~[hibernate-validator-6.0.9.Final.jar:6.0.9.Final]
        at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:155) ~[hibernate-validator-6.0.9.Final.jar:6.0.9.Final]
        at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:104) ~[spring-context-5.0.6.RELEASE.jar:5.0.6.RELEASE]
        at org.springframework.boot.context.properties.ConfigurationPropertiesJsr303Validator.validate(ConfigurationPropertiesJsr303Validator.java:52) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler.lambda$validate$1(ValidationBindHandler.java:101) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) ~[na:1.8.0_144]
        at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175) ~[na:1.8.0_144]
        at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948) ~[na:1.8.0_144]
        at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) ~[na:1.8.0_144]
        at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) ~[na:1.8.0_144]
        at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) ~[na:1.8.0_144]
        at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) ~[na:1.8.0_144]
        at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:1.8.0_144]
        at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418) ~[na:1.8.0_144]
        at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler.validate(ValidationBindHandler.java:101) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler.validate(ValidationBindHandler.java:83) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler.onFinish(ValidationBindHandler.java:72) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder.handleBindResult(Binder.java:236) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:223) ~[spring-boot-2.0.2.RELEASE.jar:2.0.2.RELEASE]
        ... 61 common frames omitted

The failure analysis could certainly be better.

The failure occurs when Hibernate Validator is trying to get a name for the 1st (index 0) parameter of the constructor protected com.example.TestEnum(java.lang.String,int). This results in a call to getParameterNames(Constructor<?> constructor) in this code in Spring Framework's LocalValidatorFactoryBean:

configuration.parameterNameProvider(new ParameterNameProvider() {
	@Override
	public List<String> getParameterNames(Constructor<?> constructor) {
		String[] paramNames = discoverer.getParameterNames(constructor);
		return (paramNames != null ? Arrays.asList(paramNames) :
				defaultProvider.getParameterNames(constructor));
	}
	@Override
	public List<String> getParameterNames(Method method) {
		String[] paramNames = discoverer.getParameterNames(method);
		return (paramNames != null ? Arrays.asList(paramNames) :
				defaultProvider.getParameterNames(method));
	}
});

discoverer, a PrioritizedParameterNameDiscoverer, returns a zero-length string array. When Hibernate Validator tries to retrieve the entry at index 0 of the array it fails with the above exception. The empty array comes from the delegate KotlinReflectionParameterNameDiscoverer as its getParameterNames(List<KParameter>) method is called with an empty list.

I'm not sure if this is a Kotlin bug or a Spring Framework bug. Hopefully @sdeleuze can point things in the right direction.

I've opened HV-1638 in case the Hibernate Validator team are interested in making it a bit more resilient to a name provider that returns too few names.

It could be linked to SPR-16931, and the related KT-25165 I have raised. I have added a comment mentioning this possibly new impact for us.

Thank you, @sdeleuze. SPR-16931 and KT-25165 look to be exactly the problem that we're seeing.

Given that those issues have the underlying problem covered, let's use this issue to improve the failure analysis. "Reason: 0" isn't at all helpful. We should perhaps opt out of failure analysis entirely in this situation (if we can).

I'm not sure if this will also happen with 1.5. Assigning to 1.5.x for now at least.

@wilkinsona about not happening in 1.5, are you referring to the underlying issue, or the obscure error message? I haven't had this issue with Kotlin enums in 1.5, and as for the error message, I understand this is generated by the error reporting in the new binding API. Obviously, checking for this in 1.5 would be satisfyingly thorough, but I'd bet this happens only in the 2.0 version.

I'm referring to the obscure error message. The Kotlin support's new in Framework 5 and Boot 2.0 so it won't be a possible cause of the obscure error message. While the exception comes from the binder, the error message comes from BindFailureAnalyzer. It exists in 1.5 and 2.0, although it's not the same in each, and the input will be different hence the need to check what happens in 1.5.

Unfortunately, I don't think there's much we can do about this. I can't find a good heuristic for detecting whether or not the exception message will be useful. Without one the two options we have are to leave things as they are or to always assume that it won't be useful. If we go with the latter the benefits of the analyser will be largely undone. I think we'll just have to live with this one. We can reconsider if the problem comes up again with a cause that isn't due to a bug in another library.

@wilkinsona @sbueringer @philwebb

I'm sorry for disturbing you, but I have a question.

Can we tweak KotlinReflectionParameterNameDiscoverer to solve this issue?

In my perspective, looks that all in our hands and we don't need to wait for something from Kotlin.

Looks, like if we add checking for enums, it will solve the issue.
I added the following lines at the beginning of the method and it works for me. KotlinReflectionParameterNameDiscoverer.getParameterNames(Constructor<?> ctor):

if (ctor.getDeclaringClass().isEnum()) {
    return null;
}

According to PrioritizedParameterNameDiscoverer.getParameterNames(Constructor<?> ctor) , ParameterNameDiscoverers will be applied until it one of them won't return null.

So, when KotlinReflectionParameterNameDiscoverer returns null, then the following StandardReflectionParameterNameDiscoverer will be applied and return { "$enum$name", "$enum$oridnal"} parameters for default enum and this won't cause an error.

And looks like Alexander Udalov from JetBrains had clear clarification: for Kotlin language, Enum doesn't have undeclared parameters.

So, because of this, don't you think that for Kotlin Enums we should delegate parameters discovering to Java?
Will the code above help?

@ruslanys Thanks for taking a look. KotlinReflectionParameterNameDiscoverer is part of Spring Framework so this isn't the right place to discuss it. SPR-16931 is a better place for the discussion I think.

@wilkinsona Thank you for the response. 🙏