| We encountered the same issue as well when using Jersey (and jersey-spring-bean-validation). Note: As the error does not occur for Spring MVC, it might be related to the was Jersey interacts with Hibernate-validator. As a result it might not be a Hibernate-validator bug, but might worth to take a look. Error occurres for built-in types as well, for example here is an exception message:
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.NotNull' validating type 'java.lang.String'.
I have create a simple application to reproduce the issue: https://github.com/helospark/hibernate-hv-1088-repro I hope it's not a problem that I have not used the given template, but generated a simple Spring-boot application for full End-To-End JUnit test (as a concurrency issue, it would probably not be a good fit to create a regression test out of it). Just run HibernateTestApplicationTests.java as a JUnit test and some of the calls should fail, and the console should contain the exception messages. As far as I can see the following occurres:
- At least 3 concurrent threads are entered to ConstraintValidatorManager class to load the same ConstraintValidator.
- Thread1 and thread2 both finds that the cache does not have this and proceeds to create it.
- Both enter the body of this if:
if ( constraintFactory != defaultConstraintValidatorFactory && constraintFactory != leastRecentlyUsedNonDefaultConstraintValidatorFactory ) {
clearEntriesForFactory( leastRecentlyUsedNonDefaultConstraintValidatorFactory );
leastRecentlyUsedNonDefaultConstraintValidatorFactory = constraintFactory;
}
The probability that many threads enter the body of the if, that clears the factory seems small, but the clearEntriesForFactory operates on ConcurrentHashMap, which has synchronized blocks, and complex logic, so many threads may enter and block each-other.
- Thread1 finishes first and adds the ConstraintValidator to the cache. (Thread2 still is in the clearEntriesForFactory method, and not finished)
- Thread3 meanwhile finds that the ConstraintValidator is in the cache, so it enters the body of this if condition:
if ( constraintValidatorCache.containsKey( key ) ) {
@SuppressWarnings("unchecked")
ConstraintValidator<A, V> constraintValidator = (ConstraintValidator<A, V>) constraintValidatorCache.get(
key
);
...
- Before Thread3 reaches the constraintValidatorCache.get(key) call, Thread2 finishes clearing the cache.
- Thread3 now has null as the ConstraintValidator, as a result ConstraintTree.throwExceptionForNullValidator throws.
The main issue seems to be that containsKey and the following get is not transactional (synchronized). In certain cases containsKey returns true, but get returns null (as the entry was removed between the two calls). On first inspection removing constraintValidatorCache.containsKey and just using get (as ConcurrentHashMap cannot contain null) would solve the issue though some ConstraintValidators would still be created multiple time. |