3

I will try to ignore other details and make it short:

@Entity
public class User
    @UniqueEmail
    @Column(unique = true)
    private String email;
}

@Component
public class UniqueEmailValidatior implements ConstraintValidator<UniqueEmail,String>, InitializingBean {

    @Autowired private UserService userService;

    @Override
    public void initialize(UniqueEmail constraintAnnotation) {

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if(userService == null) throw new IllegalStateException();
        if(value == null) return false;
        return !userService.isEmailExisted(value);
    }

}

This will work when the validation is made in Spring (Spring MVC @Valid or inject the Validator using @Autowire), everything will be fine. But as soon as I save the entity using Spring Data JPA:

User save = userRepository.save(newUser);

Hibernate will try to instantiate a new UniqueEmailValidatior without inject the UserService bean. So how can I make Hibernate to use my UniqueEmailValidatior component without it instantiate a new one. I could disable hibernate validation using spring.jpa.properties.javax.persistence.validation.mode=none but I hope there is another way

Update: Here is my UserService:

   @Autowired private Validator validator;
   @Transactional
    public SimpleUserDTO newUser(UserRegisterDTO user) {
        validator.validate(user);
        System.out.println("This passes");
        User newUser = new User(user.getUsername(),
                passwordEncoder.encode(user.getPassword()),user.getEmail(),
                "USER",
                user.getAvatar());
        User save = userRepository.save(newUser);
        System.out.println("This won't pass");
        return ....
    }
cdxf
  • 5,501
  • 11
  • 50
  • 65
  • Are you doing things to override/configure the `LocalValidatorFactoryBean`? Hibernate should use the same as Spring and as such auto wiring should work. Or do you configure things explicitly for Spring Data JPA? – M. Deinum May 07 '18 at 10:48
  • I don't override anything in my Configuration, you can check the updated part too – cdxf May 07 '18 at 10:55
  • I don't see configuration in your update (only a service)? – M. Deinum May 07 '18 at 10:57
  • I let Spring Boot autoconfigure everything database related so I don't have any configuration – cdxf May 07 '18 at 11:01
  • Which Spring Boot version are you using? – M. Deinum May 07 '18 at 11:24

3 Answers3

8

I would expect that Spring Boot would wire the existing validator to the EntityManager apparently it doesn't.

You can use a HibernatePropertiesCustomizer and add properties to the existing EntityManagerFactoryBuilder and register the Validator.

NOTE: I'm assuming here that you are using Spring Boot 2.0

@Component
public class ValidatorAddingCustomizer implements HibernatePropertiesCustomizer {

    private final ObjectProvider<javax.validation.Validator> provider;

    public ValidatorAddingCustomizer(ObjectProvider<javax.validation.Validator> provider) {
        this.provider=provider;
    }

    public void customize(Map<String, Object> hibernateProperties) {
        Validator validator = provider.getIfUnique();
        if (validator != null) {
            hibernateProperties.put("javax.persistence.validation.factory", validator);
        }
    }
}

Something like this should wire the existing validator with hibernate and with that it will make use of auto wiring.

NOTE: You don't need to use @Component on the validator the autowiring is build into the validator factory before returning the instance of the Validator.

Bambino
  • 405
  • 4
  • 15
M. Deinum
  • 115,695
  • 22
  • 220
  • 224
  • I've added ValidatorAddingCustomizer but it still not work, I've updated the sample project in case you're interested – cdxf May 07 '18 at 11:54
  • You can try debugging and see if this is getting called (you have put it in a package that is available for component scanning?). I did a `getIfUnique` could be that there isn't a unique one (but multiple). You can try the `getObject` instead and see if that works or fails. – M. Deinum May 07 '18 at 12:00
  • It get called, the validator is LocalValidatorFactoryBean, but somehow the UserService still not get injected – cdxf May 07 '18 at 12:10
  • Any chance someone familiar with the Spring ecosystem could open an issue for Spring/Spring Data? As mentioned in my answer below, it's the second time in a row we have this issue. It would be nice if it could get fixed once and for all. – Guillaume Smet May 07 '18 at 12:34
  • Anyone can register a Github issue... I would start with Spring Boot to have it included in there (or at least that it was made possible). – M. Deinum May 07 '18 at 12:56
  • While searching for this issue, I have come across a Spring Issue that is probably this issue: https://github.com/spring-projects/spring-boot/issues/15829. It might be useful to track progress there. – niknetniko Apr 21 '19 at 23:09
1

To have the Spring beans injected into your ConstraintValidator, you need a specific ConstraintValidatorFactory which should be passed at the initialization of the ValidatorFactory.

Something along the lines of:

ValidatorFactory validatorFactory = Validation.byDefaultProvider()
    .configure()
    .constraintValidatorFactory( new MySpringAwareConstraintValidatorFactory( mySpringContext ) )
    .build();

with MySpringAwareConstraintValidatorFactory being a ConstraintValidatorFactory that injects the beans inside your ConstraintValidator.

I suspect the ValidatorFactory used by Spring Data does not inject the validators when creating them, which is unfortunate.

I suppose you should be able to override it. Or better, you should open an issue against Spring Boot/Spring Data so that they properly inject the ConstraintValidators as it the second time in a row we have this question on SO.

Guillaume Smet
  • 9,921
  • 22
  • 29
  • Spring already does that with the `LocalValidatorFactoryBean` and more, the auto wiring part. You don't need a special `ConstraintValidatorFactory`. You need to make JPA aware of the correct validator. – M. Deinum May 07 '18 at 11:23
  • This is true for the `ValidatorFactory` used by Spring MVC (and indeed it works in the OP's case). What I'm wondering is if this is the `ValidatorFactory` passed to Hibernate ORM for the JPA validation. – Guillaume Smet May 07 '18 at 11:52
  • Not by default (although I expected Spring Boot to do so). However letting Hibernate (or JPA for that matter) use it isn't very hard. – M. Deinum May 07 '18 at 11:53
0

The answer is quite big to post here. Please check for this article in S.O to help you with. This should help you get started.

Test Custom Validator with Autowired spring Service

The problem is hibernate will no way know spring definition. However you can make Entities to be aware of any type of javax.validation types. Hope this helps.

Karthik R
  • 5,523
  • 2
  • 18
  • 30