You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bval.apache.org by ju...@apache.org on 2026/10/05 11:19:50 UTC

(bval) 04/04: Reduce executable validation overhead

This is an automated email from the ASF dual-hosted git repository.

jungm pushed a commit to branch perf-5-cascading-executables
in repository https://gitbox.apache.org/repos/asf/bval.git

commit 944a9891606b262cf0eb9a0031f4c737b9cb56ba
Author: Markus Jung <ju...@apache.org>
AuthorDate: Mon Oct 5 09:52:41 2026 +0200

    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.
---
 .../java/org/apache/bval/jsr/descriptor/BeanD.java | 30 ++++++++++
 .../apache/bval/jsr/job/ValidateExecutable.java    | 27 ++++++++-
 .../apache/bval/jsr/job/ValidateParameters.java    | 67 +++++++++++-----------
 .../apache/bval/jsr/job/ValidateReturnValue.java   | 15 +++--
 .../parameter/DefaultParameterNameProvider.java    | 15 ++++-
 .../apache/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 872db929..a6c7e90d 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.HashMap;
 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<T> extends ElementD<Class<T>, MetadataReader.ForBean<T>> impl
     private final GroupStrategyMap<PropertyD<?>[]> propertiesByGroups = new GroupStrategyMap<>();
     private final GroupStrategyMap<GroupPlan> plans = new GroupStrategyMap<>();
 
+    /** Descriptors by executable, {@link #NOT_CONSTRAINED} for unconstrained ones. */
+    private final ConcurrentMap<Executable, Object> executableDescriptors = new ConcurrentHashMap<>();
+
+    private static final Object NOT_CONSTRAINED = new Object();
+
     private static final GroupStrategy UNCHANGED = GroupStrategy.simple(Collections.emptySet());
 
     BeanD(MetadataReader.ForBean<T> reader) {
@@ -134,6 +145,25 @@ public class BeanD<T> extends ElementD<Class<T>, MetadataReader.ForBean<T>> impl
         });
     }
 
+    /**
+     * 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 b222f3eb..6b4ca5b9 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 java.util.function.Function;
 
 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<E extends Executable, T> extends Valida
     protected final E executable;
 
     private final Function<E, Path.Node> executableNode;
+    private PathImpl basePath;
+    private ExecutableDescriptor executableDescriptor;
+    private boolean described;
 
     @SuppressWarnings("unchecked")
     public ValidateExecutable(ApacheFactoryContext validatorContext, Class<?>[] groups, Meta<E> meta) {
@@ -64,8 +68,25 @@ public abstract class ValidateExecutable<E extends Executable, T> extends Valida
     }
 
     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 2f67de41..517ae370 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.Constructor;
 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 abstract class ValidateParameters<E extends Executable, T> extends Valida
 
         @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 abstract class ValidateParameters<E extends Executable, T> extends Valida
 
         @Override
         protected ExecutableDescriptor describe() {
-            return validatorContext.getDescriptorManager().getBeanDescriptor(executable.getDeclaringClass())
-                .getConstraintsForConstructor(executable.getParameterTypes());
+            return ((BeanD<?>) validatorContext.getDescriptorManager().getBeanDescriptor(executable.getDeclaringClass()))
+                .getConstraintsForExecutable(executable);
         }
 
         @SuppressWarnings("unchecked")
@@ -132,33 +128,34 @@ public abstract class ValidateParameters<E extends Executable, T> extends Valida
         @Override
         void process(GroupStrategy groups, Consumer<ConstraintViolation<T>> sink) {
             Validate.notNull(sink, "sink");
-            final Lazy<Set<Frame<?>>> 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<ParameterDescriptor> 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<ParameterD<?>>(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<Frame<?>> parameterFrames() {
-            return executableDescriptor.getParameterDescriptors().stream()
-                .map(pd -> new SproutFrame<ParameterD<?>>(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<List<String>> parameterNames =
-        new Lazy<>(() -> getParameterNames(validatorContext.getParameterNameProvider()));
+    private List<String> parameterNames;
 
     private final Object[] parameterValues;
 
@@ -172,25 +169,24 @@ public abstract class ValidateParameters<E extends Executable, T> extends Valida
         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<String> getParameterNames(ParameterNameProvider parameterNameProvider);
 
     protected abstract T getRootBean();
@@ -205,7 +201,10 @@ public abstract class ValidateParameters<E extends Executable, T> extends Valida
 
     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 e22431ae..a2a6ba8d 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 jakarta.validation.metadata.ExecutableDescriptor;
 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 @@ public abstract class ValidateReturnValue<E extends Executable, T> extends Valid
 
         @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 @@ public abstract class ValidateReturnValue<E extends Executable, T> extends Valid
 
         @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 @@ public abstract class ValidateReturnValue<E extends Executable, T> extends Valid
 
     @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 @@ public abstract class ValidateReturnValue<E extends Executable, T> extends Valid
 
     @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 353c8b3f..eedd2437 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.Constructor;
 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 @@ import jakarta.validation.ParameterNameProvider;
 
 public class DefaultParameterNameProvider implements ParameterNameProvider {
 
-    private static List<String> parameterNames(Executable exe) {
-        return Stream.of(exe.getParameters()).map(Parameter::getName).collect(Collectors.toList());
+    private final ConcurrentMap<Executable, List<String>> parameterNames = new ConcurrentHashMap<>();
+
+    private List<String> parameterNames(Executable exe) {
+        List<String> 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 8c10c7d8..7c84b8c0 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 class Reflection {
      * Maps primitive {@code Class}es to their corresponding wrapper {@code Class}.
      */
     private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPER_MAP;
+    private static final Map<Class<?>, Class<?>> WRAPPER_PRIMITIVE_MAP;
     static {
         final Map<Class<?>, Class<?>> m = new HashMap<>();
         m.put(Boolean.TYPE, Boolean.class);
@@ -161,6 +162,9 @@ public class Reflection {
         m.put(Float.TYPE, Float.class);
         m.put(Void.TYPE, Void.TYPE);
         PRIMITIVE_WRAPPER_MAP = Collections.unmodifiableMap(m);
+        final Map<Class<?>, Class<?>> reverse = new HashMap<>();
+        m.forEach((primitive, wrapper) -> reverse.put(wrapper, primitive));
+        WRAPPER_PRIMITIVE_MAP = Collections.unmodifiableMap(reverse);
     }
 
     /**
@@ -184,12 +188,7 @@ public class Reflection {
     }
 
     public static Class<?> wrapperToPrimitive(final Class<?> cls) {
-        for (Map.Entry<Class<?>, Class<?>> primitiveEntry : PRIMITIVE_WRAPPER_MAP.entrySet()) {
-            if (primitiveEntry.getValue().equals(cls)) {
-                return primitiveEntry.getKey();
-            }
-        }
-        return null;
+        return WRAPPER_PRIMITIVE_MAP.get(cls);
     }
 
     /**