Optimize performance of method validation

Perform the check for whether validation for a HandlerMethod is needed
earlier and only once rather than in the constructor of
DataFetcherHandlerMethod.

Make the validation helper passed to DataFetcherHandlerMethod stateful,
so that validation groups are also determined once on startup.

See gh-571
This commit is contained in:
rstoyanchev
2023-01-18 16:06:50 +00:00
parent 4c1e7c56e5
commit f65b83f65e
5 changed files with 208 additions and 234 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -125,7 +125,7 @@ public class AnnotatedControllerConfigurer
private HandlerMethodArgumentResolverComposite argumentResolvers;
@Nullable
private HandlerMethodValidationHelper validationHelper;
private ValidationHelper validationHelper;
/**
@@ -176,8 +176,7 @@ public class AnnotatedControllerConfigurer
this.argumentResolvers = initArgumentResolvers();
if (beanValidationPresent) {
this.validationHelper =
HandlerMethodValidationHelper.createIfValidatorAvailable(obtainApplicationContext());
this.validationHelper = ValidationHelper.createIfValidatorPresent(obtainApplicationContext());
}
}
@@ -228,7 +227,8 @@ public class AnnotatedControllerConfigurer
findHandlerMethods().forEach((info) -> {
DataFetcher<?> dataFetcher;
if (!info.isBatchMapping()) {
dataFetcher = new SchemaMappingDataFetcher(info, this.argumentResolvers, this.validationHelper, this.executor);
dataFetcher = new SchemaMappingDataFetcher(
info, this.argumentResolvers, this.validationHelper, this.executor);
}
else {
String dataLoaderKey = registerBatchLoader(info);
@@ -492,7 +492,7 @@ public class AnnotatedControllerConfigurer
private final HandlerMethodArgumentResolverComposite argumentResolvers;
@Nullable
private final HandlerMethodValidationHelper validatorHelper;
private final Consumer<Object[]> methodValidationHelper;
@Nullable
private final Executor executor;
@@ -501,12 +501,12 @@ public class AnnotatedControllerConfigurer
SchemaMappingDataFetcher(
MappingInfo info, HandlerMethodArgumentResolverComposite resolvers,
@Nullable HandlerMethodValidationHelper validatorHelper,
@Nullable Executor executor) {
@Nullable ValidationHelper validationHelper, @Nullable Executor executor) {
this.info = info;
this.argumentResolvers = resolvers;
this.validatorHelper = validatorHelper;
this.methodValidationHelper = (validationHelper != null ?
validationHelper.getValidationHelperFor(info.getHandlerMethod()) : null);
this.executor = executor;
this.subscription = this.info.getCoordinates().getTypeName().equalsIgnoreCase("Subscription");
}
@@ -524,7 +524,8 @@ public class AnnotatedControllerConfigurer
public Object get(DataFetchingEnvironment environment) throws Exception {
DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod(
getHandlerMethod(), this.argumentResolvers, this.validatorHelper, this.executor, this.subscription);
getHandlerMethod(), this.argumentResolvers, this.methodValidationHelper,
this.executor, this.subscription);
return handlerMethod.invoke(environment);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ package org.springframework.graphql.data.method.annotation.support;
import java.util.Arrays;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import graphql.schema.DataFetchingEnvironment;
import org.reactivestreams.Publisher;
@@ -49,8 +50,7 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport {
private final HandlerMethodArgumentResolverComposite resolvers;
@Nullable
private final HandlerMethodValidationHelper validator;
private final Consumer<Object[]> validationHelper;
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@@ -61,17 +61,17 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport {
* Constructor with a parent handler method.
* @param handlerMethod the handler method
* @param resolvers the argument resolvers
* @param validator the input validator
* @param validationHelper to apply bean validation with
* @param subscription whether the field being fetched is of subscription type
*/
public DataFetcherHandlerMethod(HandlerMethod handlerMethod,
HandlerMethodArgumentResolverComposite resolvers, @Nullable HandlerMethodValidationHelper validator,
@Nullable Executor executor, boolean subscription) {
public DataFetcherHandlerMethod(
HandlerMethod handlerMethod, HandlerMethodArgumentResolverComposite resolvers,
@Nullable Consumer<Object[]> validationHelper, @Nullable Executor executor, boolean subscription) {
super(handlerMethod, executor);
Assert.isTrue(!resolvers.getResolvers().isEmpty(), "No argument resolvers");
this.resolvers = resolvers;
this.validator = (validator != null && validator.requiresValidation(handlerMethod) ? validator : null);
this.validationHelper = (validationHelper != null ? validationHelper : args -> {});
this.subscription = subscription;
}
@@ -83,15 +83,6 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport {
return this.resolvers;
}
/**
* Return the configured input validator.
* @deprecated as of 1.1 without a replacement
*/
@Deprecated
@Nullable
public HandlerMethodValidationHelper getValidator() {
return this.validator;
}
/**
@@ -188,9 +179,7 @@ public class DataFetcherHandlerMethod extends InvocableHandlerMethodSupport {
@Nullable
private Object validateAndInvoke(Object[] args, DataFetchingEnvironment environment) {
if (this.validator != null) {
this.validator.validate(this, args);
}
this.validationHelper.accept(args);
return doInvoke(environment.getGraphQlContext(), args);
}

View File

@@ -1,146 +0,0 @@
/*
* Copyright 2020-2022 the original author or 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 org.springframework.graphql.data.method.annotation.support;
import java.lang.annotation.Annotation;
import java.util.Set;
import jakarta.validation.Constraint;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import jakarta.validation.Validator;
import jakarta.validation.metadata.BeanDescriptor;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.graphql.data.method.HandlerMethod;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
/**
* Helper class to apply standard bean validation to a {@link HandlerMethod}.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
* @since 1.0
*/
final class HandlerMethodValidationHelper {
private final Validator validator;
/**
* Constructor with the {@link Validator} instance to use.
*/
public HandlerMethodValidationHelper(Validator validator) {
Assert.notNull(validator, "validator should not be null");
if (validator instanceof LocalValidatorFactoryBean) {
this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
}
else if (validator instanceof SpringValidatorAdapter) {
this.validator = validator.unwrap(Validator.class);
}
else {
this.validator = validator;
}
}
/**
* Validate the input values to a the {@link HandlerMethod} and throw a
* {@link ConstraintViolationException} in case of violations.
* @param handlerMethod the handler method to validate
* @param arguments the input argument values
*/
public void validate(HandlerMethod handlerMethod, Object[] arguments) {
Set<ConstraintViolation<Object>> result =
this.validator.forExecutables().validateParameters(
handlerMethod.getBean(), handlerMethod.getMethod(), arguments,
determineValidationGroups(handlerMethod));
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
}
/**
* Determine the validation groups to apply to a handler method, specified
* through the {@link Validated} annotation on the method or on the class.
* @param method the method to check
* @return the applicable validation groups as a Class array
*/
private Class<?>[] determineValidationGroups(HandlerMethod method) {
Validated annotation = findAnnotation(method, Validated.class);
return (annotation != null ? annotation.value() : new Class<?>[0]);
}
@Nullable
private static <A extends Annotation> A findAnnotation(HandlerMethod method, Class<A> annotationType) {
A annotation = AnnotationUtils.findAnnotation(method.getMethod(), annotationType);
if (annotation == null) {
annotation = AnnotationUtils.findAnnotation(method.getBeanType(), annotationType);
}
return annotation;
}
/**
* Whether the given method requires standard bean validation. This is the
* case if the method or one of its parameters are annotated with
* {@link Valid} or {@link Validated}, or if any method parameter is declared
* with a {@link Constraint constraint}, or the method parameter type is
* itself {@link BeanDescriptor#isBeanConstrained() constrained}.
* @param method the handler method to check
* @return {@code true} if validation is required, {@code false} otherwise
*/
public boolean requiresValidation(HandlerMethod method) {
if (findAnnotation(method, Validated.class) != null || findAnnotation(method, Valid.class) != null) {
return true;
}
for (MethodParameter parameter : method.getMethodParameters()) {
for (Annotation annotation : parameter.getParameterAnnotations()) {
MergedAnnotations merged = MergedAnnotations.from(annotation);
if (merged.isPresent(Valid.class) || merged.isPresent(Constraint.class) || merged.isPresent(Validated.class)) {
return true;
}
}
Class<?> paramType = parameter.nestedIfOptional().getNestedParameterType();
if (this.validator.getConstraintsForClass(paramType).isBeanConstrained()) {
return true;
}
}
return false;
}
/**
* Factory method for {@link HandlerMethodValidationHelper} if a
* {@link Validator} can be found.
* @param context the context to look up a {@code Validator} bean from
* @return the helper instance, or {@code null
*/
@Nullable
public static HandlerMethodValidationHelper createIfValidatorAvailable(ApplicationContext context) {
Validator validator = context.getBeanProvider(Validator.class).getIfAvailable();
return (validator != null ? new HandlerMethodValidationHelper(validator) : null);
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2002-2023 the original author or 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 org.springframework.graphql.data.method.annotation.support;
import java.lang.annotation.Annotation;
import java.util.Set;
import java.util.function.Consumer;
import jakarta.validation.Constraint;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import jakarta.validation.Validator;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.graphql.data.method.HandlerMethod;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
/**
* Holds a {@link jakarta.validation.Validator} and helps to create a validation
* callback for a given {@link HandlerMethod} if it is determined that it
* requires bean validation.
*
* @author Rossen Stoyanchev
* @since 1.2
*/
class ValidationHelper {
private final Validator validator;
private ValidationHelper(Validator validator) {
Assert.notNull(validator, "Validator is required");
this.validator = validator;
}
/**
* Create a validation callback for the given {@link HandlerMethod},
* possibly {@code null} if the method or the method parameters do not have
* {@link Validated}, {@link Valid}, or {@link Constraint} annotations.
*/
@Nullable
public Consumer<Object[]> getValidationHelperFor(HandlerMethod handlerMethod) {
boolean required = false;
Class<?>[] groups = null;
Validated validatedAnnotation = findAnnotation(handlerMethod, Validated.class);
if (validatedAnnotation != null) {
required = true;
groups = validatedAnnotation.value();
}
else if (findAnnotation(handlerMethod, Valid.class) != null) {
required = true;
}
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
if (required) {
break;
}
for (Annotation annot : parameter.getParameterAnnotations()) {
MergedAnnotations merged = MergedAnnotations.from(annot);
if (merged.isPresent(Valid.class) || merged.isPresent(Constraint.class) || merged.isPresent(Validated.class)) {
required = true;
}
}
}
return (required ? new HandlerMethodValidator(handlerMethod, groups) : null);
}
@Nullable
private <A extends Annotation> A findAnnotation(HandlerMethod method, Class<A> annotationType) {
A annotation = AnnotationUtils.findAnnotation(method.getMethod(), annotationType);
if (annotation == null) {
annotation = AnnotationUtils.findAnnotation(method.getBeanType(), annotationType);
}
return annotation;
}
/**
* Factory method to create a {@link ValidationHelper} if there is a
* {@link Validator} bean declared, or {@code null} otherwise.
*/
@Nullable
public static ValidationHelper createIfValidatorPresent(ApplicationContext context) {
Validator validator = context.getBeanProvider(Validator.class).getIfAvailable();
if (validator instanceof LocalValidatorFactoryBean) {
validator = ((LocalValidatorFactoryBean) validator).getValidator();
}
else if (validator instanceof SpringValidatorAdapter) {
validator = validator.unwrap(Validator.class);
}
return (validator != null ? create(validator) : null);
}
/**
* Factory method with a given {@link Validator} instance.
*/
public static ValidationHelper create(Validator validator) {
return new ValidationHelper(validator);
}
/**
* Callback to apply validation to the invocation of a {@link HandlerMethod}.
*/
private class HandlerMethodValidator implements Consumer<Object[]> {
private final HandlerMethod handlerMethod;
@Nullable
private final Class<?>[] validationGroups;
private HandlerMethodValidator(HandlerMethod handlerMethod, @Nullable Class<?>[] validationGroups) {
Assert.notNull(handlerMethod, "HandlerMethod is required");
this.handlerMethod = handlerMethod;
this.validationGroups = (validationGroups != null ? validationGroups : new Class<?>[] {});
}
@Override
public void accept(Object[] arguments) {
Set<ConstraintViolation<Object>> result =
ValidationHelper.this.validator.forExecutables().validateParameters(
this.handlerMethod.getBean(), this.handlerMethod.getMethod(), arguments, this.validationGroups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
import java.util.function.Consumer;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
@@ -28,7 +28,6 @@ import jakarta.validation.Valid;
import jakarta.validation.Validation;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotNull;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.IterableAssert;
import org.assertj.core.api.ThrowableAssert;
@@ -39,71 +38,63 @@ import org.springframework.graphql.data.method.HandlerMethod;
import org.springframework.validation.annotation.Validated;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link HandlerMethodValidationHelper}.
* Unit tests for {@link ValidationHelper}.
*
* @author Brian Clozel
*/
class HandlerMethodValidationHelperTests {
private final HandlerMethodValidationHelper validator =
new HandlerMethodValidationHelper(Validation.buildDefaultValidatorFactory().getValidator());
class ValidationHelperTests {
@Test
void shouldFailWithNullValidator() {
assertThatThrownBy(() -> new HandlerMethodValidationHelper(null)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> ValidationHelper.create(null)).isInstanceOf(IllegalArgumentException.class);
}
@Test
void shouldIgnoreMethodsWithoutAnnotations() {
HandlerMethod method = findHandlerMethod(MyBean.class, "notValidatedMethod");
assertThatNoException().isThrownBy(() -> validator.validate(method, new Object[] {"test", 12}));
Consumer<Object[]> validator = createValidator(MyBean.class, "notValidatedMethod");
assertThat(validator).isNull();
}
@Test
void shouldRaiseValidationErrorForAnnotatedParams() {
HandlerMethod method = findHandlerMethod(MyBean.class, "myValidMethod");
assertViolations(() -> validator.validate(method, new Object[] {null, 2}))
Consumer<Object[]> validator = createValidator(MyBean.class, "myValidMethod");
assertViolations(() -> validator.accept(new Object[] {null, 2}))
.anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethod.arg0"));
assertViolations(() -> validator.validate(method, new Object[] {"test", 12}))
assertViolations(() -> validator.accept(new Object[] {"test", 12}))
.anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethod.arg1"));
}
@Test
void shouldRaiseValidationErrorForAnnotatedParamsWithGroups() {
HandlerMethod myValidMethodWithGroup = findHandlerMethod(MyValidationGroupsBean.class, "myValidMethodWithGroup");
assertViolations(() -> validator.validate(myValidMethodWithGroup, new Object[] {null}))
.anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethodWithGroup.arg0"));
Consumer<Object[]> validator1 = createValidator(MyValidationGroupsBean.class, "myValidMethodWithGroup");
assertViolation(() -> validator1.accept(new Object[] {null}), "myValidMethodWithGroup.arg0");
HandlerMethod myValidMethodWithGroupOnType = findHandlerMethod(MyValidationGroupsBean.class, "myValidMethodWithGroupOnType");
assertViolations(() -> validator.validate(myValidMethodWithGroupOnType, new Object[] {null}))
.anyMatch(violation -> violation.getPropertyPath().toString().equals("myValidMethodWithGroupOnType.arg0"));
Consumer<Object[]> validator2 = createValidator(MyValidationGroupsBean.class, "myValidMethodWithGroupOnType");
assertViolation(() -> validator2.accept(new Object[] {null}), "myValidMethodWithGroupOnType.arg0");
}
@Test
void shouldRecognizeMethodsThatRequireValidation() {
HandlerMethod method = findHandlerMethod(RequiresValidationBean.class, "processConstrainedValue");
assertThat(validator.requiresValidation(method)).isTrue();
Consumer<Object[]> validator1 = createValidator(RequiresValidationBean.class, "processConstrainedValue");
assertThat(validator1).isNotNull();
method = findHandlerMethod(RequiresValidationBean.class, "processValidInput");
assertThat(validator.requiresValidation(method)).isTrue();
Consumer<Object[]> validator2 = createValidator(RequiresValidationBean.class, "processValidInput");
assertThat(validator2).isNotNull();
method = findHandlerMethod(RequiresValidationBean.class, "processValidatedInput");
assertThat(validator.requiresValidation(method)).isTrue();
Consumer<Object[]> validator3 = createValidator(RequiresValidationBean.class, "processValidatedInput");
assertThat(validator3).isNotNull();
method = findHandlerMethod(RequiresValidationBean.class, "processInputWithConstrainedValue");
assertThat(validator.requiresValidation(method)).isTrue();
method = findHandlerMethod(RequiresValidationBean.class, "processOptionalInputWithConstrainedValue");
assertThat(validator.requiresValidation(method)).isTrue();
method = findHandlerMethod(RequiresValidationBean.class, "processValue");
assertThat(validator.requiresValidation(method)).isFalse();
Consumer<Object[]> validator4 = createValidator(RequiresValidationBean.class, "processValue");
assertThat(validator4).isNull();
}
private Consumer<Object[]> createValidator(Class<?> handlerType, String methodName) {
return ValidationHelper.create(Validation.buildDefaultValidatorFactory().getValidator())
.getValidationHelperFor(findHandlerMethod(handlerType, methodName));
}
private HandlerMethod findHandlerMethod(Class<?> handlerType, String methodName) {
Object handler = BeanUtils.instantiateClass(handlerType);
@@ -121,6 +112,11 @@ class HandlerMethodValidationHelperTests {
.asInstanceOf(InstanceOfAssertFactories.iterable(ConstraintViolation.class));
}
private void assertViolation(ThrowableAssert.ThrowingCallable callable, String propertyPath) {
assertViolations(callable).anyMatch(violation ->
violation.getPropertyPath().toString().equals(propertyPath));
}
@SuppressWarnings("unused")
private static class MyBean {
@@ -184,34 +180,12 @@ class HandlerMethodValidationHelperTests {
public void processValidatedInput(@Validated MyInput input) {
}
public void processInputWithConstrainedValue(MyConstrainedInput input) {
}
public void processOptionalInputWithConstrainedValue(Optional<MyConstrainedInput> input) {
}
public void processValue(int i) {
}
}
private static class MyInput {
}
private static class MyConstrainedInput {
@Max(99)
private int i;
public int getI() {
return this.i;
}
public void setI(int i) {
this.i = i;
}
}
}