Skip to content

Commit fa9f08e

Browse files
committed
feat: improve PropertyDataFetcher performance with caching
1 parent a2deb40 commit fa9f08e

File tree

1 file changed

+288
-0
lines changed

1 file changed

+288
-0
lines changed
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
package graphql.schema;
2+
3+
import static graphql.Scalars.GraphQLBoolean;
4+
5+
import java.lang.reflect.Field;
6+
import java.lang.reflect.InvocationTargetException;
7+
import java.lang.reflect.Method;
8+
import java.lang.reflect.Modifier;
9+
import java.util.Arrays;
10+
import java.util.Map;
11+
import java.util.Optional;
12+
import java.util.concurrent.ConcurrentHashMap;
13+
import java.util.concurrent.ConcurrentMap;
14+
import java.util.function.Function;
15+
16+
import graphql.Assert;
17+
import graphql.GraphQLException;
18+
import graphql.PublicApi;
19+
20+
/**
21+
* This is the default data fetcher used in graphql-java. It will examine
22+
* maps and POJO java beans for values that match the desired name, typically the field name
23+
* or it will use a provided function to obtain values.
24+
* maps and POJO java beans for values that match the desired name.
25+
*
26+
* It uses the following strategies
27+
* <ul>
28+
* <li>If the source is null, return null</li>
29+
* <li>If the source is a Map, return map.get(propertyName)</li>
30+
* <li>If a function is provided, it is used</li>
31+
* <li>Find a public JavaBean getter method named `propertyName`</li>
32+
* <li>Find any getter method named `propertyName` and call method.setAccessible(true)</li>
33+
* <li>Find a public field named `propertyName`</li>
34+
* <li>Find any field named `propertyName` and call field.setAccessible(true)</li>
35+
* <li>If this cant find anything, then null is returned</li>
36+
* </ul>
37+
*
38+
* You can write your own data fetchers to get data from some other backing system
39+
* if you need highly customised behaviour.
40+
*
41+
* @see graphql.schema.DataFetcher
42+
*/
43+
@PublicApi
44+
public class PropertyDataFetcher<T> implements DataFetcher<T> {
45+
46+
private final String propertyName;
47+
private final Function<Object, Object> function;
48+
49+
/**
50+
* This constructor will use the property name and examine the {@link DataFetchingEnvironment#getSource()}
51+
* object for a getter method or field with that name.
52+
*
53+
* @param propertyName the name of the property to retrieve
54+
*/
55+
public PropertyDataFetcher(String propertyName) {
56+
this.propertyName = propertyName;
57+
this.function = null;
58+
}
59+
60+
@SuppressWarnings("unchecked")
61+
private <O> PropertyDataFetcher(Function<O, T> function) {
62+
this.function = (Function<Object, Object>) Assert.assertNotNull(function);
63+
this.propertyName = null;
64+
}
65+
66+
/**
67+
* Returns a data fetcher that will use the property name to examine the {@link DataFetchingEnvironment#getSource()} object
68+
* for a getter method or field with that name, or if its a map, it will look up a value using
69+
* property name as a key.
70+
*
71+
* For example :
72+
* <pre>
73+
* {@code
74+
*
75+
* DataFetcher functionDataFetcher = fetching("pojoPropertyName");
76+
*
77+
* }
78+
* </pre>
79+
*
80+
* @param propertyName the name of the property to retrieve
81+
* @param <T> the type of result
82+
*
83+
* @return a new PropertyDataFetcher using the provided function as its source of values
84+
*/
85+
public static <T> PropertyDataFetcher<T> fetching(String propertyName) {
86+
return new PropertyDataFetcher<>(propertyName);
87+
}
88+
89+
/**
90+
* Returns a data fetcher that will present the {@link DataFetchingEnvironment#getSource()} object to the supplied
91+
* function to obtain a value, which allows you to use Java 8 method references say obtain values in a
92+
* more type safe way.
93+
*
94+
* For example :
95+
* <pre>
96+
* {@code
97+
*
98+
* DataFetcher functionDataFetcher = fetching(Thing::getId);
99+
*
100+
* }
101+
* </pre>
102+
*
103+
* @param function the function to use to obtain a value from the source object
104+
* @param <O> the type of the source object
105+
* @param <T> the type of result
106+
*
107+
* @return a new PropertyDataFetcher using the provided function as its source of values
108+
*/
109+
public static <T, O> PropertyDataFetcher<T> fetching(Function<O, T> function) {
110+
return new PropertyDataFetcher<>(function);
111+
}
112+
113+
114+
@SuppressWarnings("unchecked")
115+
@Override
116+
public T get(DataFetchingEnvironment environment) {
117+
Object source = environment.getSource();
118+
if (source == null) {
119+
return null;
120+
}
121+
122+
if (function != null) {
123+
return (T) function.apply(source);
124+
}
125+
126+
if (source instanceof Map) {
127+
return (T) ((Map<?, ?>) source).get(propertyName);
128+
}
129+
return (T) getPropertyViaGetter(source, environment.getFieldType());
130+
}
131+
132+
private Object getPropertyViaGetter(Object object, GraphQLOutputType outputType) {
133+
try {
134+
return getPropertyViaGetterMethod(object, outputType, this::findPubliclyAccessibleMethod);
135+
} catch (NoSuchMethodException ignored) {
136+
try {
137+
return getPropertyViaGetterMethod(object, outputType, this::findViaSetAccessible);
138+
} catch (NoSuchMethodException ignored2) {
139+
return getPropertyViaFieldAccess(object);
140+
}
141+
}
142+
}
143+
144+
@FunctionalInterface
145+
private interface MethodFinder {
146+
Method apply(Class aClass, String s) throws NoSuchMethodException;
147+
}
148+
149+
private Object getPropertyViaGetterMethod(Object object, GraphQLOutputType outputType, MethodFinder methodFinder) throws NoSuchMethodException {
150+
if (isBooleanProperty(outputType)) {
151+
try {
152+
return getPropertyViaGetterUsingPrefix(object, "is", methodFinder);
153+
} catch (NoSuchMethodException e) {
154+
return getPropertyViaGetterUsingPrefix(object, "get", methodFinder);
155+
}
156+
} else {
157+
return getPropertyViaGetterUsingPrefix(object, "get", methodFinder);
158+
}
159+
}
160+
161+
private Object getPropertyViaGetterUsingPrefix(Object object, String prefix, MethodFinder methodFinder) throws NoSuchMethodException {
162+
String getterName = prefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
163+
try {
164+
Method method = methodFinder.apply(object.getClass(), getterName);
165+
return method.invoke(object);
166+
} catch (IllegalAccessException | InvocationTargetException e) {
167+
throw new GraphQLException(e);
168+
}
169+
}
170+
171+
@SuppressWarnings("SimplifiableIfStatement")
172+
private boolean isBooleanProperty(GraphQLOutputType outputType) {
173+
if (outputType == GraphQLBoolean) return true;
174+
if (outputType instanceof GraphQLNonNull) {
175+
return ((GraphQLNonNull) outputType).getWrappedType() == GraphQLBoolean;
176+
}
177+
return false;
178+
}
179+
180+
private static final ConcurrentMap<String, Method> METHOD_CACHE = new ConcurrentHashMap<>();
181+
private static final ConcurrentMap<String, Field> FIELD_CACHE = new ConcurrentHashMap<>();
182+
/**
183+
* PropertyDataFetcher caches the methods and fields that map from a class to a property for runtime performance reasons.
184+
*
185+
* However during development you might be using an assistance tool like JRebel to allow you to tweak your code base and this
186+
* caching may interfere with this. So you can call this method to clear the cache. A JRebel plugin could
187+
* be developed to do just that.
188+
*/
189+
@SuppressWarnings("unused")
190+
public static void clearReflectionCache() {
191+
METHOD_CACHE.clear();
192+
FIELD_CACHE.clear();
193+
}
194+
private String mkKey(Class clazz, String propertyName) {
195+
return clazz.getName() + "__" + propertyName;
196+
}
197+
// by not filling out the stack trace, we gain speed when using the exception as flow control
198+
private static class FastNoSuchMethodException extends NoSuchMethodException {
199+
public FastNoSuchMethodException(String methodName) {
200+
super(methodName);
201+
}
202+
@Override
203+
public synchronized Throwable fillInStackTrace() {
204+
return this;
205+
}
206+
}
207+
208+
/**
209+
* Invoking public methods on package-protected classes via reflection
210+
* causes exceptions. This method searches a class's hierarchy for
211+
* public visibility parent classes with the desired getter. This
212+
* particular case is required to support AutoValue style data classes,
213+
* which have abstract public interfaces implemented by package-protected
214+
* (generated) subclasses.
215+
*/
216+
private Method findPubliclyAccessibleMethod(Class root, String methodName) throws NoSuchMethodException {
217+
Class currentClass = root;
218+
while (currentClass != null) {
219+
String key = mkKey(currentClass, propertyName);
220+
Method method = METHOD_CACHE.get(key);
221+
if (method != null) {
222+
return method;
223+
}
224+
if (Modifier.isPublic(currentClass.getModifiers())) {
225+
method = currentClass.getMethod(methodName);
226+
if (Modifier.isPublic(method.getModifiers())) {
227+
METHOD_CACHE.putIfAbsent(key, method);
228+
return method;
229+
}
230+
}
231+
currentClass = currentClass.getSuperclass();
232+
}
233+
//noinspection unchecked
234+
return root.getMethod(methodName);
235+
}
236+
237+
private Method findViaSetAccessible(Class aClass, String methodName) throws NoSuchMethodException {
238+
String key = mkKey(aClass, propertyName);
239+
Method method = METHOD_CACHE.get(key);
240+
if (method != null) {
241+
return method;
242+
}
243+
244+
Method[] declaredMethods = aClass.getDeclaredMethods();
245+
Optional<Method> m = Arrays.stream(declaredMethods)
246+
.filter(mtd -> methodName.equals(mtd.getName()))
247+
.findFirst();
248+
249+
if (m.isPresent()) {
250+
try {
251+
// few JVMs actually enforce this but it might happen
252+
method = m.get();
253+
method.setAccessible(true);
254+
METHOD_CACHE.putIfAbsent(key, method);
255+
return method;
256+
} catch (SecurityException ignored) {
257+
}
258+
}
259+
throw new FastNoSuchMethodException(methodName);
260+
}
261+
262+
private Object getPropertyViaFieldAccess(Object object) {
263+
Class<?> aClass = object.getClass();
264+
String key = mkKey(aClass, propertyName);
265+
try {
266+
Field field = FIELD_CACHE.get(key);
267+
if (field == null) {
268+
field = aClass.getField(propertyName);
269+
FIELD_CACHE.putIfAbsent(key, field);
270+
}
271+
return field.get(object);
272+
} catch (NoSuchFieldException e) {
273+
// if not public fields then try via setAccessible
274+
try {
275+
Field field = aClass.getDeclaredField(propertyName);
276+
field.setAccessible(true);
277+
FIELD_CACHE.putIfAbsent(key, field);
278+
return field.get(object);
279+
} catch (SecurityException | NoSuchFieldException ignored2) {
280+
return null;
281+
} catch (IllegalAccessException e1) {
282+
throw new GraphQLException(e);
283+
}
284+
} catch (IllegalAccessException e) {
285+
throw new GraphQLException(e);
286+
}
287+
}
288+
}

0 commit comments

Comments
 (0)