Skip to content

Commit 882ad0e

Browse files
committed
Allow fallback on field access for argument binding
Closes gh-599
1 parent bf1f406 commit 882ad0e

File tree

4 files changed

+84
-12
lines changed

4 files changed

+84
-12
lines changed

spring-graphql-docs/src/docs/asciidoc/index.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,6 +1514,10 @@ accordingly. For example:
15141514
}
15151515
----
15161516

1517+
TIP: If the target object doesn't have setters, and you can't change that, you can use a
1518+
property on `AnnotatedControllerConfigurer` to allow falling back on binding via direct
1519+
field access.
1520+
15171521
By default, if the method parameter name is available (requires the `-parameters` compiler
15181522
flag with Java 8+ or debugging info from the compiler), it is used to look up the argument.
15191523
If needed, you can customize the name through the annotation, e.g. `@Argument("bookInput")`.

spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.graphql.data;
1818

1919
import java.lang.reflect.Constructor;
20+
import java.lang.reflect.Field;
2021
import java.util.Collection;
2122
import java.util.Collections;
2223
import java.util.Map;
@@ -39,8 +40,10 @@
3940
import org.springframework.core.ResolvableType;
4041
import org.springframework.core.convert.ConversionService;
4142
import org.springframework.core.convert.TypeDescriptor;
43+
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
4244
import org.springframework.lang.Nullable;
4345
import org.springframework.util.ClassUtils;
46+
import org.springframework.util.ReflectionUtils;
4447
import org.springframework.validation.AbstractBindingResult;
4548
import org.springframework.validation.BindException;
4649
import org.springframework.validation.DataBinder;
@@ -78,23 +81,35 @@ public class GraphQlArgumentBinder {
7881
@Nullable
7982
private final SimpleTypeConverter typeConverter;
8083

84+
private final boolean fallBackOnDirectFieldAccess;
85+
8186

8287
public GraphQlArgumentBinder() {
8388
this(null);
8489
}
8590

8691
public GraphQlArgumentBinder(@Nullable ConversionService conversionService) {
87-
if (conversionService != null) {
88-
this.typeConverter = new SimpleTypeConverter();
89-
this.typeConverter.setConversionService(conversionService);
90-
}
91-
else {
92+
this(conversionService, false);
93+
}
94+
95+
public GraphQlArgumentBinder(@Nullable ConversionService conversionService, boolean fallBackOnDirectFieldAccess) {
96+
this.typeConverter = initTypeConverter(conversionService);
97+
this.fallBackOnDirectFieldAccess = fallBackOnDirectFieldAccess;
98+
}
99+
100+
@Nullable
101+
private static SimpleTypeConverter initTypeConverter(@Nullable ConversionService conversionService) {
102+
if (conversionService == null) {
92103
// Not thread-safe when using PropertyEditors
93-
this.typeConverter = null;
104+
return null;
94105
}
106+
SimpleTypeConverter typeConverter = new SimpleTypeConverter();
107+
typeConverter.setConversionService(conversionService);
108+
return typeConverter;
95109
}
96110

97111

112+
98113
/**
99114
* Add a {@link DataBinder} consumer that initializes the binder instance
100115
* before the binding process.
@@ -297,11 +312,18 @@ private Object bindMapToObjectViaSetters(
297312
ArgumentsBindingResult bindingResult) {
298313

299314
Object target = BeanUtils.instantiateClass(constructor);
300-
BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(target);
315+
BeanWrapper beanWrapper = (this.fallBackOnDirectFieldAccess ?
316+
new DirectFieldAccessFallbackBeanWrapper(target) : PropertyAccessorFactory.forBeanPropertyAccess(target));
301317

302318
for (Map.Entry<String, Object> entry : rawMap.entrySet()) {
303319
String key = entry.getKey();
304320
TypeDescriptor typeDescriptor = beanWrapper.getPropertyTypeDescriptor(key);
321+
if (typeDescriptor == null && this.fallBackOnDirectFieldAccess) {
322+
Field field = ReflectionUtils.findField(beanWrapper.getWrappedClass(), key);
323+
if (field != null) {
324+
typeDescriptor = new TypeDescriptor(field);
325+
}
326+
}
305327
if (typeDescriptor == null) {
306328
// Ignore unknown property
307329
continue;

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/AnnotatedControllerConfigurer.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ public class AnnotatedControllerConfigurer implements ApplicationContextAware, I
139139

140140
private final FormattingConversionService conversionService = new DefaultFormattingConversionService();
141141

142+
private boolean fallBackOnDirectFieldAccess;
143+
142144
private final List<HandlerMethodArgumentResolver> customArgumentResolvers = new ArrayList<>(8);
143145

144146
@Nullable
@@ -167,6 +169,17 @@ public void addFormatterRegistrar(FormatterRegistrar registrar) {
167169
registrar.registerFormatters(this.conversionService);
168170
}
169171

172+
/**
173+
* Whether binding GraphQL arguments onto
174+
* {@link org.springframework.graphql.data.method.annotation.Argument @Argument}
175+
* should falls back to direct field access in case the target object does
176+
* not use accessor methods.
177+
* @since 1.2
178+
*/
179+
public void setFallBackOnDirectFieldAccess(boolean fallBackOnDirectFieldAccess) {
180+
this.fallBackOnDirectFieldAccess = fallBackOnDirectFieldAccess;
181+
}
182+
170183
/**
171184
* Add a {@link HandlerMethodArgumentResolver} for custom controller method
172185
* arguments. Such custom resolvers are ordered after built-in resolvers
@@ -257,7 +270,10 @@ private HandlerMethodArgumentResolverComposite initArgumentResolvers() {
257270
// Must be ahead of ArgumentMethodArgumentResolver
258271
resolvers.addResolver(new ProjectedPayloadMethodArgumentResolver(obtainApplicationContext()));
259272
}
260-
GraphQlArgumentBinder argumentBinder = new GraphQlArgumentBinder(this.conversionService);
273+
274+
GraphQlArgumentBinder argumentBinder =
275+
new GraphQlArgumentBinder(this.conversionService, this.fallBackOnDirectFieldAccess);
276+
261277
resolvers.addResolver(new ArgumentMethodArgumentResolver(argumentBinder));
262278
resolvers.addResolver(new ArgumentsMethodArgumentResolver(argumentBinder));
263279
resolvers.addResolver(new ContextValueMethodArgumentResolver());

spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.graphql.data;
1818

1919
import java.lang.reflect.Method;
20+
import java.util.ArrayList;
2021
import java.util.Arrays;
2122
import java.util.Collections;
2223
import java.util.HashMap;
@@ -118,6 +119,19 @@ void dataBindingWithNestedBeanListEmpty() throws Exception {
118119
assertThat(((ItemListHolder) result).getItems()).hasSize(0);
119120
}
120121

122+
@Test // gh-599
123+
void dataBindingWithDirectFieldAccess() throws Exception {
124+
125+
Object result = bind(
126+
new GraphQlArgumentBinder(new DefaultFormattingConversionService(), true /* fallBackOnFieldAccess */),
127+
"{\"items\":[{\"name\":\"first\"},{\"name\":\"second\"}]}",
128+
ResolvableType.forClass(DirectFieldAccessItemListHolder.class));
129+
130+
assertThat(result).isNotNull().isInstanceOf(DirectFieldAccessItemListHolder.class);
131+
DirectFieldAccessItemListHolder holder = (DirectFieldAccessItemListHolder) result;
132+
assertThat(holder.items).hasSize(2).extracting("name").containsExactly("first", "second");
133+
}
134+
121135
@Test // gh-349
122136
void dataBindingToBeanWithEnumGenericType() throws Exception {
123137

@@ -389,14 +403,19 @@ void primaryConstructorWithEnumGenericType() throws Exception {
389403
assertThat(input.enums()).hasSize(2).containsExactly(FancyEnum.ONE, FancyEnum.TWO);
390404
}
391405

392-
@SuppressWarnings("unchecked")
393406
@Nullable
394407
private Object bind(String json, ResolvableType targetType) throws Exception {
408+
return bind(this.binder, json, targetType);
409+
}
410+
411+
@SuppressWarnings("unchecked")
412+
@Nullable
413+
private Object bind(GraphQlArgumentBinder binder, String json, ResolvableType targetType) throws Exception {
395414
DataFetchingEnvironment environment =
396415
DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
397416
.arguments(this.mapper.readValue("{\"key\":" + json + "}", Map.class))
398417
.build();
399-
return this.binder.bind(environment, "key", targetType);
418+
return binder.bind(environment, "key", targetType);
400419
}
401420

402421

@@ -607,6 +626,17 @@ public Object getValue() {
607626
}
608627

609628

629+
static class DirectFieldAccessItemListHolder {
630+
631+
private List<Item> items = new ArrayList<>();
632+
633+
public DirectFieldAccessItemListHolder addItem(Item item) {
634+
this.items.add(item);
635+
return this;
636+
}
637+
}
638+
639+
610640
@SuppressWarnings("unused")
611641
static class Item {
612642

0 commit comments

Comments
 (0)