Skip to content

Commit 9210581

Browse files
committed
reimplementation of the extraction of property fetching
1 parent 49bbbeb commit 9210581

2 files changed

Lines changed: 332 additions & 293 deletions

File tree

Lines changed: 6 additions & 293 deletions
Original file line numberDiff line numberDiff line change
@@ -1,320 +1,33 @@
11
package graphql.schema;
22

3-
import graphql.GraphQLException;
43
import graphql.Internal;
54

6-
import java.lang.reflect.Field;
7-
import java.lang.reflect.InvocationTargetException;
8-
import java.lang.reflect.Method;
9-
import java.lang.reflect.Modifier;
10-
import java.util.Arrays;
11-
import java.util.Comparator;
12-
import java.util.Map;
13-
import java.util.Optional;
14-
import java.util.concurrent.ConcurrentHashMap;
15-
import java.util.concurrent.ConcurrentMap;
16-
import java.util.concurrent.atomic.AtomicBoolean;
17-
import java.util.function.Predicate;
18-
19-
import static graphql.Assert.assertShouldNeverHappen;
20-
import static graphql.Scalars.GraphQLBoolean;
21-
import static graphql.schema.GraphQLTypeUtil.isNonNull;
22-
import static graphql.schema.GraphQLTypeUtil.unwrapOne;
23-
245
/**
256
* This class is the guts of a property data fetcher and also used in AST code to turn
267
* in memory java objects into AST elements
278
*/
289
@Internal
2910
public class PropertyDataFetcherHelper {
30-
private static final AtomicBoolean USE_SET_ACCESSIBLE = new AtomicBoolean(true);
31-
private static final AtomicBoolean USE_NEGATIVE_CACHE = new AtomicBoolean(true);
32-
private static final ConcurrentMap<String, CachedMethod> METHOD_CACHE = new ConcurrentHashMap<>();
33-
private static final ConcurrentMap<String, Field> FIELD_CACHE = new ConcurrentHashMap<>();
34-
private static final ConcurrentMap<String, String> NEGATIVE_CACHE = new ConcurrentHashMap<>();
3511

36-
private static class CachedMethod {
37-
CachedMethod(Method method) {
38-
this.method = method;
39-
this.takesDataFetcherEnvironmentAsOnlyArgument = takesDataFetcherEnvironmentAsOnlyArgument(method);
40-
}
41-
42-
Method method;
43-
boolean takesDataFetcherEnvironmentAsOnlyArgument;
44-
}
12+
private static final PropertyFetchingImpl impl = new PropertyFetchingImpl(DataFetchingEnvironment.class);
4513

4614
public static Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType) {
47-
return getPropertyValue(propertyName, object, graphQLType, null);
15+
return impl.getPropertyValue(propertyName, object, graphQLType, null);
4816
}
4917

