Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 44 additions & 15 deletions src/main/java/graphql/schema/PropertyFetchingImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import graphql.GraphQLException;
import graphql.Internal;
import graphql.schema.fetching.LambdaFetchingSupport;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
Expand All @@ -15,6 +16,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Predicate;

import static graphql.Assert.assertShouldNeverHappen;
Expand All @@ -30,6 +32,7 @@ public class PropertyFetchingImpl {

private final AtomicBoolean USE_SET_ACCESSIBLE = new AtomicBoolean(true);
private final AtomicBoolean USE_NEGATIVE_CACHE = new AtomicBoolean(true);
private final ConcurrentMap<CacheKey, CachedLambdaFunction> LAMBDA_CACHE = new ConcurrentHashMap<>();
private final ConcurrentMap<CacheKey, CachedMethod> METHOD_CACHE = new ConcurrentHashMap<>();
private final ConcurrentMap<CacheKey, Field> FIELD_CACHE = new ConcurrentHashMap<>();
private final ConcurrentMap<CacheKey, CacheKey> NEGATIVE_CACHE = new ConcurrentHashMap<>();
Expand All @@ -39,15 +42,22 @@ public PropertyFetchingImpl(Class<?> singleArgumentType) {
this.singleArgumentType = singleArgumentType;
}

private class CachedMethod {
Method method;
boolean takesSingleArgumentTypeAsOnlyArgument;
private final class CachedMethod {
private final Method method;
private final boolean takesSingleArgumentTypeAsOnlyArgument;

CachedMethod(Method method) {
this.method = method;
this.takesSingleArgumentTypeAsOnlyArgument = takesSingleArgumentTypeAsOnlyArgument(method);
}
}

private static final class CachedLambdaFunction {
private final Function<Object, Object> getter;

CachedLambdaFunction(Function<Object, Object> getter) {
this.getter = getter;
}
}

public Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType, Object singleArgumentValue) {
Expand All @@ -56,8 +66,13 @@ public Object getPropertyValue(String propertyName, Object object, GraphQLType g
}

CacheKey cacheKey = mkCacheKey(object, propertyName);
// lets try positive cache mechanisms first. If we have seen the method or field before

// let's try positive cache mechanisms first. If we have seen the method or field before
// then we invoke it directly without burning any cycles doing reflection.
CachedLambdaFunction cachedFunction = LAMBDA_CACHE.get(cacheKey);
if (cachedFunction != null) {
return cachedFunction.getter.apply(object);
}
CachedMethod cachedMethod = METHOD_CACHE.get(cacheKey);
if (cachedMethod != null) {
try {
Expand All @@ -72,9 +87,9 @@ public Object getPropertyValue(String propertyName, Object object, GraphQLType g
}

//
// if we have tried all strategies before and they have all failed then we negatively cache
// if we have tried all strategies before, and they have all failed then we negatively cache
// the cacheKey and assume that it's never going to turn up. This shortcuts the property lookup
// in systems where there was a `foo` graphql property but they never provided an POJO
// in systems where there was a `foo` graphql property, but they never provided an POJO
// version of `foo`.
//
// we do this second because we believe in the positive cached version will mostly prevail
Expand All @@ -83,10 +98,20 @@ public Object getPropertyValue(String propertyName, Object object, GraphQLType g
if (isNegativelyCached(cacheKey)) {
return null;
}

//
// 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
// 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
// expensive operation here
//

Optional<Function<Object, Object>> getterOpt = LambdaFetchingSupport.createGetter(object.getClass(), propertyName);
if (getterOpt.isPresent()) {
Function<Object, Object> getter = getterOpt.get();
cachedFunction = new CachedLambdaFunction(getter);
LAMBDA_CACHE.putIfAbsent(cacheKey, cachedFunction);
return getter.apply(object);
}

boolean dfeInUse = singleArgumentValue != null;
try {
MethodFinder methodFinder = (root, methodName) -> findPubliclyAccessibleMethod(cacheKey, root, methodName, dfeInUse);
Expand All @@ -99,7 +124,7 @@ public Object getPropertyValue(String propertyName, Object object, GraphQLType g
try {
return getPropertyViaFieldAccess(cacheKey, object, propertyName);
} catch (FastNoSuchMethodException e) {
// we have nothing to ask for and we have exhausted our lookup strategies
// we have nothing to ask for, and we have exhausted our lookup strategies
putInNegativeCache(cacheKey);
return null;
}
Expand Down Expand Up @@ -272,6 +297,7 @@ private boolean isBooleanProperty(GraphQLType graphQLType) {
}

public void clearReflectionCache() {
LAMBDA_CACHE.clear();
METHOD_CACHE.clear();
FIELD_CACHE.clear();
NEGATIVE_CACHE.clear();
Expand Down Expand Up @@ -304,8 +330,12 @@ private CacheKey(ClassLoader classLoader, String className, String propertyName)

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CacheKey)) return false;
if (this == o) {
return true;
}
if (!(o instanceof CacheKey)) {
return false;
}
CacheKey cacheKey = (CacheKey) o;
return Objects.equals(classLoader, cacheKey.classLoader) && Objects.equals(className, cacheKey.className) && Objects.equals(propertyName, cacheKey.propertyName);
}
Expand All @@ -322,10 +352,10 @@ public int hashCode() {
@Override
public String toString() {
return "CacheKey{" +
"classLoader=" + classLoader +
", className='" + className + '\'' +
", propertyName='" + propertyName + '\'' +
'}';
"classLoader=" + classLoader +
", className='" + className + '\'' +
", propertyName='" + propertyName + '\'' +
'}';
}
}

Expand All @@ -343,7 +373,6 @@ private static Comparator<? super Method> mostMethodArgsFirst() {
return Comparator.comparingInt(Method::getParameterCount).reversed();
}

@SuppressWarnings("serial")
private static class FastNoSuchMethodException extends NoSuchMethodException {
public FastNoSuchMethodException(String methodName) {
super(methodName);
Expand Down
193 changes: 193 additions & 0 deletions src/main/java/graphql/schema/fetching/LambdaFetchingSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package graphql.schema.fetching;

import graphql.Internal;
import graphql.VisibleForTesting;

import java.lang.invoke.CallSite;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import static java.util.stream.Collectors.toList;

@Internal
public class LambdaFetchingSupport {


/**
* This support class will use {@link LambdaMetafactory} and {@link MethodHandles} to create a dynamic function that allows access to a public
* getter method on the nominated class. {@link MethodHandles} is a caller senstive lookup mechanism. If the graphql-java cant lookup a class, then
* it won't be able to make dynamic lambda function to it.
* <p>
* If one cant be made, because it doesn't exist or the calling class does not have access to the method, then it will return
* an empty result indicating that this strategy cant be used.
*
* @param sourceClass the class that has the property getter method
* @param propertyName the name of the property to get
*
* @return a function that can be used to pass in an instance of source class and returns its getter method value
*/
public static Optional<Function<Object, Object>> createGetter(Class<?> sourceClass, String propertyName) {
Method candidateMethod = getCandidateMethod(sourceClass, propertyName);
if (candidateMethod != null) {
try {
Function<Object, Object> getterFunction = mkCallFunction(sourceClass, candidateMethod.getName(), candidateMethod.getReturnType());
return Optional.of(getterFunction);
} catch (Throwable ignore) {
// if we cant make a dynamic lambda here, then we give up and let the old property fetching code do its thing
}
}
return Optional.empty();
}


private static Method getCandidateMethod(Class<?> sourceClass, String propertyName) {
List<Method> allGetterMethods = findGetterMethodsForProperty(sourceClass, propertyName);
List<Method> pojoGetterMethods = allGetterMethods.stream()
.filter(LambdaFetchingSupport::isPossiblePojoMethod)
.collect(toList());
if (!pojoGetterMethods.isEmpty()) {
Method method = pojoGetterMethods.get(0);
if (isBooleanGetter(method)) {
method = findBestBooleanGetter(pojoGetterMethods);
}
return checkForSingleParameterPeer(method, allGetterMethods);
} else {
return null;
}

}

private static Method checkForSingleParameterPeer(Method candidateMethod, List<Method> allMethods) {
// getFoo(DataFetchingEnv ev) is allowed, but we don't want to handle it in this class
// so this find those edge cases
for (Method allMethod : allMethods) {
if (allMethod.getParameterCount() > 0) {
// we have some method with the property name that takes more than 1 argument
// we don't want to handle this here, so we are saying there is one
return null;
}
}
return candidateMethod;
}

private static Method findBestBooleanGetter(List<Method> methods) {
// we prefer isX() over getX() if both happen to be present
Optional<Method> isMethod = methods.stream().filter(method -> method.getName().startsWith("is")).findFirst();
return isMethod.orElse(methods.get(0));
}

/**
* Finds all methods in a class hierarchy that match the property name - they might not be suitable but they
*
* @param sourceClass the class we are looking to work on
* @param propertyName the name of the property
*
* @return a list of getter methods for that property
*/
private static List<Method> findGetterMethodsForProperty(Class<?> sourceClass, String propertyName) {
List<Method> methods = new ArrayList<>();
Class<?> currentClass = sourceClass;
while (currentClass != null) {
Method[] declaredMethods = currentClass.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
if (isGetterNamed(declaredMethod)) {
if (nameMatches(propertyName, declaredMethod)) {
methods.add(declaredMethod);
}
}
}
currentClass = currentClass.getSuperclass();
}

return methods.stream()
.sorted(Comparator.comparing(Method::getName))
.collect(toList());
}


private static boolean nameMatches(String propertyName, Method declaredMethod) {
String methodPropName = mkPropertyName(declaredMethod);
return propertyName.equals(methodPropName);
}

private static boolean isPossiblePojoMethod(Method method) {
return !isObjectMethod(method) &&
returnsSomething(method) &&
isGetterNamed(method) &&
hasNoParameters(method) &&
isPublic(method);
}

private static boolean isBooleanGetter(Method method) {
Class<?> returnType = method.getReturnType();
return isGetterNamed(method) && (returnType.equals(Boolean.class) || returnType.equals(Boolean.TYPE));
}

private static boolean hasNoParameters(Method method) {
return method.getParameterCount() == 0;
}

private static boolean isGetterNamed(Method method) {
String name = method.getName();
return ((name.startsWith("get") && name.length() > 4) || (name.startsWith("is") && name.length() > 3));
}

private static boolean returnsSomething(Method method) {
return !method.getReturnType().equals(Void.class);
}

private static boolean isPublic(Method method) {
return Modifier.isPublic(method.getModifiers());
}

private static boolean isObjectMethod(Method method) {
return method.getDeclaringClass().equals(Object.class);
}

private static String mkPropertyName(Method method) {
//
// getFooName becomes fooName
// isFoo becomes foo
//
String name = method.getName();
if (name.startsWith("get")) {
name = name.substring(3);
} else if (name.startsWith("is")) {
name = name.substring(2);
}
return decapitalize(name);
}

private static String decapitalize(String name) {
if (name.length() == 0) {
return name;
}
return name.substring(0, 1).toLowerCase() + name.substring(1);
}


@VisibleForTesting
static Function<Object, Object> mkCallFunction(Class<?> targetClass, String targetMethod, Class<?> targetMethodReturnType) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle virtualMethodHandle = lookup.findVirtual(targetClass, targetMethod, MethodType.methodType(targetMethodReturnType));
CallSite site = LambdaMetafactory.metafactory(lookup,
"apply",
MethodType.methodType(Function.class),
MethodType.methodType(Object.class, Object.class),
virtualMethodHandle,
MethodType.methodType(targetMethodReturnType, targetClass));
@SuppressWarnings("unchecked")
Function<Object, Object> getterFunction = (Function<Object, Object>) site.getTarget().invokeExact();
return getterFunction;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,16 @@ class PropertyDataFetcherTest extends Specification {
def "fetch via public method declared two classes up"() {
def environment = env(new TwoClassesDown("aValue"))
def fetcher = new PropertyDataFetcher("publicProperty")
when:
def result = fetcher.get(environment)
expect:
then:
result == "publicValue"

when:
result = fetcher.get(environment)
then:
result == "publicValue"

}

def "fetch via property only defined on package protected impl"() {
Expand Down
Loading