ConstraintValidator dependency injection leads to ValidationException when being validated at class level
Asked Answered
M

5

9

I've encountered an unexpected behaviour when using dependency injection in a ConstraintValidator which is getting evaluated at class level.

Entity class:

@Entity
@ValidDemoEntity
public class DemoEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

}

Validation annotation:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {DemoEntityValidator.class})
public @interface ValidDemoEntity {

    String message() default "{some.demo.validator.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}

Validator:

public class DemoEntityValidator implements ConstraintValidator<ValidDemoEntity, DemoEntity> {

    private DemoEntityRepository demoEntityRepository;

    public DemoEntityValidator(DemoEntityRepository demoEntityRepository) {
        this.demoEntityRepository = demoEntityRepository;
    }

    @Override
    public void initialize(ValidDemoEntity constraintAnnotation) {

    }

    @Override
    public boolean isValid(DemoEntity demoEntity, ConstraintValidatorContext constraintValidatorContext) {
        return true;
    }
}

Test class:

@SpringBootTest
public class ValidatorInstantiationTest {

    private Validator validator;

    @Before
    public void setUp() throws Exception {
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.getValidator();
    }

    @Test
    public void shouldInitiateAndCallDemoEntityValidator() {
        DemoEntity demoEntity = new DemoEntity();
        validator.validate(demoEntity);
    }

}

Validating the entity leads to:

javax.validation.ValidationException: HV000064: Unable to instantiate ConstraintValidator: com.example.demo.DemoEntityValidator.

and further down the stack trace:

Caused by: java.lang.NoSuchMethodException: com.example.demo.DemoEntityValidator.<init>()

which indicates that Hibernate tried to initiate the the class instead of letting Spring take care of that.

The strange thing about this is that dependency injection works fine for validations applied on field level.

The code is available at GitHub.

Mcquade answered 27/6, 2019 at 13:5 Comment(0)
H
5

The exception says that there is no default constructor because Hibernate Validator tries to instantiate your validator.

You have to use Spring.

1 Make your validator a Spring Bean:

@Component
public class DemoEntityValidator implements ConstraintValidator<ValidDemoEntity, DemoEntity> {

2 Inject the Spring provided validator and use the SpringRunner for executing your tests:

@SpringBootTest
@RunWith(SpringRunner.class)
public class ValidatorInstantiationTest {

    @Autowired
    private Validator validator;

    @Test
    public void shouldInitiateAndCallDemoEntityValidator() {
        DemoEntity demoEntity = new DemoEntity();
        validator.validate(demoEntity);
    }

}
Ham answered 27/6, 2019 at 13:25 Comment(0)
M
1

1 Make your validator a Spring Bean

This site states:

The Spring framework automatically detects all classes which implement the ConstraintValidator interface. The framework instantiates them and wires all dependencies like the class was a regular Spring bean.

Which clearly works for validations applied on field level.

Nevertheless I've updated the code.

DemoEntityValidator is now a Spring component:

@Component
public class DemoEntityValidator implements ConstraintValidator<ValidDemoEntity, DemoEntity>

I've changed the test to:

@SpringBootTest
@RunWith(SpringRunner.class)
public class ValidatorInstantiationTest {

    @Autowired
    private DemoEntityRepository demoEntityRepository;

    @Test
    public void shouldInitiateAndCallDemoEntityValidator() {
        DemoEntity demoEntity = new DemoEntity();
        demoEntityRepository.save(demoEntity);
    }

}

To make the usecase clearer, but the test still leads to the same exception.

Mcquade answered 27/6, 2019 at 13:47 Comment(0)
A
0

Adding an empty constructor to the class DemoEntityValidator disables the error.

Alephnull answered 9/8, 2019 at 17:6 Comment(1)
It does, but then DemoEntityRepository isn't injected.Mcquade
A
0

I think you answer is here:

https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation-beanvalidation-spring

You need to declare a LocalValidatorFactoryBean in your configuration class and it will just work.

From the documentation:

By default, the LocalValidatorFactoryBean configures a SpringConstraintValidatorFactory that uses Spring to create ConstraintValidator instances. This lets your custom ConstraintValidators benefit from dependency injection like any other Spring bean.

And an example from the same place:

import javax.validation.ConstraintValidator;

public class MyConstraintValidator implements ConstraintValidator {

    @Autowired;
    private Foo aDependency;

    ...
}

And this is how I declared that bean in a @Configuration annotated class:

  /**
   * Provides auto-wiring capabilities for validators Checkout: https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation-beanvalidation-spring
   */
  @Bean
  public LocalValidatorFactoryBean validatorFactoryBean() {
    LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
    bean.setValidationMessageSource(validationMessageSource());
    return bean;
  }
Allheal answered 14/8, 2019 at 17:47 Comment(1)
I'm running into the same issue it seems, and adding the LocalValidatorFactoryBean does not seem to fix the issue. It's like Hibernate refuses to use the Spring Validator Factory when running in a Unit Test.Creedon
I
0

There's nothing wrong with your validator class. I got this working by making two changes to the test configuration:

1. Run test with Spring

In order to have Spring manage your beans, you need to run your test with a test runner that sets up Spring. You can specify the test runner class using junit's @RunWith-annotation:

@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidatorInstantiationTest { ... }

2. Inject a Spring managed validator bean

Since you're using Spring Boot, you can inject a Spring managed validator – it's already configured. This way, Spring will handle the initiation of your DemoEntityValidator.

@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidatorInstantiationTest {

    @Autowired
    private Validator validator;

    ...
}

This is all that is needed. You should not annotate your DemoEntityValidator with @Component or similar.


Note that you need to provide Spring with a data source, since SpringRunner will set up a context based on your Spring Boot setup (I'm guessing it includes spring-boot-starter-data-jpa in your case). The easiest way to get going is just to put an in-memory DB such as h2 on the classpath.

Ipoh answered 16/8, 2019 at 12:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.