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
25 changes: 20 additions & 5 deletions src/main/java/graphql/schema/PropertyFetchingImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import graphql.GraphQLException;
import graphql.Internal;
import graphql.schema.fetching.LambdaFetchingSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
Expand Down Expand Up @@ -30,6 +32,7 @@
*/
@Internal
public class PropertyFetchingImpl {
private static final Logger log = LoggerFactory.getLogger(PropertyFetchingImpl.class);

private final AtomicBoolean USE_SET_ACCESSIBLE = new AtomicBoolean(true);
private final AtomicBoolean USE_LAMBDA_FACTORY = new AtomicBoolean(true);
Expand Down Expand Up @@ -108,10 +111,21 @@ public Object getPropertyValue(String propertyName, Object object, GraphQLType g

Optional<Function<Object, Object>> getterOpt = lambdaGetter(propertyName, object);
if (getterOpt.isPresent()) {
Function<Object, Object> getter = getterOpt.get();
cachedFunction = new CachedLambdaFunction(getter);
LAMBDA_CACHE.putIfAbsent(cacheKey, cachedFunction);
return getter.apply(object);
try {
Function<Object, Object> getter = getterOpt.get();
Object value = getter.apply(object);
cachedFunction = new CachedLambdaFunction(getter);
LAMBDA_CACHE.putIfAbsent(cacheKey, cachedFunction);
return value;
} catch (LinkageError | ClassCastException ignored) {
//
// if we get a linkage error then it maybe that class loader challenges
// are preventing the Meta Lambda from working. So let's continue with
// old skool reflection and if it's all broken there then it will eventually
// end up negatively cached
log.debug("Unable to invoke fast Meta Lambda for `{}` - Falling back to reflection", object.getClass().getName(), ignored);

}
}

//
Expand Down Expand Up @@ -241,7 +255,7 @@ private Method findPubliclyAccessibleMethod(CacheKey cacheKey, Class<?> rootClas
and fetch them - e.g. `object.propertyName()`
*/
private Method findRecordMethod(CacheKey cacheKey, Class<?> rootClass, String methodName) throws NoSuchMethodException {
return findPubliclyAccessibleMethod(cacheKey,rootClass,methodName,false);
return findPubliclyAccessibleMethod(cacheKey, rootClass, methodName, false);
}

private Method findViaSetAccessible(CacheKey cacheKey, Class<?> aClass, String methodName, boolean dfeInUse) throws NoSuchMethodException {
Expand Down Expand Up @@ -347,6 +361,7 @@ public void clearReflectionCache() {
public boolean setUseSetAccessible(boolean flag) {
return USE_SET_ACCESSIBLE.getAndSet(flag);
}

public boolean setUseLambdaFactory(boolean flag) {
return USE_LAMBDA_FACTORY.getAndSet(flag);
}
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/graphql/schema/fetching/LambdaFetchingSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ private static String decapitalize(String name) {

@VisibleForTesting
static Function<Object, Object> mkCallFunction(Class<?> targetClass, String targetMethod, Class<?> targetMethodReturnType) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandles.Lookup lookup = getLookup(targetClass);
MethodHandle virtualMethodHandle = lookup.findVirtual(targetClass, targetMethod, MethodType.methodType(targetMethodReturnType));
CallSite site = LambdaMetafactory.metafactory(lookup,
"apply",
Expand All @@ -204,4 +204,14 @@ static Function<Object, Object> mkCallFunction(Class<?> targetClass, String targ
return getterFunction;
}

private static MethodHandles.Lookup getLookup(Class<?> targetClass) throws IllegalAccessException {
MethodHandles.Lookup lookupMe = MethodHandles.lookup();
//
// This is a Java 9 approach to method look up allowing private access
// which we don't want to use yet until we get to Java 11
//
//lookupMe = MethodHandles.privateLookupIn(targetClass, lookupMe);
return lookupMe;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package graphql.schema

import graphql.GraphQLException
import graphql.Scalars
import spock.lang.Specification

class PropertyDataFetcherClassLoadingTest extends Specification {

GraphQLFieldDefinition fld(String fldName) {
return GraphQLFieldDefinition.newFieldDefinition().name(fldName).type(Scalars.GraphQLString).build()
}

static class BrokenClass {
static {
// this should prevent it from existing
throw new RuntimeException("No soup for you!")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol

}
}


static class TargetClass {

String getOkThings() {
return "ok"
}

BrokenClass getBrokenThings() {
return BrokenClass.cast(null)
}
}

def "can survive linkage errors during access to broken classes in Lambda support"() {
def okDF = PropertyDataFetcher.fetching("okThings")
def brokenDF = PropertyDataFetcher.fetching("brokenThings")

def target = new TargetClass()

when:
def value = okDF.get(fld("okThings"), target, { -> null })
then:
value == "ok"

when:
brokenDF.get(fld("brokenThings"), target, { -> null })
then:
// This is because the reflection method finder cant get to it
// but it has made it past the Meta Lambda support
thrown(GraphQLException)

// multiple times - same result
when:
value = okDF.get(fld("okThings"), target, { -> null })
then:
value == "ok"

when:
brokenDF.get(fld("brokenThings"), target, { -> null })
then:
thrown(GraphQLException)

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package graphql.schema.fetching

import graphql.Scalars
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.PropertyDataFetcher
import graphql.util.javac.DynamicJavacSupport
import spock.lang.Specification

class LambdaFetchingSupportTest extends Specification {
Expand Down Expand Up @@ -141,4 +145,56 @@ class LambdaFetchingSupportTest extends Specification {
then:
!getter.isPresent()
}

GraphQLFieldDefinition fld(String fldName) {
return GraphQLFieldDefinition.newFieldDefinition().name(fldName).type(Scalars.GraphQLString).build()
}

def "different class loaders induce certain behaviours"() {
String sourceCode = '''
package com.dynamic;
public class TestClass {
public String hello() {
return "world";
}
}
'''

def customClass = new DynamicJavacSupport(null).compile("com.dynamic.TestClass", sourceCode)
def targetObject = customClass.getDeclaredConstructor().newInstance()

// show that the graphql-java classes cant access this custom loaded class
when:
LambdaFetchingSupport.class.getClassLoader().loadClass("com.dynamic.TestClass")
then:
thrown(ClassNotFoundException)

// show that reflection works
when:
def helloMethod = targetObject.getClass().getMethod("hello")
def reflectedValue = helloMethod.invoke(targetObject)
then:
reflectedValue == "world"

// without MethodHandles.privateLookupIn this will fail crossing class loaders in Java 8
// if we change to privateLookupIn - then this will start working and this test will need to be changed
when:
def getter = LambdaFetchingSupport.createGetter(customClass, "hello")
then:

getter.isPresent()
try {
getter.get().apply(targetObject)
assert false, "We expect this to fail on Java 8 without access to MethodHandles.privateLookupIn"
} catch (LinkageError | ClassCastException ignored) {
}

// show that a DF can still be used access this because of the reflection fallback
// in the future it will work via MethodHandles.privateLookupIn
when:
def ageDF = PropertyDataFetcher.fetching("hello")
def value = ageDF.get(fld("hello"), targetObject, { -> null })
then:
value == "world"
}
}
157 changes: 157 additions & 0 deletions src/test/groovy/graphql/util/javac/DynamicJavacSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package graphql.util.javac;

import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static java.util.Objects.requireNonNull;


/**
* This utility allows is to dynamically create Java classes and place them into
* floating class loaders. This will allow us to test class loader challenges
* <p>
* Proprs to https://www.baeldung.com/java-string-compile-execute-code where
* most of this code came from.
*/
public class DynamicJavacSupport {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this to allow is to generate classes in the tests - that are in seperate and diverse class loaders.

Neato!


private final JavaCompiler compiler;
private final InMemoryFileManager manager;

public DynamicJavacSupport(ClassLoader parentClassLoader) {
compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(null, null, null);
manager = new InMemoryFileManager(parentClassLoader, standardFileManager);
}


public <T> Class<T> compile(String qualifiedClassName, String sourceCode) throws ClassNotFoundException {

List<JavaFileObject> sourceFiles = Collections.singletonList(new JavaSourceFromString(qualifiedClassName, sourceCode));

DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, diagnostics, null, null, sourceFiles);

boolean result = task.call();

if (!result) {
diagnostics.getDiagnostics()
.forEach(d -> System.out.printf("dyna-javac : %s\n", d));
throw new IllegalStateException("Could not compile " + qualifiedClassName + " as a class");
} else {
ClassLoader classLoader = manager.getClassLoader(null);
Class<?> clazz = classLoader.loadClass(qualifiedClassName);
return (Class<T>) clazz;
}
}

static class InMemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
private final InMemoryClassLoader loader;
private final Map<String, JavaClassAsBytes> compiledClasses;

InMemoryFileManager(ClassLoader parentClassLoader, StandardJavaFileManager standardManager) {
super(standardManager);
this.compiledClasses = new ConcurrentHashMap<>();
this.loader = new InMemoryClassLoader(parentClassLoader, this);
}

@Override
public JavaFileObject getJavaFileForOutput(Location location,
String className, JavaFileObject.Kind kind, FileObject sibling) {

JavaClassAsBytes classAsBytes = new JavaClassAsBytes(className, kind);
compiledClasses.put(className, classAsBytes);

return classAsBytes;
}

public Map<String, JavaClassAsBytes> getBytesMap() {
return compiledClasses;
}

@Override
public ClassLoader getClassLoader(Location location) {
return loader;
}
}

static class InMemoryClassLoader extends ClassLoader {

private InMemoryFileManager manager;

InMemoryClassLoader(ClassLoader parentClassLoader, InMemoryFileManager manager) {
super(parentClassLoader);
this.manager = requireNonNull(manager, "manager must not be null");
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

Map<String, JavaClassAsBytes> compiledClasses = manager.getBytesMap();

if (compiledClasses.containsKey(name)) {
byte[] bytes = compiledClasses.get(name).getBytes();
return defineClass(name, bytes, 0, bytes.length);
} else {
throw new ClassNotFoundException();
}
}

}

static class JavaSourceFromString extends SimpleJavaFileObject {


private String sourceCode;

JavaSourceFromString(String name, String sourceCode) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension),
Kind.SOURCE);
this.sourceCode = requireNonNull(sourceCode, "sourceCode must not be null");
}

@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return sourceCode;
}

}

static class JavaClassAsBytes extends SimpleJavaFileObject {


protected ByteArrayOutputStream bos =
new ByteArrayOutputStream();

JavaClassAsBytes(String name, Kind kind) {
super(URI.create("string:///" + name.replace('.', '/')
+ kind.extension), kind);
}

public byte[] getBytes() {
return bos.toByteArray();
}

@Override
public OutputStream openOutputStream() {
return bos;
}

}


}
Loading