Configure auto grow collection limit in DataBinder

Prior to this commit, the `GraphQlArgumentBinder` would use the default
`DataBinder` configuration, including its
`DEFAULT_AUTO_GROW_COLLECTION_LIMIT` which is 256. While this is a
sensible default value for many use cases, GraphQL applications are data
oriented.

This commit configures a larger default for GraphQL applications at
1024. This also provides a way to configure the underlying data binder
before the binding proces starts.

Closes gh-392
This commit is contained in:
Brian Clozel
2022-07-18 22:51:08 +02:00
parent 4dd13ed8e6
commit 96f158b682
3 changed files with 60 additions and 0 deletions

View File

@@ -17,12 +17,14 @@
package org.springframework.graphql.data;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Stack;
import java.util.function.Consumer;
import graphql.schema.DataFetchingEnvironment;
@@ -59,11 +61,18 @@ import org.springframework.validation.FieldError;
*/
public class GraphQlArgumentBinder {
/**
* Use a larger {@link DataBinder#DEFAULT_AUTO_GROW_COLLECTION_LIMIT} for GraphQL use cases
*/
private static final int DEFAULT_AUTO_GROW_COLLECTION_LIMIT = 1024;
@Nullable
private final SimpleTypeConverter typeConverter;
private final BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();
private List<Consumer<DataBinder>> dataBinderInitializers = new ArrayList<>();
public GraphQlArgumentBinder() {
this(null);
@@ -90,6 +99,15 @@ public class GraphQlArgumentBinder {
return (this.typeConverter != null ? this.typeConverter.getConversionService() : null);
}
/**
* Add a {@link DataBinder} consumer that initializes the binder instance before the binding process.
* @param dataBinderInitializer the data binder initializer
* @since 1.0.1
*/
public void addDataBinderInitializer(Consumer<DataBinder> dataBinderInitializer) {
this.dataBinderInitializers.add(dataBinderInitializer);
}
/**
* Bind a single argument, or the full arguments map, onto an object of the
@@ -122,6 +140,7 @@ public class GraphQlArgumentBinder {
Assert.notNull(targetClass, "Could not determine target type from " + targetType);
DataBinder binder = new DataBinder(null, argumentName != null ? argumentName : "arguments");
initDataBinder(binder);
BindingResult bindingResult = binder.getBindingResult();
Stack<String> segments = new Stack<>();
@@ -159,6 +178,11 @@ public class GraphQlArgumentBinder {
}
}
private void initDataBinder(DataBinder binder) {
binder.setAutoGrowCollectionLimit(DEFAULT_AUTO_GROW_COLLECTION_LIMIT);
this.dataBinderInitializers.forEach(initializer -> initializer.accept(binder));
}
@Nullable
private Object wrapAsOptionalIfNecessary(@Nullable Object value, ResolvableType type) {
return (type.resolve(Object.class).equals(Optional.class) ? Optional.ofNullable(value) : value);
@@ -228,6 +252,7 @@ public class GraphQlArgumentBinder {
if (ctor.getParameterCount() == 0) {
target = BeanUtils.instantiateClass(ctor);
DataBinder dataBinder = new DataBinder(target);
initDataBinder(dataBinder);
dataBinder.getBindingResult().setNestedPath(toArgumentPath(segments));
dataBinder.setConversionService(getConversionService());
dataBinder.bind(initBindValues(rawMap));

View File

@@ -27,6 +27,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.validation.Validator;
@@ -69,6 +70,7 @@ import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.DataBinder;
/**
* {@link RuntimeWiringConfigurer} that detects {@link SchemaMapping @SchemaMapping}
@@ -127,6 +129,9 @@ public class AnnotatedControllerConfigurer
@Nullable
private HandlerMethodInputValidator validator;
@Nullable
private Consumer<DataBinder> dataBinderInitializer;
/**
* Add a {@code FormatterRegistrar} to customize the {@link ConversionService}
@@ -149,6 +154,15 @@ public class AnnotatedControllerConfigurer
this.executor = executor;
}
/**
* Configure an initializer that configures the {@link DataBinder} before the binding process.
* @param dataBinderInitializer the data binder initializer
* @since 1.0.1
*/
public void setDataBinderInitializer(@Nullable Consumer<DataBinder> dataBinderInitializer) {
this.dataBinderInitializer = dataBinderInitializer;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
@@ -176,6 +190,9 @@ public class AnnotatedControllerConfigurer
}
resolvers.addResolver(new ArgumentMapMethodArgumentResolver());
GraphQlArgumentBinder argumentBinder = new GraphQlArgumentBinder(this.conversionService);
if (this.dataBinderInitializer != null) {
argumentBinder.addDataBinderInitializer(this.dataBinderInitializer);
}
resolvers.addResolver(new ArgumentMethodArgumentResolver(argumentBinder));
resolvers.addResolver(new ArgumentsMethodArgumentResolver(argumentBinder));
resolvers.addResolver(new ContextValueMethodArgumentResolver());

View File

@@ -16,10 +16,14 @@
package org.springframework.graphql.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -30,6 +34,8 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.ResolvableType;
import org.springframework.graphql.Book;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
@@ -227,6 +233,7 @@ class GraphQlArgumentBinderTests {
}
@Test // gh-410
@SuppressWarnings("unchecked")
void coercionWithSingletonList() throws Exception {
Map<String, String> itemMap = new HashMap<>();
@@ -250,6 +257,17 @@ class GraphQlArgumentBinderTests {
assertThat(items.get(0).getAge()).isEqualTo(37);
}
@Test // gh-392
@SuppressWarnings("unchecked")
void shouldHaveHigherDefaultAutoGrowLimit() throws Exception {
String items = IntStream.range(0, 260).mapToObj(value -> "{\"name\":\"test\"}").collect(Collectors.joining(","));
Object result = this.binder.bind(
environment("{\"key\":{\"items\":[" + items + "]}}"), "key",
ResolvableType.forClass(ItemListHolder.class));
assertThat(result).isNotNull().isInstanceOf(ItemListHolder.class);
assertThat(((ItemListHolder) result).getItems()).hasSize(260);
}
@SuppressWarnings("unchecked")
private DataFetchingEnvironment environment(String jsonPayload) throws JsonProcessingException {