After changing from Spring Boot 2.1.9 to Spring Boot 2.2.2, the error messages about constraint validation for some of our configuration properties started including null as the rejected value, rather than the actual value (-1 in this case).
The root cause seems to be that SpringValidationAdapter's getRejectedValue(ield, violation, bindingResult)
has started returning null in certain cases with the bindingResult
that's present in 2.2.2. Or rather, the getRawFieldValue(String)
has started returning null.
Failing case in 2.2.2:
SpringValidationAdapter
calls ValidationBindHandlers$ValidationResult.getRawFieldValue("intermediatefield.leafFieldInCamelCase")
which returns null because it expects field names in form "intermediatefield.leaf-field-in-camel-case".
Working case in 2.1.9:
SpringValidationAdapter
calls BeanPropertyBindingResult.getRawFieldValue("intermediatefield.leafFieldInCamelCase")
which returns the actual field value.
I'll try to produce a minimal reproduction later.
Comment From: mbhave
@tazle A sample that we can use to reproduce the issue would be very helpful, thanks.
Comment From: tazle
Reproducer: pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>repro_19580</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>repro_19580</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.18.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
src/main/java/com/example/repro_19580/Repro19580Application.java:
package com.example.repro_19580;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.lang.invoke.MethodHandles;
@ConfigurationProperties(prefix = "inner")
@Validated
class InnerProperties {
@Min(0)
private int camelCaseValue = 0;
public int getCamelCaseValue() {
return camelCaseValue;
}
public void setCamelCaseValue(final int camelCaseValue) {
this.camelCaseValue = camelCaseValue;
}
}
@ConfigurationProperties("test")
@Validated
class TestProperties {
@Valid
InnerProperties inner = new InnerProperties();
public InnerProperties getInner() {
return inner;
}
}
@SpringBootApplication
@EnableConfigurationProperties(TestProperties.class)
@Component
public class Repro19580Application {
private final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@Autowired
TestProperties props;
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(Repro19580Application.class, args);
ctx.getBean(Repro19580Application.class).checkValue();
}
void checkValue() {
log.info("Configured value is: {}", props.getInner().getCamelCaseValue());
}
}
To reproduce:
mvnq package && java -jar target/repro_19580-0.0.1-SNAPSHOT.jar --test.inner.camel-case-value=-1
Comment From: snicoll
@tazle thank you for the sample but rather than code in text, please create a project (either a github project or a zip) as we'd have to copy paste all that in order to do something useful with it anyway.
Comment From: tazle
Zipped up:
Comment From: nosan
Looks like gh-17424 is a cause. ValidationBindHandler$ValidationResult.getActualFieldValue
does not work at all if the provided value is in a camelCase
format:
org.springframework.boot.context.properties.source.InvalidConfigurationPropertyNameException: Configuration property name 'camelCaseValue' is not valid
at org.springframework.boot.context.properties.source.ConfigurationPropertyName.elementsOf(ConfigurationPropertyName.java:522)
at org.springframework.boot.context.properties.source.ConfigurationPropertyName.probablySingleElementOf(ConfigurationPropertyName.java:495)
at org.springframework.boot.context.properties.source.ConfigurationPropertyName.append(ConfigurationPropertyName.java:191)
at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler$ValidationResult.getName(ValidationBindHandler.java:190)
at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler$ValidationResult.getFieldType(ValidationBindHandler.java:168)
at org.springframework.validation.AbstractBindingResult.resolveMessageCodes(AbstractBindingResult.java:325)
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.processConstraintViolations(SpringValidatorAdapter.java:175)
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:109)
at org.springframework.boot.context.properties.ConfigurationPropertiesJsr303Validator.validate(ConfigurationPropertiesJsr303Validator.java:51)
at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler.validateAndPush(ValidationBindHandler.java:132)
at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler.validate(ValidationBindHandler.java:110)
at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler.onFinish(ValidationBindHandler.java:102)
at org.springframework.boot.context.properties.bind.Binder.handleBindResult(Binder.java:340)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:321)
problem line: https://github.com/spring-projects/spring-boot/blob/c584334f5ecfcf3837dfad5150f08c8c3661a2f7/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java#L190
Btw, I have found that https://github.com/spring-projects/spring-boot/blob/c584334f5ecfcf3837dfad5150f08c8c3661a2f7/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java#L180 catches an exception but just swallows it, and that's why this issue was hidden from folks :( Maybe it makes sense at least to log it in DEBUG
level.
This issue can be easily fixed with:
return this.name.append(DataObjectPropertyName.toDashedForm(field));
But I am not sure about this and in addition DataObjectPropertyName
is a package-private class.
Comment From: mbhave
Thanks for the analysis, @nosan. I was initially planning to change the code to skip validation if a previous validation exception was found and that would've fixed this issue as a side effect. I decided to create a separate issue for that.
Comment From: tazle
This fix seems to have gotten rid of some of the issues, but others still persist. I'll create a test case.
Comment From: tazle
This fix seems to have gotten rid of some of the issues, but others still persist. I'll create a test case.
Perhaps that was a transient issue with IDEA and outdated classpath before Maven reimport.