5018
public static Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType, DataFetchingEnvironment environment) {
51-
if (object instanceof Map) {
52-
return ((Map<?, ?>) object).get(propertyName);
53-
}
54-
55-
String cacheKey = mkKey(object, propertyName);
56-
// lets try positive cache mechanisms first. If we have seen the method or field before
57-
// then we invoke it directly without burning any cycles doing reflection.
58-
CachedMethod cachedMethod = METHOD_CACHE.get(cacheKey);
59-
if (cachedMethod != null) {
60-
try {
61-
return invokeMethod(object, environment, cachedMethod.method, cachedMethod.takesDataFetcherEnvironmentAsOnlyArgument);
62-
} catch (NoSuchMethodException ignored) {
63-
assertShouldNeverHappen("A method cached as '%s' is no longer available??", cacheKey);
64-
}
65-
}
66-
Field cachedField = FIELD_CACHE.get(cacheKey);
67-
if (cachedField != null) {
68-
return invokeField(object, cachedField);
69-
}
70-
71-
//
72-
// if we have tried all strategies before and they have all failed then we negatively cache
73-
// the cacheKey and assume that its never going to turn up. This shortcuts the property lookup
74-
// in systems where there was a `foo` graphql property but they never provided an POJO
75-
// version of `foo`.
76-
//
77-
// we do this second because we believe in the positive cached version will mostly prevail
78-
// but if we then look it up and negatively cache it then lest do that look up next
79-
//
80-
if (isNegativelyCached(cacheKey)) {
81-
return null;
82-
}
83-
//
84-
// ok we haven't cached it and we haven't negatively cached it so we have to find the POJO method which is the most
85-
// expensive operation here
86-
//
87-
boolean dfeInUse = environment != null;
88-
try {
89-
MethodFinder methodFinder = (root, methodName) -> findPubliclyAccessibleMethod(cacheKey, propertyName, root, methodName, dfeInUse);
90-
return getPropertyViaGetterMethod(object, propertyName, graphQLType, methodFinder, environment);
91-
} catch (NoSuchMethodException ignored) {
92-
try {
93-
MethodFinder methodFinder = (aClass, methodName) -> findViaSetAccessible(cacheKey, propertyName, aClass, methodName, dfeInUse);
94-
return getPropertyViaGetterMethod(object, propertyName, graphQLType, methodFinder, environment);
95-
} catch (NoSuchMethodException ignored2) {
96-
try {
97-
return getPropertyViaFieldAccess(cacheKey, object, propertyName);
98-
} catch (FastNoSuchMethodException e) {
99-
// we have nothing to ask for and we have exhausted our lookup strategies
100-
putInNegativeCache(cacheKey);
101-
return null;
102-
}
103-
}
104-
}
105-
}
106-
107-
private static boolean isNegativelyCached(String key) {
108-
if (USE_NEGATIVE_CACHE.get()) {
109-
return NEGATIVE_CACHE.containsKey(key);
110-
}
111-
return false;
112-
}
113-
114-
private static void putInNegativeCache(String key) {
115-
if (USE_NEGATIVE_CACHE.get()) {
116-
NEGATIVE_CACHE.put(key, key);
117-
}
118-
}
119-
120-
private interface MethodFinder {
121-
Method apply(Class<?> aClass, String s) throws NoSuchMethodException;
122-
}
123-
124-
private static Object getPropertyViaGetterMethod(Object object, String propertyName, GraphQLType graphQLType, MethodFinder methodFinder, DataFetchingEnvironment environment) throws NoSuchMethodException {
125-
if (isBooleanProperty(graphQLType)) {
126-
try {
127-
return getPropertyViaGetterUsingPrefix(object, propertyName, "is", methodFinder, environment);
128-
} catch (NoSuchMethodException e) {
129-
return getPropertyViaGetterUsingPrefix(object, propertyName, "get", methodFinder, environment);
130-
}
131-
} else {
132-
return getPropertyViaGetterUsingPrefix(object, propertyName, "get", methodFinder, environment);
133-
}
134-
}
135-
136-
private static Object getPropertyViaGetterUsingPrefix(Object object, String propertyName, String prefix, MethodFinder methodFinder, DataFetchingEnvironment environment) throws NoSuchMethodException {
137-
String getterName = prefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
138-
Method method = methodFinder.apply(object.getClass(), getterName);
139-
return invokeMethod(object, environment, method, takesDataFetcherEnvironmentAsOnlyArgument(method));
140-
}
141-
142-
/**
143-
* Invoking public methods on package-protected classes via reflection
144-
* causes exceptions. This method searches a class's hierarchy for
145-
* public visibility parent classes with the desired getter. This
146-
* particular case is required to support AutoValue style data classes,
147-
* which have abstract public interfaces implemented by package-protected
148-
* (generated) subclasses.
149-
*/
150-
private static Method findPubliclyAccessibleMethod(String cacheKey, String propertyName, Class<?> rootClass, String methodName, boolean dfeInUse) throws NoSuchMethodException {
151-
Class<?> currentClass = rootClass;
152-
while (currentClass != null) {
153-
if (Modifier.isPublic(currentClass.getModifiers())) {
154-
if (dfeInUse) {
155-
//
156-
// try a getter that takes DataFetchingEnvironment first (if we have one)
157-
try {
158-
Method method = currentClass.getMethod(methodName, DataFetchingEnvironment.class);
159-
if (Modifier.isPublic(method.getModifiers())) {
160-
METHOD_CACHE.putIfAbsent(cacheKey, new CachedMethod(method));
161-
return method;
162-
}
163-
} catch (NoSuchMethodException e) {
164-
// ok try the next approach
165-
}
166-
}
167-
Method method = currentClass.getMethod(methodName);
168-
if (Modifier.isPublic(method.getModifiers())) {
169-
METHOD_CACHE.putIfAbsent(cacheKey, new CachedMethod(method));
170-
return method;
171-
}
172-
}
173-
currentClass = currentClass.getSuperclass();
174-
}
175-
assert rootClass != null;
176-
return rootClass.getMethod(methodName);
177-
}
178-
179-
private static Method findViaSetAccessible(String cacheKey, String propertyName, Class<?> aClass, String methodName, boolean dfeInUse) throws NoSuchMethodException {
180-
if (!USE_SET_ACCESSIBLE.get()) {
181-
throw new FastNoSuchMethodException(methodName);
182-
}
183-
Class<?> currentClass = aClass;
184-
while (currentClass != null) {
185-
Predicate<Method> whichMethods = mth -> {
186-
if (dfeInUse) {
187-
return hasZeroArgs(mth) || takesDataFetcherEnvironmentAsOnlyArgument(mth);
188-
}
189-
return hasZeroArgs(mth);
190-
};
191-
Method[] declaredMethods = currentClass.getDeclaredMethods();
192-
Optional<Method> m = Arrays.stream(declaredMethods)
193-
.filter(mth -> methodName.equals(mth.getName()))
194-
.filter(whichMethods)
195-
.min(mostMethodArgsFirst());
196-
if (m.isPresent()) {
197-
try {
198-
// few JVMs actually enforce this but it might happen
199-
Method method = m.get();
200-
method.setAccessible(true);
201-
METHOD_CACHE.putIfAbsent(cacheKey, new CachedMethod(method));
202-
return method;
203-
} catch (SecurityException ignored) {
204-
}
205-
}
206-
currentClass = currentClass.getSuperclass();
207-
}
208-
throw new FastNoSuchMethodException(methodName);
209-
}
210-
211-
private static Object getPropertyViaFieldAccess(String cacheKey, Object object, String propertyName) throws FastNoSuchMethodException {
212-
Class<?> aClass = object.getClass();
213-
try {
214-
Field field = aClass.getField(propertyName);
215-
FIELD_CACHE.putIfAbsent(cacheKey, field);
216-
return field.get(object);
217-
} catch (NoSuchFieldException e) {
218-
if (!USE_SET_ACCESSIBLE.get()) {
219-
throw new FastNoSuchMethodException(cacheKey);
220-
}
221-
// if not public fields then try via setAccessible
222-
try {
223-
Field field = aClass.getDeclaredField(propertyName);
224-
field.setAccessible(true);
225-
FIELD_CACHE.putIfAbsent(cacheKey, field);
226-
return field.get(object);
227-
} catch (SecurityException | NoSuchFieldException ignored2) {
228-
throw new FastNoSuchMethodException(cacheKey);
229-
} catch (IllegalAccessException e1) {
230-
throw new GraphQLException(e);
231-
}
232-
} catch (IllegalAccessException e) {
233-
throw new GraphQLException(e);
234-
}
235-
}
236-
237-
private static Object invokeMethod(Object object, DataFetchingEnvironment environment, Method method, boolean takesDataFetcherEnvironmentAsOnlyArgument) throws FastNoSuchMethodException {
238-
try {
239-
if (takesDataFetcherEnvironmentAsOnlyArgument) {
240-
if (environment == null) {
241-
throw new FastNoSuchMethodException(method.getName());
242-
}
243-
return method.invoke(object, environment);
244-
} else {
245-
return method.invoke(object);
246-
}
247-
} catch (IllegalAccessException | InvocationTargetException e) {
248-
throw new GraphQLException(e);
249-
}
250-
}
251-
252-
private static Object invokeField(Object object, Field field) {
253-
try {
254-
return field.get(object);
255-
} catch (IllegalAccessException e) {
256-
throw new GraphQLException(e);
257-
}
258-
}
259-
260-
@SuppressWarnings("SimplifiableIfStatement")
261-
private static boolean isBooleanProperty(GraphQLType graphQLType) {
262-
if (graphQLType == GraphQLBoolean) {
263-
return true;
264-
}
265-
if (isNonNull(graphQLType)) {
266-
return unwrapOne(graphQLType) == GraphQLBoolean;
267-
}
268-
return false;
19+
return impl.getPropertyValue(propertyName, object, graphQLType, environment);
26920
}
27021

