Just recently upgraded from SpringBoot 2.6.6 to 2.6.7, but despite the GAV for hibernate-validator has not changed ( both releases use org.hibernate.validator:hibernate-validator:6.2.3.Final
and jakarta.validation:jakarta.validation-api:2.0.2
), I noticed a change in behaviour using the validation API.
I was able to reduce it to simple test case to show the difference between 2.6.6 and 2.6.7.
NOTE: I understand the reason why it fails and know the fix, but what I don't understand is why it only started to fail with SpringBoot 2.6.7 when Hibernate validator artifact did not change between releases.
To demonstrate, here are the code and test cases:
- First the POM file ( using SpringBoot 2.6.6 ):
<?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.6.6</version>
<relativePath/>
</parent>
<groupId>org.example</groupId>
<artifactId>validator</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- Then the java interface and classes :
package org.example.demo;
public interface WithNameOnCard {
String getNameOnCard();
void setNameOnCard(String nameOnCard);
}
package org.example.demo;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Getter
@Setter
public class MembershipCard implements WithNameOnCard {
@NotNull
@NotBlank
private String nameOnCard;
@NotNull
private String membershipNumber;
}
package org.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
- Finally, the test case :
package org.example.demo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
public class ValidatorTest {
public static Validator validator;
@BeforeAll
public static void setUp() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
@Test
public void whenPropertiesEmptyThenFailValidation() {
MembershipCard membershipCard = new MembershipCard();
Set<ConstraintViolation<MembershipCard>> violations = validator.validate(membershipCard);
Assertions.assertTrue(violations.size() > 0);
}
@Test
public void whenPropertiesNotEmptyThenFailPasses() {
MembershipCard membershipCard = new MembershipCard();
membershipCard.setMembershipNumber("123456");
membershipCard.setNameOnCard("JOHN SMITH");
Set<ConstraintViolation<MembershipCard>> violations = validator.validate(membershipCard);
Assertions.assertEquals(0, violations.size());
}
}
- Run the test, and you get :
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running org.example.demo.ValidatorTest
< ... snipped ... >
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.196 s - in org.example.demo.ValidatorTest
- Now change the POM file so that it uses SpringBoot 2.6.7 :
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/>
</parent>
- Run the test, and you get :
INFO] Running org.example.demo.ValidatorTest
<... snipped ...>
[ERROR] Tests run: 2, Failures: 0, Errors: 2, Skipped: 0, Time elapsed: 0.17 s <<< FAILURE! - in org.example.demo.ValidatorTest
[ERROR] whenPropertiesEmptyThenFailValidation Time elapsed: 0.026 s <<< ERROR!
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method MembershipCard#setNameOnCard(String) redefines the configuration of WithNameOnCard#setNameOnCard(String).
at org.example.demo.ValidatorTest.whenPropertiesEmptyThenFailValidation(ValidatorTest.java:27)
[ERROR] whenPropertiesNotEmptyThenFailPasses Time elapsed: 0.003 s <<< ERROR!
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method MembershipCard#setNameOnCard(String) redefines the configuration of WithNameOnCard#setNameOnCard(String).
at org.example.demo.ValidatorTest.whenPropertiesNotEmptyThenFailPasses(ValidatorTest.java:37)
[ERROR] Errors:
[ERROR] ValidatorTest.whenPropertiesEmptyThenFailValidation:27 » ConstraintDeclaration
[ERROR] ValidatorTest.whenPropertiesNotEmptyThenFailPasses:37 » ConstraintDeclaration ...
[INFO]
[ERROR] Tests run: 3, Failures: 0, Errors: 2, Skipped: 0
NOTE: Like I mentioned above, I understand the reason for the error so that the class and interface must obey the Liskov substitution principle.
The fixes for the error to go away are, either :
- Removing the setter method
setNameOnCard()
from theWithNameOnCard
interface. OR - Moving the validation annotations from the fields of the implementing class to the getter method of the interface. OR
- Moving only the
@NotNull
annotation from thenameOnCard
field of theMembershipCard
class to thegetNameOnCard()
method of theNameOnCard
interface, while leaving the@NotBlank
annotation on thenameOncard
field in the class. ( This last fix I actually don't understand. What's unique about@NotNull
vs@NotBlank
that would cause this difference between 2.6.6 and 2.6.7 ? )
What I don't understand why was this error NOT generated with SpringBoot 2.6.6 when the Hibernate validator artifact remained the same between 2.6.6 and 2.6.7.
So it must be something else that is causing this change in behaviour, but I can't pinpoint to what it is.
CodePudding user response:
Spring Boot 2.6.7 upgraded the Lombok version. The newer Lombok version propagates the field annotations to the generated setter methods. As a work around, you can downgrade the Lombok version by setting the Maven property:
<lombok.version>1.8.22</lombok.version>