Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lambdas for proxies take2 #686

Open
wants to merge 2 commits into
base: 2.x
Choose a base branch
from
Open
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
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 All @@ -38,11 +33,11 @@
* Factory for binding a configuration interface to properties in a {@link PropertyFactory}
* instance. Getter methods on the interface are mapped by naming convention
* by the property name may be overridden using the @PropertyName annotation.
*
* <p>
* For example,
* <pre>
* {@code
* {@literal @}Configuration(prefix="foo")
* @Configuration(prefix="foo")
* interface FooConfiguration {
* int getTimeout(); // maps to "foo.timeout"
*
Expand All @@ -55,11 +50,11 @@
* that the default value type is a string to allow for interpolation. Alternatively, methods can
* provide a default method implementation. Note that {@literal @}DefaultValue cannot be added to a default
* method as it would introduce ambiguity as to which mechanism wins.
*
* <p>
* For example,
* <pre>
* {@code
* {@literal @}Configuration(prefix="foo")
* @Configuration(prefix="foo")
* interface FooConfiguration {
* @DefaultValue("1000")
* int getReadTimeout(); // maps to "foo.timeout"
Expand All @@ -78,22 +73,23 @@
* </pre>
*
* To override the prefix in {@literal @}Configuration or provide a prefix when there is no
* @Configuration annotation simply pass in a prefix in the call to newProxy.
* {@literal @}Configuration annotation simply pass in a prefix in the call to newProxy.
*
* <pre>
* {@code
* FooConfiguration fooConfiguration = configProxyFactory.newProxy(FooConfiguration.class, "otherprefix.foo");
* }
* </pre>
*
* By default all properties are dynamic and can therefore change from call to call. To make the
* By default, all properties are dynamic and can therefore change from call to call. To make the
* configuration static set the immutable attributes of @Configuration to true.
*
* Note that an application should normally have just one instance of ConfigProxyFactory
* and PropertyFactory since PropertyFactory caches {@link com.netflix.archaius.api.Property} objects.
*
* @see {@literal }@Configuration
* @see Configuration
*/
@SuppressWarnings("deprecation")
public class ConfigProxyFactory {
private static final Logger LOG = LoggerFactory.getLogger(ConfigProxyFactory.class);

Expand All @@ -104,6 +100,7 @@ public class ConfigProxyFactory {
private final PropertyRepository propertyRepository;
private final Config config;

@SuppressWarnings("DIAnnotationInspectionTool")
@Inject
public ConfigProxyFactory(Config config, Decoder decoder, PropertyFactory factory) {
this.decoder = decoder;
Expand All @@ -128,10 +125,6 @@ public ConfigProxyFactory(Config config) {
/**
* Create a proxy for the provided interface type for which all getter methods are bound
* to a Property.
*
* @param type
* @param config
* @return
*/
public <T> T newProxy(final Class<T> type) {
return newProxy(type, null);
Expand All @@ -152,22 +145,22 @@ private String derivePrefix(Configuration annot, String prefix) {

return prefix + ".";
}


/**
* Create a proxy for the provided interface type for which all getter methods are bound
* to a Property. The proxy uses the provided prefix, even if there is a {@link Configuration} annotation in TYPE.
*/
public <T> T newProxy(final Class<T> type, final String initialPrefix) {
Configuration annot = type.getAnnotation(Configuration.class);
return newProxy(type, initialPrefix, annot == null ? false : annot.immutable());
return newProxy(type, initialPrefix, annot != null && annot.immutable());
}

/**
* Encapsulated the invocation of a single method of the interface
*
* @param <T>
* Encapsulate the invocation of a single method of the interface
*/
interface MethodInvoker<T> {
/**
* Invoke the method with the provided arguments
* @param args
* @return
*/
T invoke(Object[] args);
}
Expand Down Expand Up @@ -199,30 +192,16 @@ <T> T newProxy(final Class<T> type, final String initialPrefix, boolean immutabl
if (invoker != null) {
return invoker.invoke(args);
}
if ("equals".equals(method.getName())) {
return proxy == args[0];
}
else if ("hashCode".equals(method.getName())) {
return System.identityHashCode(proxy);
}
else if ("toString".equals(method.getName())) {
StringBuilder sb = new StringBuilder();
sb.append(type.getSimpleName()).append("[");
sb.append(invokers.entrySet().stream().map(entry -> {
StringBuilder sbProperty = new StringBuilder();
sbProperty.append(propertyNames.get(entry.getKey()).substring(prefix.length())).append("='");
try {
sbProperty.append(entry.getValue().invoke(null));
} catch (Exception e) {
sbProperty.append(e.getMessage());
}
sbProperty.append("'");
return sbProperty.toString();
}).collect(Collectors.joining(",")));
sb.append("]");
return sb.toString();
} else {
throw new NoSuchMethodError(method.getName() + " not found on interface " + type.getName());

switch (method.getName()) {
case "equals":
return proxy == args[0];
case "hashCode":
return System.identityHashCode(proxy);
case "toString":
return proxyToString(type, prefix, invokers, propertyNames);
default:
throw new NoSuchMethodError(method.getName() + " not found on interface " + type.getName());
}
};

Expand All @@ -244,33 +223,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 +267,34 @@ 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) {
//noinspection unchecked
return memoize((T) config.resolve(value)); // The cast is actually a no-op, T == String here!
} 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 @@ -412,13 +356,27 @@ <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);
/** Compute a reasonable toString() for a proxy object */
private static <T> String proxyToString(Class<T> type, String prefix, Map<Method, MethodInvoker<?>> invokers, Map<Method, String> propertyNames) {
StringBuilder sb = new StringBuilder();
sb.append(type.getSimpleName()).append("[");

sb.append(invokers.entrySet().stream().map(entry -> {
StringBuilder sbProperty = new StringBuilder();
sbProperty.append(propertyNames.get(entry.getKey()).substring(prefix.length())).append("='");

try {
sbProperty.append(entry.getValue().invoke(null));
} catch (Exception e) {
sbProperty.append(e.getMessage());
}

sbProperty.append("'");
return sbProperty.toString();

}).collect(Collectors.joining(",")));

sb.append("]");
return sb.toString();
}
}
Loading