Heap analysis of my webapp showed that I have several thousand live instances of ConstraintValidatorManager$CacheKey, with the numbers growing with every subsequent request. This was surprising given that the application had only 3 constraints configured (via XML).
Hooking up the debugger, I could see that the cache lookup on line 92 of ConstraintValidatorManager always fails - even though the validator is already cached, a new instance is initialized and returned.
I set a breakpoint on the equals method of CacheKey, and found that it always returns false when comparing the annotation member of the two CacheKeys - even when both annotations refer to the same instance.
The annotation in this case isn't a real annotation (because the constraints are defined in XML) - it's a JDK dynamic Proxy which uses the AnnotationProxy class as its InvocationHandler.
When equals() is called via the dynamic Proxy, the eventual invocation is a call to Object.equals(). The problem is that the left-hand argument is this - an AnnotationProxy instance, and the right-hand argument is a dynamic Proxy instance. Object.equals() therefore always returns false.
This simple piece of code shows the broken call to equals() clearly.
The call annotation.equals(annotation) always returns false.