Skip to content
Closed
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ dependencies {
api 'org.reactivestreams:reactive-streams:' + reactiveStreamsVersion
antlr 'org.antlr:antlr4:' + antlrVersion
implementation 'com.google.guava:guava:31.0.1-jre'
implementation('org.javassist:javassist:3.29.2-GA')
testImplementation group: 'junit', name: 'junit', version: '4.13.2'
testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0'
testImplementation 'org.codehaus.groovy:groovy:3.0.9'
Expand Down
109 changes: 98 additions & 11 deletions src/main/java/graphql/schema/PropertyFetchingImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import graphql.GraphQLException;
import graphql.Internal;
import graphql.schema.bytecode.ByteCodePojoFetchingGenerator;
import org.slf4j.Logger;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
Expand All @@ -12,6 +14,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
Expand All @@ -21,15 +24,19 @@
import static graphql.Scalars.GraphQLBoolean;
import static graphql.schema.GraphQLTypeUtil.isNonNull;
import static graphql.schema.GraphQLTypeUtil.unwrapOne;
import static org.slf4j.LoggerFactory.getLogger;

/**
* A re-usable class that can fetch from POJOs
*/
@Internal
public class PropertyFetchingImpl {

private static final Logger log = getLogger(PropertyFetchingImpl.class);

private final AtomicBoolean USE_SET_ACCESSIBLE = new AtomicBoolean(true);
private final AtomicBoolean USE_NEGATIVE_CACHE = new AtomicBoolean(true);
private final ConcurrentMap<GenClassCacheKey, ByteCodePojoFetchingGenerator.Result> GENERATED_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 @@ -56,8 +63,23 @@ 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
GenClassCacheKey genClassCacheKey = mkGenCacheKey(object);
// 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.

ByteCodePojoFetchingGenerator.Result generatedFetcher = GENERATED_CACHE.get(genClassCacheKey);
boolean performGeneration = true;
if (generatedFetcher != null) {
if (generatedFetcher.handlesProperty(propertyName)) {
try {
return generatedFetcher.getFetcher().fetch(object, propertyName);
} catch (Exception e) {
// we don't expect this but let's go to the old way
}
}
performGeneration = false;
}

CachedMethod cachedMethod = METHOD_CACHE.get(cacheKey);
if (cachedMethod != null) {
try {
Expand All @@ -84,7 +106,29 @@ public Object getPropertyValue(String propertyName, Object object, GraphQLType g
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
// we can try to generate a dynamic class for the object which can prove faster
if (performGeneration) {
ByteCodePojoFetchingGenerator.Result result = null;
try {
result = ByteCodePojoFetchingGenerator.generateClassFor(object.getClass());
GENERATED_CACHE.put(genClassCacheKey, result);
} catch (Exception e) {
// We might not have enough
log.debug("Unable to generate a dynamic class for {}", object.getClass(), e);
}
if (result != null) {
if (result.handlesProperty(propertyName)) {
try {
return result.getFetcher().fetch(object, propertyName);
} catch (Exception e) {
// ok ignore it - we will use the old way
}
}
}
}

//
// 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
//
boolean dfeInUse = singleArgumentValue != null;
Expand Down Expand Up @@ -272,6 +316,7 @@ private boolean isBooleanProperty(GraphQLType graphQLType) {
}

public void clearReflectionCache() {
GENERATED_CACHE.clear();
METHOD_CACHE.clear();
FIELD_CACHE.clear();
NEGATIVE_CACHE.clear();
Expand All @@ -287,8 +332,47 @@ public boolean setUseNegativeCache(boolean flag) {

private CacheKey mkCacheKey(Object object, String propertyName) {
Class<?> clazz = object.getClass();
ClassLoader classLoader = clazz.getClassLoader();
return new CacheKey(classLoader, clazz.getName(), propertyName);
return new CacheKey(clazz.getClassLoader(), clazz.getName(), propertyName);
}

private GenClassCacheKey mkGenCacheKey(Object object) {
Class<?> clazz = object.getClass();
return new GenClassCacheKey(clazz.getClassLoader(), clazz.getName());
}

public static final class GenClassCacheKey {
private final ClassLoader classLoader;
private final String className;

private GenClassCacheKey(ClassLoader classLoader, String className) {
this.classLoader = classLoader;
this.className = className;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
GenClassCacheKey that = (GenClassCacheKey) o;
return classLoader.equals(that.classLoader) && className.equals(that.className);
}

@Override
public int hashCode() {
return Objects.hash(classLoader, className);
}

@Override
public String toString() {
return new StringJoiner(", ", GenClassCacheKey.class.getSimpleName() + "[", "]")
.add("classLoader=" + classLoader)
.add("className='" + className + "'")
.toString();
}
}

private static final class CacheKey {
Expand All @@ -304,8 +388,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 +410,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 +431,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
15 changes: 15 additions & 0 deletions src/main/java/graphql/schema/bytecode/ByteCodeException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package graphql.schema.bytecode;

import graphql.GraphQLException;

public class ByteCodeException extends GraphQLException {
public ByteCodeException(String message) {
super(message);
}

public ByteCodeException(String message, Throwable cause) {
super(message, cause);
}


}
8 changes: 8 additions & 0 deletions src/main/java/graphql/schema/bytecode/ByteCodeFetcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package graphql.schema.bytecode;

import graphql.Internal;

@Internal
public interface ByteCodeFetcher {
Object fetch(Object sourceObject, String propertyName);
}
Loading