Is it possible to use an interface as a validatedBy

784 Views Asked by At

I want to split a declaration and implementation of my validator very similar to this question in my Spring boot environment. It looks like I somehow made it almost working. I see that my validator is actually called by Spring validation, but after a validation is executed, Hibernate throws an exception:


java.lang.NoSuchMethodException: test.UniqueUsernameValidator.<init>()
    at java.base/java.lang.Class.getConstructor0(Class.java:3427) ~[na:na]
    at java.base/java.lang.Class.getConstructor(Class.java:2165) ~[na:na]
    at org.hibernate.validator.internal.util.privilegedactions.NewInstance.run(NewInstance.java:41) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl.run(ConstraintValidatorFactoryImpl.java:43) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorFactoryImpl.getInstance(ConstraintValidatorFactoryImpl.java:28) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.constraintvalidation.ClassBasedValidatorDescriptor.newInstance(ClassBasedValidatorDescriptor.java:84) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.constraintvalidation.AbstractConstraintValidatorManagerImpl.createAndInitializeValidator(AbstractConstraintValidatorManagerImpl.java:89) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManagerImpl.getInitializedValidator(ConstraintValidatorManagerImpl.java:117) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.getInitializedConstraintValidator(ConstraintTree.java:136) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree.validateConstraints(SimpleConstraintTree.java:54) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:75) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.doValidateConstraint(MetaConstraint.java:130) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:123) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:555) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:518) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:488) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:450) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:400) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateCascadedAnnotatedObjectForCurrentGroup(ValidatorImpl.java:629) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateCascadedConstraints(ValidatorImpl.java:590) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateParametersInContext(ValidatorImpl.java:880) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:283) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.hibernate.validator.internal.engine.ValidatorImpl.validateParameters(ValidatorImpl.java:235) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
    at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:104) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
    at test.AuthenticationController$$EnhancerBySpringCGLIB$$f2b10b56.signUp(<generated>) ~[main/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
    ...
    

It's because UniqueUsernameValidator is an interface (as intended).

I've configured Spring like this (solution from this answert to some differenet question):

  @Bean
  public Validator validator() {
    SpringConstraintValidatorFactoryEx scvf =
        new SpringConstraintValidatorFactoryEx(wac.getAutowireCapableBeanFactory());

    LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
    validator.setConstraintValidatorFactory(scvf);
    validator.setApplicationContext(wac);
    validator.afterPropertiesSet();
    return validator;
  }

My custom validator:

@Component
@Slf4j
public class SpringConstraintValidatorFactoryEx implements ConstraintValidatorFactory {

  private AutowireCapableBeanFactory beanFactory;

  public SpringConstraintValidatorFactoryEx(AutowireCapableBeanFactory beanFactory) {
    super();
    this.beanFactory = beanFactory;
  }

  public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
    T bean = null;

    try {
      log.info("Trying to find a validator bean of class " + key.getSimpleName());
      bean = (T) this.beanFactory.getBean(key);
    } catch (BeansException exc) {
      log.info(
          "Failed to find a bean of class {}, message {}", key.getSimpleName(), exc.getMessage());
    }

    if (bean == null) {
      try {
        log.info("Creating a new validator bean of class " + key.getSimpleName());
        bean = this.beanFactory.createBean(key);
      } catch (BeansException exc) {
        log.info("Failed to create a validator of class " + key.getSimpleName());
      }
    }

    if (bean == null) {
      log.warn("Failed to get validator of class " + key.getSimpleName());
    }

    return bean;
  }

  @Override
  public void releaseInstance(ConstraintValidator<?, ?> instance) {}
}

So it tries to find a Spring bean by a validator name. So I have an implementation of my validator:

@Component
public class UniqueUsernameValidatorImpl implements UniqueUsernameValidator {
  @Autowired private UserCredentialsRepository repository;

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    return repository.findByUsername(value).isEmpty();
  }
}

A validator interface:

public interface UniqueUsernameValidator extends ConstraintValidator<UniqueUsername, String> {

}

A constraint annotation:

@Target({
  ElementType.METHOD,
  ElementType.FIELD,
  ElementType.ANNOTATION_TYPE,
  ElementType.CONSTRUCTOR,
  ElementType.PARAMETER,
  ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(UniqueUsername.List.class)
@Documented
@Constraint(validatedBy = {UniqueUsernameValidator.class})
public @interface UniqueUsername {
  String message() default "username must be unique";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  @Target({
    ElementType.METHOD,
    ElementType.FIELD,
    ElementType.ANNOTATION_TYPE,
    ElementType.CONSTRUCTOR,
    ElementType.PARAMETER,
    ElementType.TYPE_USE
  })
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
  @interface List {
    UniqueUsername[] value();
  }
}

It looks like Spring validation somehow manage to use my implementation of a validator, but then a Hibernate fails to instantiate a validator(?). I don't fully understand why there are 2 different initializations of validation mechanisms.

Is there a way to "register" or "inform" Hibernate validation to use my implementation of my custom validation annotation?

1

There are 1 best solutions below

0
On

Hibernate Validator is using the default impl of ConstraintValidatorFactory (see ConstraintValidatorFactoryImpl in your stacktrace) so that means your custom one is not taken into account somehow.

It's failing in the method validation, maybe you need to provide an ExecutableValidator bean?

(Hibernate Validator lead here, I don't know exactly how Spring wired things).