diff --git a/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java b/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java index 81015a6e..9cc2ea07 100644 --- a/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java +++ b/validation/src/main/java/io/micronaut/validation/ValidatingInterceptor.java @@ -19,9 +19,11 @@ import io.micronaut.aop.InterceptedMethod; import io.micronaut.aop.MethodInterceptor; import io.micronaut.aop.MethodInvocationContext; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.convert.ConversionService; import io.micronaut.core.type.Argument; +import io.micronaut.core.util.memo.MemoizedReference; import io.micronaut.inject.ExecutableMethod; import io.micronaut.validation.validator.ExecutableMethodValidator; import io.micronaut.validation.validator.ReactiveValidator; @@ -52,6 +54,9 @@ public class ValidatingInterceptor implements MethodInterceptor */ public static final int POSITION = InterceptPhase.VALIDATE.getPosition(); + private static final MemoizedReference[]> VALIDATION_GROUPS = + AnnotationMetadata.MEMOIZER_NAMESPACE.newReference(m -> m.classValues(Validated.class, "groups")); + private final @Nullable ExecutableValidator executableValidator; private final @Nullable ExecutableMethodValidator micronautValidator; private final ConversionService conversionService; @@ -183,6 +188,6 @@ private Object validateReturnExecutableValidator(MethodInvocationContext[] getValidationGroups(MethodInvocationContext context) { - return context.classValues(Validated.class, "groups"); + return VALIDATION_GROUPS.get(context); } } diff --git a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java index 8a70f644..4697ed77 100644 --- a/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java +++ b/validation/src/main/java/io/micronaut/validation/validator/DefaultValidator.java @@ -18,9 +18,7 @@ import io.micronaut.aop.Intercepted; import io.micronaut.context.BeanResolutionContext; import io.micronaut.context.ExecutionHandleLocator; -import io.micronaut.context.annotation.ConfigurationReader; import io.micronaut.context.annotation.Primary; -import io.micronaut.context.annotation.Property; import io.micronaut.context.exceptions.BeanInstantiationException; import io.micronaut.core.annotation.AnnotatedElement; import io.micronaut.core.annotation.AnnotationMetadata; @@ -41,6 +39,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.CopyOnWriteMap; import io.micronaut.core.util.StringUtils; +import io.micronaut.core.util.memo.MemoizedReference; import io.micronaut.inject.BeanDefinition; import io.micronaut.inject.ExecutableMethod; import io.micronaut.inject.InjectionPoint; @@ -49,7 +48,6 @@ import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.validation.BeanDefinitionValidator; -import io.micronaut.validation.annotation.ValidatedElement; import io.micronaut.validation.validator.constraints.ConstraintValidator; import io.micronaut.validation.validator.constraints.ConstraintValidatorContext; import io.micronaut.validation.validator.constraints.ConstraintValidatorRegistry; @@ -68,7 +66,6 @@ import jakarta.validation.Payload; import jakarta.validation.TraversableResolver; import jakarta.validation.UnexpectedTypeException; -import jakarta.validation.Valid; import jakarta.validation.ValidationException; import jakarta.validation.metadata.BeanDescriptor; import jakarta.validation.metadata.ConstraintDescriptor; @@ -114,6 +111,8 @@ public class DefaultValidator implements receiver.indexedValue("", i++, item); } }; + private static final MemoizedReference>> CONSTRAINTS = + AnnotationMetadata.MEMOIZER_NAMESPACE.newReference(DefaultValidator::getConstraints0); final MessageInterpolator messageInterpolator; final ConcurrentMap, List> findGroupSequencesCache = new CopyOnWriteMap<>(16 * 1024); @@ -128,12 +127,6 @@ public class DefaultValidator implements private final InternalConstraintValidatorFactory constraintValidatorFactory; private final boolean isPrependPropertyPath; - // The advantage of CopyOnWriteMap over ConcurrentHashMap is that here we can define a maximum - // size after which entries are evicted. This can save us from a memory leak if we cache more - // than we should. We still set it comfortably high to avoid unnecessary evictions. - private final ConcurrentMap>> constraintCache = - new CopyOnWriteMap<>(65536); - /** * Default constructor. * @@ -313,7 +306,7 @@ public Set> validateValue(Class beanType, String p @Override public Set validatedAnnotatedElement(@NonNull AnnotatedElement element, @Nullable Object value) { requireNonNull("element", element); - if (!element.getAnnotationMetadata().hasStereotype(Constraint.class)) { + if (!ValidatorAnnotations.hasStereotypeConstraint(element.getAnnotationMetadata())) { return Collections.emptySet(); } @@ -509,19 +502,17 @@ public Set> validateReturnValue(T bean, ExecutableMet try (DefaultConstraintValidatorContext.GroupsValidation validation = context.withGroupSequence(groupSequence)) { // Strip class annotations AnnotationMetadata returnAm = returnType.asArgument().getAnnotationMetadata(); - boolean cacheConstraints = true; if (returnAm instanceof AnnotationMetadataHierarchy annotationMetadataHierarchy) { if (returnAm.getDeclaredMetadata() instanceof AnnotationMetadataHierarchy) { returnAm = new AnnotationMetadataHierarchy( annotationMetadataHierarchy.getRootMetadata(), annotationMetadataHierarchy.getDeclaredMetadata().getDeclaredMetadata() ); - cacheConstraints = false; } else { returnAm = annotationMetadataHierarchy.getDeclaredMetadata(); } } - visitElement(context, bean, returnType.asArgument(), returnAm, returnValue, canCascade, false, cacheConstraints); + visitElement(context, bean, returnType.asArgument(), returnAm, returnValue, canCascade, false); if (validation.isFailed()) { return context.getOverallViolations(); @@ -687,8 +678,8 @@ public void validateBeanArgument(@NonNull BeanResolutionContext resolutionCo int index, @Nullable T value) throws BeanInstantiationException { final AnnotationMetadata annotationMetadata = argument.getAnnotationMetadata(); - final boolean hasValid = annotationMetadata.hasStereotype(Valid.class); - final boolean hasConstraint = annotationMetadata.hasStereotype(Constraint.class); + final boolean hasValid = ValidatorAnnotations.hasStereotypeValid(annotationMetadata); + final boolean hasConstraint = ValidatorAnnotations.hasStereotypeConstraint(annotationMetadata); if (!hasConstraint && !hasValid) { return; @@ -735,7 +726,7 @@ public void validateBean(@NonNull BeanResolutionContext resolutionContext, if (introspection != null) { Set> errors = validate(introspection, bean); failOnError(resolutionContext, errors, beanType); - } else if (bean instanceof Intercepted && definition.hasStereotype(ConfigurationReader.class)) { + } else if (bean instanceof Intercepted && ValidatorAnnotations.hasStereotypeConfigurationReader(definition)) { final Collection> executableMethods = definition.getExecutableMethods(); if (CollectionUtils.isEmpty(executableMethods)) { return; @@ -750,9 +741,9 @@ public void validateBean(@NonNull BeanResolutionContext resolutionContext, } try (ValidationPath.ContextualPath ignored = context.getCurrentPath().addConstructorNode(constructorName)) { for (ExecutableMethod executableMethod : executableMethods) { - if (executableMethod.hasAnnotation(Property.class)) { - final boolean hasConstraint = executableMethod.hasStereotype(Constraint.class); - final boolean isValid = executableMethod.hasStereotype(Valid.class); + if (ValidatorAnnotations.hasAnnotationProperty(executableMethod)) { + final boolean hasConstraint = ValidatorAnnotations.hasStereotypeConstraint(executableMethod); + final boolean isValid = ValidatorAnnotations.hasStereotypeValid(executableMethod); if (hasConstraint || isValid) { final Object value = executableMethod.invoke(bean); @@ -935,15 +926,15 @@ private void validateParametersInternal(@NonNull DefaultConstraintValidatorC for (DefaultConstraintValidatorContext.ValidationGroup groupSequence : groupSequences) { try (DefaultConstraintValidatorContext.GroupsValidation validation = context.withGroupSequence(groupSequence)) { - if (methodAnnotationMetadata.hasStereotype(Constraint.class)) { + if (ValidatorAnnotations.hasStereotypeConstraint(methodAnnotationMetadata)) { try (ValidationPath.ContextualPath ignored = context.getCurrentPath().addCrossParameterNode()) { - validateConstrains(context, bean, Argument.of(Object[].class, methodAnnotationMetadata), parameters, true); + validateConstrains(context, bean, Argument.of(Object[].class, methodAnnotationMetadata), parameters); } } for (int parameterIndex = 0; parameterIndex < argLen; parameterIndex++) { Argument argument = (Argument) arguments[parameterIndex]; - if (!argument.getAnnotationMetadata().hasAnnotation(ValidatedElement.class)) { + if (!ValidatorAnnotations.hasAnnotationValidatedElement(argument.getAnnotationMetadata())) { continue; } try (ValidationPath.ContextualPath ignored = context.getCurrentPath().addParameterNode(argument.getName(), parameterIndex)) { @@ -972,8 +963,7 @@ private void validateParametersInternal(@NonNull DefaultConstraintValidatorC argument, parameterValue, canCascade, - false, - true + false ); } } @@ -1111,7 +1101,7 @@ private void visitElement(DefaultConstraintValidatorContext context, annotationMetadata, elementValue, canCascade, - canCascade && annotationMetadata.hasStereotype(Valid.class), + canCascade && ValidatorAnnotations.hasStereotypeValid(annotationMetadata), true ); } @@ -1121,8 +1111,7 @@ private void visitElement(DefaultConstraintValidatorContext context, Argument elementArgument, E elementValue, boolean canCascade, - boolean needsCanCascadeCheck, - boolean cacheConstraints) { + boolean needsCanCascadeCheck) { AnnotationMetadata annotationMetadata = elementArgument.getAnnotationMetadata(); visitElement(context, bean, @@ -1130,8 +1119,7 @@ private void visitElement(DefaultConstraintValidatorContext context, annotationMetadata, elementValue, canCascade, - needsCanCascadeCheck, - cacheConstraints + needsCanCascadeCheck ); } @@ -1141,17 +1129,15 @@ private void visitElement(DefaultConstraintValidatorContext context, AnnotationMetadata annotationMetadata, E elementValue, boolean canCascade, - boolean needsCanCascadeCheck, - boolean cacheConstraints) { + boolean needsCanCascadeCheck) { visitElement(context, bean, elementArgument, annotationMetadata, elementValue, canCascade, - canCascade && annotationMetadata.hasStereotype(Valid.class), - needsCanCascadeCheck, - cacheConstraints + canCascade && ValidatorAnnotations.hasStereotypeValid(annotationMetadata), + needsCanCascadeCheck ); } @@ -1162,10 +1148,9 @@ private void visitElement(DefaultConstraintValidatorContext context, E elementValue, boolean canCascade, boolean hasValid, - boolean needsCanCascadeCheck, - boolean cacheConstraints) { + boolean needsCanCascadeCheck) { - List> constraints = getConstraints(context, annotationMetadata, cacheConstraints); + List> constraints = getConstraints(context, annotationMetadata); if (visitContainer(context, leftBean, elementArgument, annotationMetadata, elementValue, constraints, canCascade)) { return; @@ -1193,7 +1178,7 @@ private boolean visitContainer(DefaultConstraintValidatorContext conte return false; } - boolean isLegacyValid = annotationMetadata.hasAnnotation(Valid.class) + boolean isLegacyValid = ValidatorAnnotations.hasAnnotationValid(annotationMetadata) && (Iterable.class.isAssignableFrom(containerArgument.getType()) || Map.class.isAssignableFrom(containerArgument.getType()) || Object[].class.isAssignableFrom(containerArgument.getType()) @@ -1337,9 +1322,8 @@ private void validateContainerValue(Object value) { containerValueArgument.getAnnotationMetadata(), value, canCascade, - containerValueArgument.getAnnotationMetadata().hasStereotype(Valid.class) || isLegacyValid, - true, - false // might be possible to cache, investigate if there's a perf problem here + ValidatorAnnotations.hasStereotypeValid(containerValueArgument.getAnnotationMetadata()) || isLegacyValid, + true ); } @@ -1369,8 +1353,8 @@ private void validateContainerValue(DefaultConstraintValidatorContext boolean isValidated(Argument containerArgument) { - return containerArgument.getAnnotationMetadata().hasAnnotation(ValidatedElement.class); + private static boolean isValidated(Argument containerArgument) { + return ValidatorAnnotations.hasAnnotationValidatedElement(containerArgument.getAnnotationMetadata()); } private void propagateValidation(DefaultConstraintValidatorContext context, @@ -1397,10 +1381,9 @@ private void propagateValidation(DefaultConstraintValidatorContext con private void validateConstrains(DefaultConstraintValidatorContext context, @Nullable Object leftBean, @NonNull Argument elementArgument, - @Nullable E elementValue, - boolean cacheConstraints) { + @Nullable E elementValue) { AnnotationMetadata annotationMetadata = elementArgument.getAnnotationMetadata(); - List> constraints = getConstraints(context, annotationMetadata, cacheConstraints); + List> constraints = getConstraints(context, annotationMetadata); validateConstrains(context, leftBean, elementArgument, elementValue, constraints); } @@ -1514,28 +1497,22 @@ private DefaultConstraintViolation createConstraintViolation(DefaultConst ); } - private boolean isConstraintIncluded(DefaultConstraintValidatorContext context, + private static boolean isConstraintIncluded(DefaultConstraintValidatorContext context, DefaultConstraintDescriptor constraint) { return context.containsGroup(constraint.getGroups()); } private List> getConstraints(DefaultConstraintValidatorContext context, - AnnotationMetadata annotationMetadata, - boolean cache) { - if (cache) { - List> cached = constraintCache.computeIfAbsent(annotationMetadata, m -> getConstraints0(null, m)); - if (!cached.isEmpty()) { - cached = new ArrayList<>(cached); - cached.removeIf(descriptor -> !isConstraintIncluded(context, descriptor)); - } - return cached; - } else { - return getConstraints0(context, annotationMetadata); + AnnotationMetadata annotationMetadata) { + List> cached = CONSTRAINTS.get(annotationMetadata); + if (!cached.isEmpty()) { + cached = new ArrayList<>(cached); + cached.removeIf(descriptor -> !isConstraintIncluded(context, descriptor)); } + return cached; } - private List> getConstraints0(@Nullable DefaultConstraintValidatorContext context, - AnnotationMetadata annotationMetadata) { + private static List> getConstraints0(AnnotationMetadata annotationMetadata) { List> descriptors = new ArrayList<>(); for (Class constraintType : annotationMetadata.getAnnotationTypesByStereotype(Constraint.class)) { List> annotationValuesByType = annotationMetadata.getAnnotationValuesByType(constraintType); @@ -1543,14 +1520,11 @@ private List> getConstraints0(@Nulla annotationValuesByType = annotationMetadata.getDeclaredAnnotationValuesByType(constraintType); } for (AnnotationValue annotationValue : annotationValuesByType) { - DefaultConstraintDescriptor descriptor = new DefaultConstraintDescriptor<>( + descriptors.add(new DefaultConstraintDescriptor( (Class) constraintType, (AnnotationValue) annotationValue, annotationMetadata - ); - if (context == null || isConstraintIncluded(context, descriptor)) { - descriptors.add(descriptor); - } + )); } } return descriptors; diff --git a/validation/src/main/java/io/micronaut/validation/validator/ValidatorAnnotations.java b/validation/src/main/java/io/micronaut/validation/validator/ValidatorAnnotations.java new file mode 100644 index 00000000..e9a11803 --- /dev/null +++ b/validation/src/main/java/io/micronaut/validation/validator/ValidatorAnnotations.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2025 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.validation.validator; + +import io.micronaut.context.annotation.ConfigurationReader; +import io.micronaut.context.annotation.Property; +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.util.memo.MemoizedFlag; +import io.micronaut.validation.annotation.ValidatedElement; +import jakarta.validation.Constraint; +import jakarta.validation.Valid; + +@Internal +final class ValidatorAnnotations { + private static final MemoizedFlag STEREOTYPE_VALID = AnnotationMetadata.MEMOIZER_NAMESPACE.newFlag(m -> m.hasStereotype(Valid.class)); + private static final MemoizedFlag STEREOTYPE_CONSTRAINT = AnnotationMetadata.MEMOIZER_NAMESPACE.newFlag(m -> m.hasStereotype(Constraint.class)); + private static final MemoizedFlag STEREOTYPE_CONFIGURATION_READER = AnnotationMetadata.MEMOIZER_NAMESPACE.newFlag(m -> m.hasStereotype(ConfigurationReader.class)); + private static final MemoizedFlag ANNOTATION_PROPERTY = AnnotationMetadata.MEMOIZER_NAMESPACE.newFlag(m -> m.hasAnnotation(Property.class)); + private static final MemoizedFlag ANNOTATION_VALID = AnnotationMetadata.MEMOIZER_NAMESPACE.newFlag(m -> m.hasAnnotation(Valid.class)); + private static final MemoizedFlag ANNOTATION_VALIDATED_ELEMENT = AnnotationMetadata.MEMOIZER_NAMESPACE.newFlag(m -> m.hasAnnotation(ValidatedElement.class)); + + private ValidatorAnnotations() { + } + + static boolean hasStereotypeValid(AnnotationMetadata annotationMetadata) { + return STEREOTYPE_VALID.get(annotationMetadata); + } + + static boolean hasStereotypeConstraint(AnnotationMetadata annotationMetadata) { + return STEREOTYPE_CONSTRAINT.get(annotationMetadata); + } + + static boolean hasStereotypeConfigurationReader(AnnotationMetadata annotationMetadata) { + return STEREOTYPE_CONFIGURATION_READER.get(annotationMetadata); + } + + static boolean hasAnnotationProperty(AnnotationMetadata executableMethod) { + return ANNOTATION_PROPERTY.get(executableMethod); + } + + static boolean hasAnnotationValid(AnnotationMetadata annotationMetadata) { + return ANNOTATION_VALID.get(annotationMetadata); + } + + static boolean hasAnnotationValidatedElement(AnnotationMetadata annotationMetadata) { + return ANNOTATION_VALIDATED_ELEMENT.get(annotationMetadata); + } +}