1

TLDR: I wanted separate custom bean validation definition and its ConstraintValidator implementations in separate modules. To do that, I have to manually register using ConstraintMapping. It works for annotated classes. But defined binding are not shared/available for validations defined via validation-constraints.xml. How to fix? I tried to debug it, to find out, where it's initialized and why it's problem, but initialization of these is far from easy.

Motivations:

I) separated module: not to enforce users of API to bring ConstraintValidator if they don't want to.

II) usage of validation-constraint.xml: if you have to use java classes, which you cannot modify. Used as xml-defined "mixin".

EDIT: better implementation

as @Guillaume Smet pointed out (Thanks!) all of my implementation can be greatly simplified., as bean validation supports registering using service loader. So you literary don't need anything I described below — custom mapping creation, registering, even Validator/LocalValidatorFactoryBean registering. The autoconfigured ones will work the same. I verified, that service loader config is picked up (remove all my stuff, tried without config, then add it and retried, it works now). Yet the situation is the same When we mention in validation-constraints.xml annotation, which was registered using service loader, we get exception, that give ConstraintValidator does not exist. Annotated classes works.

EDIT: validation classes

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
@SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
@ReportAsSingleViolation
public @interface Iso8601Timestamp {
    String message() default "must have format for example '1970-01-01T01:00:00.000+01:00[CET]', " +
            "but '${validatedValue}' was provided. Timestamp is parsed using {parsingStrategy} strategy.";

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

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

    ParsingStrategy parsingStrategy() default ParsingStrategy.ISO_OFFSET_DATE_TIME;

    enum ParsingStrategy {
        STRICT,
        ISO_OFFSET_DATE_TIME,

    }
}

and

public class Iso8601TimestampValidator implements ConstraintValidator<Iso8601Timestamp, Object> {

    private Iso8601Timestamp.ParsingStrategy parsingStrategy;

    @Override
    public void initialize(Iso8601Timestamp constraint) {
        parsingStrategy = Objects.requireNonNull(constraint.parsingStrategy());
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        //...
    }
}

Implementation:

Since I had to use @Constraint(validatedBy = {}) to avoid compile-time dependency, annotation-ConstraintValidator bindings are externalized into file, and read into:

@Data
public static class Mapping {
    Class annotation;
    Class validator;
}

Now methods, which will create Validator instance. But it works the same with LocalValidatorFactoryBean, and whether I customize it further with MessageInterpolator and ConstraintValidatorFactory or I don't touch it at all.

public static Validator getValidator(List<CustomValidator.Mapping> mappings) {
    return getValidationConfiguration(mappings).buildValidatorFactory().getValidator();
}

private static Configuration<?> getValidationConfiguration(List<CustomValidator.Mapping> mappings) {
    GenericBootstrap genericBootstrap = Validation.byDefaultProvider();
    Configuration<?> configuration = genericBootstrap.configure();
    if (!(configuration instanceof HibernateValidatorConfiguration)) {
        throw new CampException("HibernateValidatorConfiguration is required.");
    }

    registerMappings((HibernateValidatorConfiguration) configuration, mappings);
    return configuration;
}

private static void registerMappings(HibernateValidatorConfiguration hibernateValidatorConfiguration,
                                     List<CustomValidator.Mapping> mappings) {
    mappings.stream()
            .map(pair -> creteConstraintMapping(hibernateValidatorConfiguration, pair))
            .forEach(hibernateValidatorConfiguration::addMapping);
}

@SuppressWarnings("unchecked")
private static ConstraintMapping creteConstraintMapping(HibernateValidatorConfiguration hibernateValidatorConfiguration,
                                                        CustomValidator.Mapping pair) {
    ConstraintMapping constraintMapping = hibernateValidatorConfiguration.createConstraintMapping();
    constraintMapping
            .constraintDefinition(pair.getAnnotation())
            .includeExistingValidators(false)
            .validatedBy(pair.getValidator());

    return constraintMapping;
}

This is our TestInstance:

@Data
@AllArgsConstructor
public class TestInstance {
    @Iso8601Timestamp
    public CharSequence timestamp;

    public String text;
}

And everything will just work, but if we create validation-constraints.xml file and put following validation definition into it(package names removed), everything goes sideways. Also <class ignore-annotations="false"/> is ignored. Value of this element is ignored, annotations on TestInstance are ignored.

<constraint-mappings
    xmlns="http://jboss.org/xml/ns/javax/validation/mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/mapping
                    validation-mapping-1.1.xsd"
    version="1.1">

  <bean class="TestInstance">
    <class ignore-annotations="false"/>
    <getter name="timestamp" ignore-annotations="true">
      <constraint annotation="Iso8601Timestamp"/>
    </getter>
    <getter name="text" ignore-annotations="true">
      <constraint annotation="javax.validation.constraints.NotBlank"/>
    </getter>
  </bean>
</constraint-mappings>

With this we will get following exception when we validate instance of TestInstance(bad naming):

javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint '….Iso8601Timestamp' validating type 'java.lang.CharSequence'. Check configuration for 'timestamp'

But it's not a problem with validation-constraints.xml per se. It's fine. Provided NotBlank works like charm. Only the custom validations which requires manual binding, here represented by Iso8601Timestamp, does not work.

Workaround for validation-constraints.xml — don't use it. Remap data from unmodifiable file to file you own, or just don't validate and let it fail, or … depends on your options.

But I'd really like to know, why this is a problem in first place, since the mapping was provided. Is there some different initialization needed to be made if validation-constraints.xml is (also) used?

1 Answer 1

2

You can declare constraint validators that are in external libraries using the service loader. That is definitely the way to go.

Typically, you would add the name of your constraint validators in a META-INF/services/javax.validation.ConstraintValidator file located in your library.

This way, as soon as your library is in the classpath, the constraint validators in it are automatically declared to Hibernate Validator.

You have a full article explaining all this on our Hibernate blog: https://in.relation.to/2017/03/02/adding-custom-constraint-definitions-via-the-java-service-loader/ .

Sign up to request clarification or add additional context in comments.

6 Comments

ach. SO many hours of research how to do that and I did see this memo anywhere. But GREAT! Thanks! I will verify if this works, since that would significantly simplify my usecase. I event want to use service loader myself for configuration, yet I'm a little surprised, that service loader works with annotations. Anyways thank you for you hint and nice article. Will check that and be in touch. Thanks again!
It's not the annotation that you declare but the ConstraintValidator (which then registers everything needed).
I though we will maintain annotation-validator pairs using service loader, but it's used to discover all validators using it's interface ConstraintValidator. I couldn't see at first how they could create bindings, but it seems they can use class of parameter of method initialize. Got it now. Thanks. I need to read your full article slowly and closely in the evening. Will try and update. No more comments (from me) since then!
Ok, I tried and updated the message. Nice post you made, I learned some new stuff I didn't know about, but sadly, the service loader approach fails equally for me. I will add bean annotation and ConstraintViolaiton to question. But this is the only stuff which remains. These two classes + service loader file, which is correct.
Update:this does work ONLY IF validator implementation is within the same module as validation-constraints.xml configuration. If I move the implementation to different module, with or without service loader configuration file, then during application startup I can see, that validators are found and picked by each call going into ServiceLoader, validation works in annotated classes, but validation declared via validation-constraints.xml does not have these validators available.No java 9 modules in action.I assume,that it configures separate validator instance which don't work with serviceloader
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.