Skip to content

Commit a5cfbd8

Browse files
committed
Introduce ResponseMapFactory
This will allows to customize which java.util.Map concrete class to use. For example, eclipse-collections has Map implementations with better memory footprint than JDK maps.
1 parent 59932ef commit a5cfbd8

13 files changed

+190
-29
lines changed

src/main/java/graphql/GraphQL.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import graphql.execution.ExecutionId;
1010
import graphql.execution.ExecutionIdProvider;
1111
import graphql.execution.ExecutionStrategy;
12+
import graphql.execution.ResponseMapFactory;
1213
import graphql.execution.SimpleDataFetcherExceptionHandler;
1314
import graphql.execution.SubscriptionExecutionStrategy;
1415
import graphql.execution.ValueUnboxer;
@@ -92,6 +93,7 @@ public class GraphQL {
9293
private final Instrumentation instrumentation;
9394
private final PreparsedDocumentProvider preparsedDocumentProvider;
9495
private final ValueUnboxer valueUnboxer;
96+
private final ResponseMapFactory responseMapFactory;
9597
private final boolean doNotAutomaticallyDispatchDataLoader;
9698

9799

@@ -104,6 +106,7 @@ private GraphQL(Builder builder) {
104106
this.instrumentation = assertNotNull(builder.instrumentation, () -> "instrumentation must not be null");
105107
this.preparsedDocumentProvider = assertNotNull(builder.preparsedDocumentProvider, () -> "preparsedDocumentProvider must be non null");
106108
this.valueUnboxer = assertNotNull(builder.valueUnboxer, () -> "valueUnboxer must not be null");
109+
this.responseMapFactory = assertNotNull(builder.responseMapFactory, () -> "responseMapFactory must be not null");
107110
this.doNotAutomaticallyDispatchDataLoader = builder.doNotAutomaticallyDispatchDataLoader;
108111
}
109112

@@ -213,7 +216,7 @@ public static class Builder {
213216
private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE;
214217
private boolean doNotAutomaticallyDispatchDataLoader = false;
215218
private ValueUnboxer valueUnboxer = ValueUnboxer.DEFAULT;
216-
219+
private ResponseMapFactory responseMapFactory = ResponseMapFactory.DEFAULT;
217220

218221
public Builder(GraphQLSchema graphQLSchema) {
219222
this.graphQLSchema = graphQLSchema;
@@ -284,6 +287,11 @@ public Builder valueUnboxer(ValueUnboxer valueUnboxer) {
284287
return this;
285288
}
286289

290+
public Builder mapFactory(ResponseMapFactory responseMapFactory) {
291+
this.responseMapFactory = responseMapFactory;
292+
return this;
293+
}
294+
287295
public GraphQL build() {
288296
// we use the data fetcher exception handler unless they set their own strategy in which case bets are off
289297
if (queryExecutionStrategy == null) {
@@ -543,7 +551,7 @@ private CompletableFuture<ExecutionResult> execute(ExecutionInput executionInput
543551
EngineRunningState engineRunningState
544552
) {
545553

546-
Execution execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, valueUnboxer, doNotAutomaticallyDispatchDataLoader);
554+
Execution execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, instrumentation, valueUnboxer, responseMapFactory, doNotAutomaticallyDispatchDataLoader);
547555
ExecutionId executionId = executionInput.getExecutionId();
548556

549557
return execution.execute(document, graphQLSchema, executionId, executionInput, instrumentationState, engineRunningState);

src/main/java/graphql/execution/AbstractAsyncExecutionStrategy.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package graphql.execution;
22

3-
import com.google.common.collect.Maps;
43
import graphql.ExecutionResult;
54
import graphql.ExecutionResultImpl;
65
import graphql.PublicSpi;
@@ -28,12 +27,7 @@ protected BiConsumer<List<Object>, Throwable> handleResults(ExecutionContext exe
2827
return;
2928
}
3029

31-
Map<String, Object> resolvedValuesByField = Maps.newLinkedHashMapWithExpectedSize(fieldNames.size());
32-
int ix = 0;
33-
for (Object result : results) {
34-
String fieldName = fieldNames.get(ix++);
35-
resolvedValuesByField.put(fieldName, result);
36-
}
30+
Map<String, Object> resolvedValuesByField = executionContext.getResponseMapFactory().create(fieldNames, results);
3731
overallResult.complete(new ExecutionResultImpl(resolvedValuesByField, executionContext.getErrors()));
3832
};
3933
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package graphql.execution;
2+
3+
import com.google.common.collect.Maps;
4+
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
public class DefaultResponseMapFactory implements ResponseMapFactory {
9+
10+
@Override
11+
public Map<String, Object> create(List<String> fieldNames, List<Object> results) {
12+
Map<String, Object> result = Maps.newLinkedHashMapWithExpectedSize(fieldNames.size());
13+
int ix = 0;
14+
for (Object fieldValue : results) {
15+
String fieldName = fieldNames.get(ix++);
16+
result.put(fieldName, fieldValue);
17+
}
18+
return result;
19+
}
20+
}

src/main/java/graphql/execution/Execution.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,22 @@ public class Execution {
5757
private final ExecutionStrategy subscriptionStrategy;
5858
private final Instrumentation instrumentation;
5959
private final ValueUnboxer valueUnboxer;
60+
private final ResponseMapFactory responseMapFactory;
6061
private final boolean doNotAutomaticallyDispatchDataLoader;
6162

6263
public Execution(ExecutionStrategy queryStrategy,
6364
ExecutionStrategy mutationStrategy,
6465
ExecutionStrategy subscriptionStrategy,
6566
Instrumentation instrumentation,
6667
ValueUnboxer valueUnboxer,
68+
ResponseMapFactory responseMapFactory,
6769
boolean doNotAutomaticallyDispatchDataLoader) {
6870
this.queryStrategy = queryStrategy != null ? queryStrategy : new AsyncExecutionStrategy();
6971
this.mutationStrategy = mutationStrategy != null ? mutationStrategy : new AsyncSerialExecutionStrategy();
7072
this.subscriptionStrategy = subscriptionStrategy != null ? subscriptionStrategy : new AsyncExecutionStrategy();
7173
this.instrumentation = instrumentation;
7274
this.valueUnboxer = valueUnboxer;
75+
this.responseMapFactory = responseMapFactory;
7376
this.doNotAutomaticallyDispatchDataLoader = doNotAutomaticallyDispatchDataLoader;
7477
}
7578

@@ -110,6 +113,7 @@ public CompletableFuture<ExecutionResult> execute(Document document, GraphQLSche
110113
.dataLoaderRegistry(executionInput.getDataLoaderRegistry())
111114
.locale(executionInput.getLocale())
112115
.valueUnboxer(valueUnboxer)
116+
.responseMapFactory(responseMapFactory)
113117
.executionInput(executionInput)
114118
.propagapropagateErrorsOnNonNullContractFailureeErrors(propagateErrorsOnNonNullContractFailure)
115119
.engineRunningState(engineRunningState)

src/main/java/graphql/execution/ExecutionContext.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ public class ExecutionContext {
6161
private final Locale locale;
6262
private final IncrementalCallState incrementalCallState = new IncrementalCallState();
6363
private final ValueUnboxer valueUnboxer;
64+
private final ResponseMapFactory responseMapFactory;
65+
6466
private final ExecutionInput executionInput;
6567
private final Supplier<ExecutableNormalizedOperation> queryTree;
6668
private final boolean propagateErrorsOnNonNullContractFailure;
@@ -92,6 +94,7 @@ public class ExecutionContext {
9294
this.dataLoaderRegistry = builder.dataLoaderRegistry;
9395
this.locale = builder.locale;
9496
this.valueUnboxer = builder.valueUnboxer;
97+
this.responseMapFactory = builder.responseMapFactory;
9598
this.errors.set(builder.errors);
9699
this.localContext = builder.localContext;
97100
this.executionInput = builder.executionInput;
@@ -101,7 +104,6 @@ public class ExecutionContext {
101104
this.engineRunningState = builder.engineRunningState;
102105
}
103106

104-
105107
public ExecutionId getExecutionId() {
106108
return executionId;
107109
}
@@ -295,6 +297,10 @@ public void addErrors(List<GraphQLError> errors) {
295297
});
296298
}
297299

300+
public ResponseMapFactory getResponseMapFactory() {
301+
return responseMapFactory;
302+
}
303+
298304
/**
299305
* @return the total list of errors for this execution context
300306
*/

src/main/java/graphql/execution/ExecutionContextBuilder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class ExecutionContextBuilder {
5252
DataLoaderDispatchStrategy dataLoaderDispatcherStrategy = DataLoaderDispatchStrategy.NO_OP;
5353
boolean propagateErrorsOnNonNullContractFailure = true;
5454
EngineRunningState engineRunningState;
55+
ResponseMapFactory responseMapFactory = ResponseMapFactory.DEFAULT;
5556

5657
/**
5758
* @return a new builder of {@link graphql.execution.ExecutionContext}s
@@ -100,6 +101,7 @@ public ExecutionContextBuilder() {
100101
dataLoaderDispatcherStrategy = other.getDataLoaderDispatcherStrategy();
101102
propagateErrorsOnNonNullContractFailure = other.propagateErrorsOnNonNullContractFailure();
102103
engineRunningState = other.getEngineRunningState();
104+
responseMapFactory = other.getResponseMapFactory();
103105
}
104106

105107
public ExecutionContextBuilder instrumentation(Instrumentation instrumentation) {
@@ -224,6 +226,11 @@ public ExecutionContextBuilder dataLoaderDispatcherStrategy(DataLoaderDispatchSt
224226
return this;
225227
}
226228

229+
public ExecutionContextBuilder responseMapFactory(ResponseMapFactory responseMapFactory) {
230+
this.responseMapFactory = responseMapFactory;
231+
return this;
232+
}
233+
227234
public ExecutionContextBuilder resetErrors() {
228235
this.errors = emptyList();
229236
return this;

src/main/java/graphql/execution/ExecutionStrategy.java

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package graphql.execution;
22

33
import com.google.common.collect.ImmutableList;
4-
import com.google.common.collect.Maps;
54
import graphql.DuckTyped;
65
import graphql.EngineRunningState;
76
import graphql.ExecutionResult;
@@ -260,7 +259,7 @@ protected Object executeObject(ExecutionContext executionContext, ExecutionStrat
260259
overallResult.whenComplete(resolveObjectCtx::onCompleted);
261260
return overallResult;
262261
} else {
263-
Map<String, Object> fieldValueMap = buildFieldValueMap(fieldsExecutedOnInitialResult, (List<Object>) completedValuesObject);
262+
Map<String, Object> fieldValueMap = executionContext.getResponseMapFactory().create(fieldsExecutedOnInitialResult, (List<Object>) completedValuesObject);
264263
resolveObjectCtx.onCompleted(fieldValueMap, null);
265264
return fieldValueMap;
266265
}
@@ -281,22 +280,11 @@ private BiConsumer<List<Object>, Throwable> buildFieldValueMap(List<String> fiel
281280
handleValueException(overallResult, exception, executionContext);
282281
return;
283282
}
284-
Map<String, Object> resolvedValuesByField = buildFieldValueMap(fieldNames, results);
283+
Map<String, Object> resolvedValuesByField = executionContext.getResponseMapFactory().create(fieldNames, results);
285284
overallResult.complete(resolvedValuesByField);
286285
};
287286
}
288287

289-
@NonNull
290-
private static Map<String, Object> buildFieldValueMap(List<String> fieldNames, List<Object> results) {
291-
Map<String, Object> resolvedValuesByField = Maps.newLinkedHashMapWithExpectedSize(fieldNames.size());
292-
int ix = 0;
293-
for (Object fieldValue : results) {
294-
String fieldName = fieldNames.get(ix++);
295-
resolvedValuesByField.put(fieldName, fieldValue);
296-
}
297-
return resolvedValuesByField;
298-
}
299-
300288
DeferredExecutionSupport createDeferredExecutionSupport(ExecutionContext executionContext, ExecutionStrategyParameters parameters) {
301289
MergedSelectionSet fields = parameters.getFields();
302290

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package graphql.execution;
2+
3+
import graphql.PublicSpi;
4+
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
/**
9+
* Allows to customize the concrete class {@link Map} implementation. For example, it could be possible to use
10+
* memory-efficient implementations, like eclipse-collections.
11+
*/
12+
@PublicSpi
13+
public interface ResponseMapFactory {
14+
15+
/**
16+
* The default implementation uses JDK's {@link java.util.LinkedHashMap}.
17+
*/
18+
ResponseMapFactory DEFAULT = new DefaultResponseMapFactory();
19+
20+
Map<String, Object> create(List<String> fieldNames, List<Object> results);
21+
22+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package graphql
2+
3+
import graphql.execution.ResponseMapFactory
4+
import graphql.schema.idl.RuntimeWiring
5+
import groovy.transform.Immutable
6+
import spock.lang.Specification
7+
8+
class CustomMapImplementationTest extends Specification {
9+
10+
@Immutable
11+
static class Person {
12+
String name
13+
@SuppressWarnings('unused') // used by graphql-java
14+
int age
15+
}
16+
17+
class CustomResponseMapFactory implements ResponseMapFactory {
18+
19+
@Override
20+
Map<String, Object> create(List<String> fieldNames, List<Object> results) {
21+
return Collections.unmodifiableMap(DEFAULT.create(fieldNames, results))
22+
}
23+
}
24+
25+
def graphql = TestUtil.graphQL("""
26+
type Query {
27+
people: [Person!]!
28+
}
29+
30+
type Person {
31+
name: String!
32+
age: Int!
33+
}
34+
35+
""",
36+
RuntimeWiring.newRuntimeWiring()
37+
.type("Query", {
38+
it.dataFetcher("people", { List.of(new Person("Mario", 18), new Person("Luigi", 21))})
39+
})
40+
.build())
41+
.mapFactory(new CustomResponseMapFactory())
42+
.build()
43+
44+
def "customMapImplementation"() {
45+
when:
46+
def input = ExecutionInput.newExecutionInput()
47+
.query('''
48+
query {
49+
people {
50+
name
51+
age
52+
}
53+
}
54+
''')
55+
.build()
56+
57+
def executionResult = graphql.execute(input)
58+
59+
then:
60+
executionResult.errors.isEmpty()
61+
executionResult.data == [ people: [
62+
[name: "Mario", age: 18],
63+
[name: "Luigi", age: 21],
64+
]]
65+
executionResult.data.getClass().getSimpleName() == 'UnmodifiableMap'
66+
executionResult.data['people'].each { it -> it.getClass().getSimpleName() == 'UnmodifiableMap' }
67+
}
68+
69+
}

src/test/groovy/graphql/EngineRunningTest.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ class EngineRunningTest extends Specification {
387387

388388
then:
389389
result.errors.collect { it.message } == ["recovered"]
390-
// we expect simply going from running to finshed
390+
// we expect simply going from running to finished
391391
states == [RUNNING, NOT_RUNNING]
392392
}
393393

@@ -445,7 +445,7 @@ class EngineRunningTest extends Specification {
445445

446446
then:
447447
result.errors.collect { it.message } == ["recovered"]
448-
// we expect simply going from running to finshed
448+
// we expect simply going from running to finished
449449
new ArrayList<>(states) == [RUNNING, NOT_RUNNING]
450450
}
451451

0 commit comments

Comments
 (0)