From e42eb6baf8ff44d0a6047ade4b6ad7a24e8d6ed6 Mon Sep 17 00:00:00 2001 From: Markus Jung Date: Mon, 5 Oct 2026 09:52:04 +0200 Subject: [PATCH 1/4] Precompute container element descriptors and cache runtime container keys - Precompute the leaf container element descriptors of an element as a flat array instead of unwrapping composed descriptors with a lambda for every validated value. - Cache the runtime container key per container class on container element descriptors instead of resolving type arguments reflectively for every cascaded container. - Collect extracted values without a Lazy holder. --- .../jsr/descriptor/CascadableContainerD.java | 18 ++++++ .../jsr/descriptor/ContainerElementTypeD.java | 25 ++++++++ .../apache/bval/jsr/job/ValidationJob.java | 59 +++++++++++-------- .../jsr/valueextraction/ExtractValues.java | 10 ++-- 4 files changed, 82 insertions(+), 30 deletions(-) diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/CascadableContainerD.java b/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/CascadableContainerD.java index 30adbdc0f3..208c76c5e5 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/CascadableContainerD.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/CascadableContainerD.java @@ -19,6 +19,8 @@ package org.apache.bval.jsr.descriptor; import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import jakarta.validation.metadata.CascadableDescriptor; @@ -37,6 +39,7 @@ public abstract class CascadableContainerD