27122
public static void clearReflectionCache() {
272-
METHOD_CACHE.clear();
273-
FIELD_CACHE.clear();
274-
NEGATIVE_CACHE.clear();
23+
impl.clearReflectionCache();
27524
}
27625

27726
public static boolean setUseSetAccessible(boolean flag) {
278-
return USE_SET_ACCESSIBLE.getAndSet(flag);
27+
return impl.setUseSetAccessible(flag);
27928
}
28029

28130
public static boolean setUseNegativeCache(boolean flag) {
282-
return USE_NEGATIVE_CACHE.getAndSet(flag);
283-
}
284-
285-
private static String mkKey(Object object, String propertyName) {
286-
Class<?> clazz = object.getClass();
287-
ClassLoader classLoader = clazz.getClassLoader();
288-
if (classLoader != null) {
289-
return classLoader.hashCode() + "__" + clazz.getName() + "__" + propertyName;
290-
} else {
291-
return clazz.getName() + "__" + propertyName;
292-
}
293-
}
294-
295-
// by not filling out the stack trace, we gain speed when using the exception as flow control
296-
private static boolean hasZeroArgs(Method mth) {
297-
return mth.getParameterCount() == 0;
298-
}
299-
300-
private static boolean takesDataFetcherEnvironmentAsOnlyArgument(Method mth) {
301-
return mth.getParameterCount() == 1 &&
302-
mth.getParameterTypes()[0].equals(DataFetchingEnvironment.class);
303-
}
304-
305-
private static Comparator<? super Method> mostMethodArgsFirst() {
306-
return Comparator.comparingInt(Method::getParameterCount).reversed();
307-
}
308-
309-
@SuppressWarnings("serial")
310-
private static class FastNoSuchMethodException extends NoSuchMethodException {
311-
public FastNoSuchMethodException(String methodName) {
312-
super(methodName);
313-
}
314-
315-
@Override
316-
public synchronized Throwable fillInStackTrace() {
317-
return this;
318-
}
31+
return impl.setUseNegativeCache(flag);
31932
}
32033
}

0 commit comments

Comments
 (0)