Skip to content
This repository has been archived by the owner on May 22, 2024. It is now read-only.

Commit

Permalink
Use dynamically-created lambdas to call default methods in config pro…
Browse files Browse the repository at this point in the history
…xies

This is a re-factor of the code reverted in PR Netflix#685, originally by @kilink
  • Loading branch information
rgallardo-netflix committed Oct 11, 2023
1 parent f52ab93 commit 05d51af
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 91 deletions.
2 changes: 1 addition & 1 deletion archaius2-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ apply plugin: 'java-library'
dependencies {
api project(':archaius2-api')
implementation 'org.slf4j:slf4j-api:1.7.36'
implementation 'org.apache.commons:commons-lang3:3.3.2'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'commons-codec:commons-codec:1.16.0'
testImplementation 'com.google.code.findbugs:jsr305:3.0.1'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,11 @@
import com.netflix.archaius.api.annotations.PropertyName;
import com.netflix.archaius.util.Maps;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
Expand Down Expand Up @@ -244,33 +239,18 @@ else if ("toString".equals(method.getName())) {
}

final Class<?> returnType = m.getReturnType();

Function defaultSupplier = knownCollections.getOrDefault(returnType, (ignored) -> null);

// The proper type for this would be Function<Object[], returnType> but we can't express that in Java.
final Function defaultSupplier;
if (m.getAnnotation(DefaultValue.class) != null) {
if (m.isDefault()) {
throw new IllegalArgumentException("@DefaultValue cannot be defined on a method with a default implementation for method "
+ m.getDeclaringClass().getName() + "#" + m.getName());
} else if (
Map.class.isAssignableFrom(returnType) ||
List.class.isAssignableFrom(returnType) ||
Set.class.isAssignableFrom(returnType) ) {
throw new IllegalArgumentException("@DefaultValue cannot be used with collections. Use default method implemenation instead "
+ m.getDeclaringClass().getName() + "#" + m.getName());
}

String value = m.getAnnotation(DefaultValue.class).value();
if (returnType == String.class) {
defaultSupplier = memoize((T) config.resolve(value));
} else {
defaultSupplier = memoize(decoder.decode(returnType, config.resolve(value)));
}
}

if (m.isDefault()) {
defaultSupplier = createAnnotatedMethodSupplier(m, returnType, config, decoder);
} else if (m.isDefault()) {
defaultSupplier = createDefaultMethodSupplier(m, type, proxyObject);
} else {
defaultSupplier = knownCollections.getOrDefault(returnType, (ignored) -> null);
}


final PropertyName nameAnnot = m.getAnnotation(PropertyName.class);
final String propName = nameAnnot != null && nameAnnot.name() != null
? prefix + nameAnnot.name()
Expand Down Expand Up @@ -303,54 +283,33 @@ else if ("toString".equals(method.getName())) {
return proxyObject;
}

private static <T> Function<Object[], T> createAnnotatedMethodSupplier(Method m, Class<T> returnType, Config config, Decoder decoder) {
if (m.isDefault()) {
throw new IllegalArgumentException("@DefaultValue cannot be defined on a method with a default implementation for method "
+ m.getDeclaringClass().getName() + "#" + m.getName());
} else if (
Map.class.isAssignableFrom(returnType) ||
List.class.isAssignableFrom(returnType) ||
Set.class.isAssignableFrom(returnType) ) {
throw new IllegalArgumentException("@DefaultValue cannot be used with collections. Use default method implemenation instead "
+ m.getDeclaringClass().getName() + "#" + m.getName());
}

String value = m.getAnnotation(DefaultValue.class).value();
if (returnType == String.class) {
return memoize((T) config.resolve(value));
} else {
return memoize(decoder.decode(returnType, config.resolve(value)));
}
}

private static <T> Function<Object[], T> memoize(T value) {
return (ignored) -> value;
}

private static <T> Function<Object[], T> createDefaultMethodSupplier(Method method, Class<T> type, T proxyObject) {
final MethodHandle methodHandle;

try {
if (SystemUtils.IS_JAVA_1_8) {
Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
.getDeclaredConstructor(Class.class, int.class);
constructor.setAccessible(true);
methodHandle = constructor.newInstance(type, MethodHandles.Lookup.PRIVATE)
.unreflectSpecial(method, type)
.bindTo(proxyObject);
}
else {
// Java 9 onwards
methodHandle = MethodHandles.lookup()
.findSpecial(type,
method.getName(),
MethodType.methodType(method.getReturnType(), method.getParameterTypes()),
type)
.bindTo(proxyObject);
}
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to create temporary object for " + type.getName(), e);
}

return (args) -> {
try {
if (methodHandle.type().parameterCount() == 0) {
//noinspection unchecked
return (T) methodHandle.invokeWithArguments();
} else if (args != null) {
//noinspection unchecked
return (T) methodHandle.invokeWithArguments(args);
} else {
// This is a handle to a method WITH arguments, being called with none. This happens when toString()
// is trying to build a representation of a proxy that has a parametrized property AND the interface
// provides a default method for it. There's no good default to return here, so we'll just use null
return null;
}
} catch (Throwable e) {
maybeWrapThenRethrow(e);
return null; // Unreachable, but the compiler doesn't know
}
};
// type param ? is the return type for the METHOD, but we can't express that in Java
private static <T> Function<Object[], ?> createDefaultMethodSupplier(Method method, Class<T> type, T proxyObject) {
return DefaultMethodInvokerFactory.getFactory().findOrCreateDefaultMethodInvoker(type, method, proxyObject);
}

protected <T> MethodInvoker<T> createInterfaceProperty(String propName, final T proxy) {
Expand Down Expand Up @@ -411,14 +370,4 @@ <R> R getPropertyWithDefault(Class<R> type, String propName) {
}
};
}

private static void maybeWrapThenRethrow(Throwable t) {
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
}
if (t instanceof Error) {
throw (Error) t;
}
throw new RuntimeException(t);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package com.netflix.archaius;

import org.apache.commons.lang3.JavaVersion;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Function;

abstract class DefaultMethodInvokerFactory {
private static final Logger LOG = LoggerFactory.getLogger(DefaultMethodInvokerFactory.class);

private static final DefaultMethodInvokerFactory INSTANCE;
static {
if (SystemUtils.isJavaVersionAtMost(JavaVersion.JAVA_1_8)) {
INSTANCE = new LegacyJava8Factory();
} else if (SystemUtils.isJavaVersionAtMost(JavaVersion.JAVA_16)) {
INSTANCE = new BoundMethodHandlesFactory();
} else { // Java 17 onwards
INSTANCE = new LambdasFactory();
}
LOG.info("Choosing {} as invoker factory for default methods", INSTANCE.getClass());
}

static DefaultMethodInvokerFactory getFactory() {
return INSTANCE;
}

/**
* Returns a Function object that acts as a call to <code>invokerBound.method(...)</code>.
* <p>
* {@literal <}R> is expected to be the METHOD's return type. Unfortunately this constraint can't be enforced by the compiler.
*
* @param methodOwner An interface that declares (or inherits) the wanted method.
* @param method The method to be called. This is expected to be a <code>default</code> method in METHOD_OWNER.
* @param invokerBound An instance of the METHOD_OWNER interface.
*/
abstract <T, R> Function<Object[], R> findOrCreateDefaultMethodInvoker(Class<T> methodOwner, Method method, T invokerBound);

/**
* Shared "fall back" implementation using reflection to invoke the method handle.
*/
<T, R> Function<Object[], R> bindMethodHandle(MethodHandle methodHandle, T invokerBound) {
// Exceptions from this call are NOT caught because we want to fail fast if the arguments to the factory are bad.
MethodHandle boundHandle = methodHandle.bindTo(invokerBound);

return (args) -> {
try {
if (boundHandle.type().parameterCount() == 0) {

//noinspection unchecked
return (R) boundHandle.invokeWithArguments();
} else if (args != null) {

//noinspection unchecked
return (R) boundHandle.invokeWithArguments(args);
} else {
// This is a handle to a method WITH arguments, being called with none. This happens when
// invokerBound.toString() is called on an object that has a parametrized property AND the interface
// provides a default method for it. There's no good default to return here, so we'll just use null
return null;
}
} catch (Throwable e) {
maybeWrapThenRethrow(e);
return null; // Unreachable, but the compiler doesn't know
}
};

}


/**
* For Java 17 onwards, we wrap the method calls in lambdas and return that. This skips the reflection machinery and
* provides a noticeable performance boost. This implementation is known to fail in java 9 - 11. It *should* work
* from 12 or 13 onwards, but we have only tested it in 17 and 21.
*/
private static class LambdasFactory extends DefaultMethodInvokerFactory {
// We keep a running count of how many times we've seen a given Class. If we cross the threshold we stop creating
// lambdas for that class and fall back to the reflective implementation. This is motivated by an edge case we
// saw in clients, where they create (and leak) large numbers of proxies to the same interface. Because each lambda
// object requires its own anon class object, which lives in metaspace, and since users sometimes size their
// metaspaces to be much smaller than the heap, the leak becomes much more visible and causes OOMs.
private static final Map<String, Integer> SEEN_COUNTS = new ConcurrentHashMap<>();

// The threshold is simply a best guess. Creating 1000 lambdas for the same interface is almost certainly
// a leak, right?
private static final int MAX_SEEN_THRESHOLD = 1000;

@Override
<T, R> Function<Object[], R> findOrCreateDefaultMethodInvoker(Class<T> methodOwner, Method method, T invokerBound) {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle methodHandle;

try {
methodHandle = lookup.findSpecial(
method.getDeclaringClass(),
method.getName(),
MethodType.methodType(method.getReturnType(), method.getParameterTypes()),
method.getDeclaringClass());
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException(e);
}

if (methodHandle.type().parameterCount() <= 2 &&
SEEN_COUNTS.getOrDefault(methodOwner.getName(), 0) <= MAX_SEEN_THRESHOLD) {

// For 1 or 2 argument handles (which translate to 0 and 1 argument *methods*), build a lambda object
// and use that to make the call. This avoids the cost of the reflection machinery and has a significant
// performance boost.
// But first, count how many times we've been here for this owner
SEEN_COUNTS.merge(methodOwner.getName(), 1, (oldValue, ignore) -> oldValue + 1);

if (methodHandle.type().parameterCount() == 1) {
Function<Object, Object> getter = asFunction(lookup, methodHandle);
//noinspection unchecked,DataFlowIssue
return (args) -> (R) getter.apply(invokerBound);

} else if (methodHandle.type().parameterCount() == 2) {
BiFunction<Object, Object, Object> getter = asBiFunction(lookup, methodHandle);
//noinspection unchecked,DataFlowIssue
return (args) ->
args == null ? null : (R) getter.apply(invokerBound, args[0]);
}
}

// Otherwise, for handles with more than 2 arguments or if we've reached the threshold, fall back to
// reflection
return bindMethodHandle(methodHandle, invokerBound);
}

/**
* For a given no-args method M, return a (possibly cached) Function equivalent to the lambda
* <code>instance -> instance.M()</code>,
* where <code>instance</code> is an object of an adequate type.
*/
@SuppressWarnings("unchecked")
private static Function<Object, Object> asFunction(MethodHandles.Lookup lookup, MethodHandle methodHandle) {
try {
CallSite site = LambdaMetafactory.metafactory(lookup,
"apply",
MethodType.methodType(Function.class),
MethodType.methodType(Object.class, Object.class),
methodHandle,
methodHandle.type());
return (Function<Object, Object>) site.getTarget().invokeExact();
} catch (Throwable t) {
maybeWrapThenRethrow(t);
return null; // Unreachable, but the compiler doesn't know.
}
}

@SuppressWarnings("unchecked")
private static BiFunction<Object, Object, Object> asBiFunction(MethodHandles.Lookup lookup, MethodHandle methodHandle) {
try {
CallSite site = LambdaMetafactory.metafactory(lookup,
"apply",
MethodType.methodType(BiFunction.class),
MethodType.methodType(Object.class, Object.class, Object.class),
methodHandle,
methodHandle.type());
return (BiFunction<Object, Object, Object>) site.getTarget().invokeExact();
} catch (Throwable t) {
maybeWrapThenRethrow(t);
return null; // Unreachable, but the compiler doesn't know
}
}
}

/**
* An implementation safe to use in Java 9 - 16. It looks up a method handle which it then
* binds directly to the requested instance.
*/
private static class BoundMethodHandlesFactory extends DefaultMethodInvokerFactory {
@Override
<T, R> Function<Object[], R> findOrCreateDefaultMethodInvoker(Class<T> methodOwner, Method method, T invokerBound) {
final MethodHandle methodHandle;

try {
methodHandle = MethodHandles.lookup()
.findSpecial(methodOwner,
method.getName(),
MethodType.methodType(method.getReturnType(), method.getParameterTypes()),
methodOwner);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException("Failed to create temporary object for " + methodOwner.getName(), e);
}

return bindMethodHandle(methodHandle, invokerBound);
}
}

/**
* For Java 8 we use a hacky mechanism (which got disabled in Java 9) to look up a method handle which we then
* bind directly to the requested function object.
*/
private static class LegacyJava8Factory extends DefaultMethodInvokerFactory {

@Override
<T, R> Function<Object[], R> findOrCreateDefaultMethodInvoker(Class<T> methodOwner, Method method, T invokerBound) {
final MethodHandle methodHandle;

try {
Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
.getDeclaredConstructor(Class.class, int.class);
constructor.setAccessible(true);
methodHandle = constructor.newInstance(methodOwner, MethodHandles.Lookup.PRIVATE)
.unreflectSpecial(method, methodOwner);

} catch (NoSuchMethodException | InstantiationException |
IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Failed to create temporary object for " + methodOwner.getName(), e);
}

return bindMethodHandle(methodHandle, invokerBound);
}
}

private static void maybeWrapThenRethrow(Throwable t) {
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
}
if (t instanceof Error) {
throw (Error) t;
}
throw new RuntimeException(t);
}
}
Loading

0 comments on commit 05d51af

Please sign in to comment.