, E extends A private final Set groupConversions; private final Set containerElementTypes; private final boolean cascadedDeep; + private final ContainerElementTypeD[] leafContainerElementTypes; protected CascadableContainerD(MetadataReader.ForContainer reader, P parent) { super(reader, parent); @@ -47,6 +50,21 @@ protected CascadableContainerD(MetadataReader.ForContainer reader, P parent) .filter(DescriptorManager::isConstrained) .collect(ToUnmodifiable.set()); cascadedDeep = cascaded || containerElementTypes.stream().anyMatch(DescriptorManager::isCascaded); + final List leaves = new ArrayList<>(); + for (ContainerElementTypeDescriptor d : containerElementTypes) { + ComposedD.forEachUnwrapped(d, ContainerElementTypeD.class, leaves::add); + } + leafContainerElementTypes = leaves.toArray(new ContainerElementTypeD[0]); + } + + /** + * Get the leaf (non-composed) delegates of {@link #getConstrainedContainerElementTypes()}. The result must not + * be modified. + * + * @return {@link ContainerElementTypeD} array, possibly empty + */ + public ContainerElementTypeD[] getLeafContainerElementTypes() { + return leafContainerElementTypes; } /** diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/ContainerElementTypeD.java b/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/ContainerElementTypeD.java index ac5337a090..7c56ffb502 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/ContainerElementTypeD.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/ContainerElementTypeD.java @@ -19,9 +19,12 @@ package org.apache.bval.jsr.descriptor; import java.lang.reflect.AnnotatedType; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import jakarta.validation.metadata.ContainerElementTypeDescriptor; +import org.apache.bval.jsr.GraphContext; import org.apache.bval.jsr.metadata.ContainerElementKey; import org.apache.bval.util.Validate; @@ -29,6 +32,8 @@ public class ContainerElementTypeD extends CascadableContainerD, ContainerElementKey> runtimeKeys = new ConcurrentHashMap<>(); ContainerElementTypeD(ContainerElementKey key, MetadataReader.ForContainer reader, CascadableContainerD parent) { @@ -49,4 +54,24 @@ public Integer getTypeArgumentIndex() { public ContainerElementKey getKey() { return key; } + + /** + * Get {@link #getKey()} in terms of the runtime type of the container held by {@code context}. + * + * @param context + * @return {@link ContainerElementKey} + * @see GraphContext#runtimeKey(ContainerElementKey) + */ + public ContainerElementKey getRuntimeKey(GraphContext context) { + final Object value = context.getValue(); + if (value == null || value.getClass() == key.getContainerClass()) { + return key; + } + ContainerElementKey result = runtimeKeys.get(value.getClass()); + if (result == null) { + result = context.runtimeKey(key); + runtimeKeys.putIfAbsent(value.getClass(), result); + } + return result; + } } diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java index 84bbcfe4e3..ef9b35a435 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java @@ -21,6 +21,7 @@ import java.lang.reflect.Array; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; @@ -53,6 +54,7 @@ import org.apache.bval.jsr.ConstraintViolationImpl; import org.apache.bval.jsr.GraphContext; import org.apache.bval.jsr.descriptor.BeanD; +import org.apache.bval.jsr.descriptor.CascadableContainerD; import org.apache.bval.jsr.descriptor.ComposedD; import org.apache.bval.jsr.descriptor.ConstraintD; import org.apache.bval.jsr.descriptor.ContainerElementTypeD; @@ -381,21 +383,17 @@ public SproutFrame(Frame parent, D descriptor, GraphContext context) { void validateDescriptorConstraints(GroupStrategy groups, Consumer> sink) { super.validateDescriptorConstraints(groups, sink); if (context.getValue() != null) { - for (final ContainerElementTypeDescriptor ctd : descriptor.getConstrainedContainerElementTypes()) { - ComposedD.forEachUnwrapped(ctd, ContainerElementTypeD.class, d -> { - if (d.getConstraintsFor(groups).length == 0 - && d.getConstrainedContainerElementTypes().isEmpty()) { - return; - } - final ValueExtractor declaredTypeValueExtractor = - context.getValidatorContext().getValueExtractors().find(d.getKey()); - for (final GraphContext e : ExtractValues.extract(context, d.getKey(), declaredTypeValueExtractor)) { - if (!e.isRecursive()) { - new ContainerElementConstraintsFrame(this, d, e) - .validateDescriptorConstraints(groups, sink); - } + for (final ContainerElementTypeD d : containerElementTypes(descriptor)) { + if (d.getConstraintsFor(groups).length == 0 && d.getConstrainedContainerElementTypes().isEmpty()) { + continue; + } + final ValueExtractor declaredTypeValueExtractor = + context.getValidatorContext().getValueExtractors().find(d.getKey()); + for (final GraphContext e : ExtractValues.extract(context, d.getKey(), declaredTypeValueExtractor)) { + if (!e.isRecursive()) { + new ContainerElementConstraintsFrame(this, d, e).validateDescriptorConstraints(groups, sink); } - }); + } } } } @@ -415,19 +413,17 @@ void recurse(GroupStrategy groups, Consumer> sink) { } private void cascade(GroupStrategy groups, Consumer> sink) { - for (final ContainerElementTypeDescriptor ctd : descriptor.getConstrainedContainerElementTypes()) { - ComposedD.forEachUnwrapped(ctd, ContainerElementTypeD.class, d -> { - if (!d.isCascaded() && d.getConstrainedContainerElementTypes().isEmpty()) { - return; - } - final ValueExtractor runtimeTypeValueExtractor = - context.getValidatorContext().getValueExtractors().find(context.runtimeKey(d.getKey())); - for (final GraphContext e : ExtractValues.extract(context, d.getKey(), runtimeTypeValueExtractor)) { - if (!e.isRecursive()) { - new ContainerElementCascadeFrame(this, d, e).recurse(groups, sink); - } + for (final ContainerElementTypeD d : containerElementTypes(descriptor)) { + if (!d.isCascaded() && d.getConstrainedContainerElementTypes().isEmpty()) { + continue; + } + final ValueExtractor runtimeTypeValueExtractor = + context.getValidatorContext().getValueExtractors().find(d.getRuntimeKey(context)); + for (final GraphContext e : ExtractValues.extract(context, d.getKey(), runtimeTypeValueExtractor)) { + if (!e.isRecursive()) { + new ContainerElementCascadeFrame(this, d, e).recurse(groups, sink); } - }); + } } if (!descriptor.isCascaded()) { return; @@ -619,6 +615,17 @@ Object getBean() { private static final Consumer DISCARD = cv -> { }; + private static ContainerElementTypeD[] containerElementTypes(ContainerDescriptor descriptor) { + if (descriptor instanceof CascadableContainerD) { + return ((CascadableContainerD) descriptor).getLeafContainerElementTypes(); + } + final List result = new ArrayList<>(); + for (final ContainerElementTypeDescriptor d : descriptor.getConstrainedContainerElementTypes()) { + ComposedD.forEachUnwrapped(d, ContainerElementTypeD.class, result::add); + } + return result.toArray(new ContainerElementTypeD[0]); + } + protected static final TypeVariable MAP_VALUE = Map.class.getTypeParameters()[1]; protected static final TypeVariable ITERABLE_ELEMENT = Iterable.class.getTypeParameters()[0]; diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ExtractValues.java b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ExtractValues.java index 137e68f8bc..fb077032bf 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ExtractValues.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/valueextraction/ExtractValues.java @@ -29,7 +29,6 @@ import org.apache.bval.jsr.metadata.ContainerElementKey; import org.apache.bval.jsr.util.NodeImpl; import org.apache.bval.util.Exceptions; -import org.apache.bval.util.Lazy; import org.apache.bval.util.Validate; /** @@ -40,7 +39,7 @@ public final class ExtractValues { private static class Receiver implements ValueExtractor.ValueReceiver { private final GraphContext context; private final ContainerElementKey containerElementKey; - private final Lazy> result = new Lazy<>(ArrayList::new); + private List result; Receiver(GraphContext context, ContainerElementKey containerElementKey) { super(); @@ -77,7 +76,10 @@ public void keyedValue(String nodeName, Object key, Object object) { private void addChild(NodeImpl node, Object value) { // child(NodeImpl, ...) copies the path exactly once; building the path here and passing it to // child(Path, ...) would copy it a second time. - result.get().add(context.child( + if (result == null) { + result = new ArrayList<>(); + } + result.add(context.child( node.inContainer(containerElementKey.getContainerClass(), containerElementKey.getTypeArgumentIndex()), value)); } @@ -102,7 +104,7 @@ public static List extract(GraphContext context, ContainerElementK } catch (Exception e) { throw new ValidationException(e); } - return receiver.result.optional().orElse(Collections.emptyList()); + return receiver.result == null ? Collections. emptyList() : receiver.result; } return Collections.singletonList(context); } From 1b697cae8c6923f8428b47aa4dfcfe47a456c693 Mon Sep 17 00:00:00 2001 From: Markus Jung Date: Mon, 5 Oct 2026 09:52:11 +0200 Subject: [PATCH 2/4] Find the ancestor of a cascaded container element without copying paths Compare paths in place to find the context whose path is the element's minus its container element node, without materializing the element's path when it is its parent's plus one node. --- .../org/apache/bval/jsr/GraphContext.java | 24 ++++++++++++++++++ .../apache/bval/jsr/job/ValidationJob.java | 25 +++++++------------ .../org/apache/bval/jsr/util/PathImpl.java | 25 +++++++++++++++++++ 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/GraphContext.java b/bval-jsr/src/main/java/org/apache/bval/jsr/GraphContext.java index ae4e0ad1cb..0ed7756ba6 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/GraphContext.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/GraphContext.java @@ -99,6 +99,30 @@ private PathImpl derivePath() { return p; } + /** + * Get the leaf node of this context's path without materializing the path when it is only pending. + * + * @return {@link NodeImpl} + */ + public NodeImpl getLeafNode() { + return path == null && pendingNode != null ? pendingNode : pathReference().getLeafNode(); + } + + /** + * Learn whether {@code other}'s path equals this context's path with its leaf node removed, as by + * {@link PathImpl#removeLeafNode()}. + * + * @param other + * @return {@code boolean} + */ + public boolean hasParentPath(GraphContext other) { + if (path == null && pendingNode != null && !parent.pathReference().isRootPath()) { + // our path is the parent's with pendingNode appended, so removing the leaf yields the parent's path + return other == parent || other.pathReference().equals(parent.pathReference()); + } + return pathReference().isParentPath(other.pathReference()); + } + public Object getValue() { return value; } diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java index ef9b35a435..b4297ccf88 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java @@ -551,32 +551,25 @@ void validateDescriptorConstraints(GroupStrategy groups, Consumer Date: Mon, 5 Oct 2026 09:52:26 +0200 Subject: [PATCH 3/4] Cascade without capturing lambdas Cascade a single-group strategy without group conversions directly instead of through GroupStrategy#applyTo, and into container elements without allocating a capturing consumer per cascaded value. --- .../apache/bval/jsr/job/ValidationJob.java | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java index b4297ccf88..14ad9140b6 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidationJob.java @@ -403,8 +403,18 @@ void recurse(GroupStrategy groups, Consumer> sink) { if (context.getValue() == null || !DescriptorManager.isCascaded(descriptor)) { return; } + final Set groupConversions = descriptor.getGroupConversions(); + if (groupConversions.isEmpty()) { + if (groups instanceof Group) { + // a single group: applyTo would just call back once, and the result is not needed + cascade(groups, sink); + } else { + groups.applyTo(noViolations(gs -> cascade(gs, sink))); + } + return; + } final Map conversions = new HashMap<>(); - for (final GroupConversionDescriptor gc : descriptor.getGroupConversions()) { + for (final GroupConversionDescriptor gc : groupConversions) { conversions.put(Group.of(gc.getFrom()), validatorContext.getGroupsComputer().computeGroups(gc.getTo()).asStrategy()); } @@ -448,18 +458,20 @@ private void cascade(GroupStrategy groups, Consumer> sink throw new ValidationException(e); } } - multiplexEach(cx -> { - if (cx.getValue() != null && !cx.isRecursive()) { - new BeanFrame<>(this, cx).process(groups, sink); - } - }); + multiplexEach(groups, sink); } protected GraphContext getMultiplexContext() { return context; } - private void multiplexEach(Consumer consumer) { + private void cascadeInto(GraphContext cx, GroupStrategy groups, Consumer> sink) { + if (cx.getValue() != null && !cx.isRecursive()) { + new BeanFrame<>(this, cx).process(groups, sink); + } + } + + private void multiplexEach(GroupStrategy groups, Consumer> sink) { final GraphContext multiplexContext = getMultiplexContext(); final Object value = multiplexContext.getValue(); if (value == null) { @@ -469,37 +481,37 @@ private void multiplexEach(Consumer consumer) { // inconsistent: use Object[] here but specific type for Iterable? RI compatibility final Class arrayType = value instanceof Object[] ? Object[].class : value.getClass(); for (int i = 0, n = Array.getLength(value); i < n; i++) { - consumer.accept( - multiplexContext.child(NodeImpl.atIndex(i).inContainer(arrayType, null), Array.get(value, i))); + cascadeInto(multiplexContext.child(NodeImpl.atIndex(i).inContainer(arrayType, null), + Array.get(value, i)), groups, sink); } return; } if (Map.class.isInstance(value)) { for (final Map.Entry e : ((Map) value).entrySet()) { - consumer.accept(multiplexContext.child( + cascadeInto(multiplexContext.child( setContainerInformation(NodeImpl.atKey(e.getKey()), MAP_VALUE, descriptor.getElementClass()), - e.getValue())); + e.getValue()), groups, sink); } return; } if (List.class.isInstance(value)) { final List l = (List) value; for (int i = 0, n = l.size(); i < n; i++) { - consumer.accept(multiplexContext.child( + cascadeInto(multiplexContext.child( setContainerInformation(NodeImpl.atIndex(i), ITERABLE_ELEMENT, descriptor.getElementClass()), - l.get(i))); + l.get(i)), groups, sink); } return; } if (Iterable.class.isInstance(value)) { for (final Object o : (Iterable) value) { - consumer.accept(multiplexContext.child( + cascadeInto(multiplexContext.child( setContainerInformation(NodeImpl.atIndex(null), ITERABLE_ELEMENT, descriptor.getElementClass()), - o)); + o), groups, sink); } return; } - consumer.accept(multiplexContext); + cascadeInto(multiplexContext, groups, sink); } // RI apparently wants to use e.g. Set for Iterable containers, so use declared type + assigned type From d0bfa4cc761f796edf01d8bbbcf811d890b5b3ab Mon Sep 17 00:00:00 2001 From: Markus Jung Date: Mon, 5 Oct 2026 09:52:41 +0200 Subject: [PATCH 4/4] Reduce executable validation overhead - Cache executable descriptors per executable instead of hashing a new Signature for every call, and resolve them once per validation. - Reuse the executable's base path and the bean's Default group redefinition, and create parameter frames without streams. - Cache default parameter names per executable. - Map wrapper types to primitives with a lookup rather than a scan. --- .../org/apache/bval/jsr/descriptor/BeanD.java | 30 +++++++++ .../bval/jsr/job/ValidateExecutable.java | 27 +++++++- .../bval/jsr/job/ValidateParameters.java | 67 +++++++++---------- .../bval/jsr/job/ValidateReturnValue.java | 15 ++--- .../DefaultParameterNameProvider.java | 15 ++++- .../bval/util/reflection/Reflection.java | 11 ++- 6 files changed, 112 insertions(+), 53 deletions(-) diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/BeanD.java b/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/BeanD.java index 7942c5f25d..cb1ef473ca 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/BeanD.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/descriptor/BeanD.java @@ -18,6 +18,9 @@ */ package org.apache.bval.jsr.descriptor; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; @@ -26,9 +29,12 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import jakarta.validation.metadata.BeanDescriptor; import jakarta.validation.metadata.ConstructorDescriptor; +import jakarta.validation.metadata.ExecutableDescriptor; import jakarta.validation.metadata.MethodDescriptor; import jakarta.validation.metadata.MethodType; import jakarta.validation.metadata.PropertyDescriptor; @@ -65,6 +71,11 @@ public class BeanD extends ElementD, MetadataReader.ForBean> impl private final GroupStrategyMap[]> propertiesByGroups = new GroupStrategyMap<>(); private final GroupStrategyMap plans = new GroupStrategyMap<>(); + /** Descriptors by executable, {@link #NOT_CONSTRAINED} for unconstrained ones. */ + private final ConcurrentMap executableDescriptors = new ConcurrentHashMap<>(); + + private static final Object NOT_CONSTRAINED = new Object(); + private static final GroupStrategy UNCHANGED = GroupStrategy.simple(Collections.emptySet()); BeanD(MetadataReader.ForBean reader) { @@ -134,6 +145,25 @@ public Set getConstrainedMethods(MethodType methodType, Method }); } + /** + * Like {@link #getConstraintsForMethod(String, Class...)} or {@link #getConstraintsForConstructor(Class...)} for + * {@code executable}, cached by executable. + * + * @param executable + * @return {@link ExecutableDescriptor} or {@code null} if {@code executable} is not constrained + */ + public ExecutableDescriptor getConstraintsForExecutable(Executable executable) { + Object result = executableDescriptors.get(executable); + if (result == null) { + final ExecutableDescriptor descriptor = executable instanceof Method + ? getConstraintsForMethod(executable.getName(), executable.getParameterTypes()) + : getConstraintsForConstructor(((Constructor) executable).getParameterTypes()); + result = descriptor == null ? NOT_CONSTRAINED : descriptor; + executableDescriptors.putIfAbsent(executable, result); + } + return result == NOT_CONSTRAINED ? null : (ExecutableDescriptor) result; + } + @Override public ConstructorDescriptor getConstraintsForConstructor(Class... parameterTypes) { return constructors.get(new Signature(beanClass.getName(), parameterTypes)); diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateExecutable.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateExecutable.java index b222f3ebeb..6b4ca5b998 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateExecutable.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateExecutable.java @@ -25,6 +25,7 @@ import jakarta.validation.Path; import jakarta.validation.Path.Node; +import jakarta.validation.metadata.ExecutableDescriptor; import org.apache.bval.jsr.ApacheFactoryContext; import org.apache.bval.jsr.metadata.Meta; @@ -44,6 +45,9 @@ public abstract class ValidateExecutable extends Valida protected final E executable; private final Function executableNode; + private PathImpl basePath; + private ExecutableDescriptor executableDescriptor; + private boolean described; @SuppressWarnings("unchecked") public ValidateExecutable(ApacheFactoryContext validatorContext, Class[] groups, Meta meta) { @@ -64,8 +68,25 @@ public ValidateExecutable(ApacheFactoryContext validatorContext, Class[] grou } protected PathImpl createBasePath() { - final PathImpl path = PathImpl.create(); - path.addNode(executableNode.apply(executable)); - return path; + if (basePath == null) { + basePath = PathImpl.create(); + basePath.addNode(executableNode.apply(executable)); + } + return PathImpl.copy(basePath); } + + /** + * Get the descriptor of {@link #executable}, computed once per job. + * + * @return {@link ExecutableDescriptor} or {@code null} if the executable is not constrained + */ + protected final ExecutableDescriptor getExecutableDescriptor() { + if (!described) { + executableDescriptor = describe(); + described = true; + } + return executableDescriptor; + } + + protected abstract ExecutableDescriptor describe(); } \ No newline at end of file diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateParameters.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateParameters.java index 2f67de4171..517ae37001 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateParameters.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateParameters.java @@ -22,31 +22,27 @@ import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Type; -import java.util.Collections; import java.util.List; -import java.util.Set; import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import jakarta.validation.ConstraintViolation; import jakarta.validation.ParameterNameProvider; import jakarta.validation.constraintvalidation.ValidationTarget; import jakarta.validation.metadata.ExecutableDescriptor; +import jakarta.validation.metadata.ParameterDescriptor; import org.apache.bval.jsr.ApacheFactoryContext; import org.apache.bval.jsr.ConstraintViolationImpl; import org.apache.bval.jsr.GraphContext; +import org.apache.bval.jsr.descriptor.BeanD; import org.apache.bval.jsr.descriptor.ConstraintD; import org.apache.bval.jsr.descriptor.CrossParameterD; import org.apache.bval.jsr.descriptor.ParameterD; -import org.apache.bval.jsr.groups.Group; import org.apache.bval.jsr.groups.GroupStrategy; import org.apache.bval.jsr.metadata.Meta; import org.apache.bval.jsr.util.NodeImpl; import org.apache.bval.jsr.util.PathImpl; import org.apache.bval.util.Exceptions; -import org.apache.bval.util.Lazy; import org.apache.bval.util.Validate; import org.apache.bval.util.reflection.TypeUtils; @@ -63,8 +59,8 @@ public static class ForMethod extends ValidateParameters { @Override protected ExecutableDescriptor describe() { - return validatorContext.getDescriptorManager().getBeanDescriptor(object.getClass()) - .getConstraintsForMethod(executable.getName(), executable.getParameterTypes()); + return ((BeanD) validatorContext.getDescriptorManager().getBeanDescriptor(object.getClass())) + .getConstraintsForExecutable(executable); } @SuppressWarnings("unchecked") @@ -94,8 +90,8 @@ public static class ForConstructor extends ValidateParameters) validatorContext.getDescriptorManager().getBeanDescriptor(executable.getDeclaringClass())) + .getConstraintsForExecutable(executable); } @SuppressWarnings("unchecked") @@ -132,33 +128,34 @@ protected ValidationTarget getValidationTarget() { @Override void process(GroupStrategy groups, Consumer> sink) { Validate.notNull(sink, "sink"); - final Lazy>> parameterFrames = new Lazy<>(this::parameterFrames); - - GroupStrategy.redefining(groups, Collections.singletonMap(Group.DEFAULT, descriptor.getGroupStrategy())) - .applyTo(noViolations(gs -> { - validateDescriptorConstraints(gs, sink); - parameterFrames.get().forEach(p -> p.validateDescriptorConstraints(gs, sink)); - })); - parameterFrames.get().forEach(p -> p.recurse(groups, sink)); + final List parameters = executableDescriptor.getParameterDescriptors(); + @SuppressWarnings("unchecked") + final Frame[] parameterFrames = new Frame[parameters.size()]; + for (int i = 0; i < parameterFrames.length; i++) { + final ParameterD pd = (ParameterD) parameters.get(i); + parameterFrames[i] = new SproutFrame>(this, pd, parameter(pd.getIndex())); + } + descriptor.getLocalGroupStrategy(groups).applyTo(noViolations(gs -> { + validateDescriptorConstraints(gs, sink); + for (final Frame p : parameterFrames) { + p.validateDescriptorConstraints(gs, sink); + } + })); + for (final Frame p : parameterFrames) { + p.recurse(groups, sink); + } } @Override Object getBean() { return object; } - - private Set> parameterFrames() { - return executableDescriptor.getParameterDescriptors().stream() - .map(pd -> new SproutFrame>(this, (ParameterD) pd, parameter(pd.getIndex()))) - .collect(Collectors.toSet()); - } } private static final String PARAMETERS_DO_NOT_MATCH = "Parameters do not match"; protected final T object; - protected final Lazy> parameterNames = - new Lazy<>(() -> getParameterNames(validatorContext.getParameterNameProvider())); + private List parameterNames; private final Object[] parameterValues; @@ -172,25 +169,24 @@ private Set> parameterFrames() { final Type[] genericParameterTypes = executable.getGenericParameterTypes(); Exceptions.raiseUnless(parameterValues.length == genericParameterTypes.length, IllegalArgumentException::new, PARAMETERS_DO_NOT_MATCH); - IntStream.range(0, genericParameterTypes.length) - .forEach(n -> Exceptions.raiseUnless(TypeUtils.isInstance(parameterValues[n], genericParameterTypes[n]), - IllegalArgumentException::new, PARAMETERS_DO_NOT_MATCH)); + for (int n = 0; n < genericParameterTypes.length; n++) { + Exceptions.raiseUnless(TypeUtils.isInstance(parameterValues[n], genericParameterTypes[n]), + IllegalArgumentException::new, PARAMETERS_DO_NOT_MATCH); + } } @Override protected Frame computeBaseFrame() { - return new ParametersFrame(describe(), new GraphContext(validatorContext, + return new ParametersFrame(getExecutableDescriptor(), new GraphContext(validatorContext, createBasePath().addNode(new NodeImpl.CrossParameterNodeImpl()), parameterValues)); } @Override protected boolean hasWork() { - final ExecutableDescriptor descriptor = describe(); + final ExecutableDescriptor descriptor = getExecutableDescriptor(); return descriptor != null && descriptor.hasConstrainedParameters(); } - protected abstract ExecutableDescriptor describe(); - protected abstract List getParameterNames(ParameterNameProvider parameterNameProvider); protected abstract T getRootBean(); @@ -205,7 +201,10 @@ ConstraintViolationImpl createViolation(String messageTemplate, String messag private GraphContext parameter(int i) { final PathImpl path = createBasePath(); - path.addNode(new NodeImpl.ParameterNodeImpl(parameterNames.get().get(i), i)); + if (parameterNames == null) { + parameterNames = getParameterNames(validatorContext.getParameterNameProvider()); + } + path.addNode(new NodeImpl.ParameterNodeImpl(parameterNames.get(i), i)); return new GraphContext(validatorContext, path, parameterValues[i]); } } diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java index e22431ae3a..a2a6ba8dca 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/job/ValidateReturnValue.java @@ -26,6 +26,7 @@ import org.apache.bval.jsr.ApacheFactoryContext; import org.apache.bval.jsr.ConstraintViolationImpl; import org.apache.bval.jsr.GraphContext; +import org.apache.bval.jsr.descriptor.BeanD; import org.apache.bval.jsr.descriptor.ConstraintD; import org.apache.bval.jsr.descriptor.ReturnValueD; import org.apache.bval.jsr.metadata.Meta; @@ -60,8 +61,8 @@ protected Class getRootBeanClass() { @Override protected ExecutableDescriptor describe() { - return validatorContext.getDescriptorManager().getBeanDescriptor(object.getClass()) - .getConstraintsForMethod(executable.getName(), executable.getParameterTypes()); + return ((BeanD) validatorContext.getDescriptorManager().getBeanDescriptor(object.getClass())) + .getConstraintsForExecutable(executable); } @Override @@ -97,8 +98,8 @@ protected Class getRootBeanClass() { @Override protected ExecutableDescriptor describe() { - return validatorContext.getDescriptorManager().getBeanDescriptor(executable.getDeclaringClass()) - .getConstraintsForConstructor(executable.getParameterTypes()); + return ((BeanD) validatorContext.getDescriptorManager().getBeanDescriptor(executable.getDeclaringClass())) + .getConstraintsForExecutable(executable); } @Override @@ -127,7 +128,7 @@ Object getBean() { @Override protected Frame computeBaseFrame() { - return createBaseFrame((ReturnValueD) describe().getReturnValueDescriptor(), new GraphContext( + return createBaseFrame((ReturnValueD) getExecutableDescriptor().getReturnValueDescriptor(), new GraphContext( validatorContext, createBasePath().addNode(new NodeImpl.ReturnValueNodeImpl()), returnValue)); } @@ -141,12 +142,10 @@ ConstraintViolationImpl createViolation(String messageTemplate, String messag @Override protected boolean hasWork() { - final ExecutableDescriptor descriptor = describe(); + final ExecutableDescriptor descriptor = getExecutableDescriptor(); return descriptor != null && descriptor.hasConstrainedReturnValue(); } - protected abstract ExecutableDescriptor describe(); - protected abstract T getRootBean(); protected abstract Frame createBaseFrame(ReturnValueD descriptor, GraphContext context); diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/parameter/DefaultParameterNameProvider.java b/bval-jsr/src/main/java/org/apache/bval/jsr/parameter/DefaultParameterNameProvider.java index 353c8b3f51..eedd243783 100644 --- a/bval-jsr/src/main/java/org/apache/bval/jsr/parameter/DefaultParameterNameProvider.java +++ b/bval-jsr/src/main/java/org/apache/bval/jsr/parameter/DefaultParameterNameProvider.java @@ -22,7 +22,10 @@ import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.util.Collections; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -30,8 +33,16 @@ public class DefaultParameterNameProvider implements ParameterNameProvider { - private static List parameterNames(Executable exe) { - return Stream.of(exe.getParameters()).map(Parameter::getName).collect(Collectors.toList()); + private final ConcurrentMap> parameterNames = new ConcurrentHashMap<>(); + + private List parameterNames(Executable exe) { + List result = parameterNames.get(exe); + if (result == null) { + result = Collections.unmodifiableList( + Stream.of(exe.getParameters()).map(Parameter::getName).collect(Collectors.toList())); + parameterNames.putIfAbsent(exe, result); + } + return result; } @Override diff --git a/bval-jsr/src/main/java/org/apache/bval/util/reflection/Reflection.java b/bval-jsr/src/main/java/org/apache/bval/util/reflection/Reflection.java index 8c10c7d86d..7c84b8c0f8 100644 --- a/bval-jsr/src/main/java/org/apache/bval/util/reflection/Reflection.java +++ b/bval-jsr/src/main/java/org/apache/bval/util/reflection/Reflection.java @@ -149,6 +149,7 @@ public enum Interfaces { * Maps primitive {@code Class}es to their corresponding wrapper {@code Class}. */ private static final Map, Class> PRIMITIVE_WRAPPER_MAP; + private static final Map, Class> WRAPPER_PRIMITIVE_MAP; static { final Map, Class> m = new HashMap<>(); m.put(Boolean.TYPE, Boolean.class); @@ -161,6 +162,9 @@ public enum Interfaces { m.put(Float.TYPE, Float.class); m.put(Void.TYPE, Void.TYPE); PRIMITIVE_WRAPPER_MAP = Collections.unmodifiableMap(m); + final Map, Class> reverse = new HashMap<>(); + m.forEach((primitive, wrapper) -> reverse.put(wrapper, primitive)); + WRAPPER_PRIMITIVE_MAP = Collections.unmodifiableMap(reverse); } /** @@ -184,12 +188,7 @@ public static Class primitiveToWrapper(final Class cls) { } public static Class wrapperToPrimitive(final Class cls) { - for (Map.Entry, Class> primitiveEntry : PRIMITIVE_WRAPPER_MAP.entrySet()) { - if (primitiveEntry.getValue().equals(cls)) { - return primitiveEntry.getKey(); - } - } - return null; + return WRAPPER_PRIMITIVE_MAP.get(cls); } /**