![JAR search and dependency download from the Maven repository](/logo.png)
com.netflix.archaius.ConfigProxyFactory Maven / Gradle / Ivy
package com.netflix.archaius;
import com.netflix.archaius.api.Config;
import com.netflix.archaius.api.Decoder;
import com.netflix.archaius.api.Property;
import com.netflix.archaius.api.PropertyFactory;
import com.netflix.archaius.api.PropertyRepository;
import com.netflix.archaius.api.annotations.Configuration;
import com.netflix.archaius.api.annotations.DefaultValue;
import com.netflix.archaius.api.annotations.PropertyName;
import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.text.StrSubstitutor;
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.ParameterizedType;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* 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.
*
* For example,
*
* {@code
* {@literal @}Configuration(prefix="foo")
* interface FooConfiguration {
* int getTimeout(); // maps to "foo.timeout"
*
* String getName(); // maps to "foo.name"
* }
* }
*
*
* Default values may be set by adding a {@literal @}DefaultValue with a default value string. Note
* 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.
*
* For example,
*
* {@code
* {@literal @}Configuration(prefix="foo")
* interface FooConfiguration {
* @DefaultValue("1000")
* int getReadTimeout(); // maps to "foo.timeout"
*
* default int getWriteTimeout() {
* return 1000;
* }
* }
* }
*
* To create a proxy instance,
*
* {@code
* FooConfiguration fooConfiguration = configProxyFactory.newProxy(FooConfiguration.class);
* }
*
*
* 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.
*
*
* {@code
* FooConfiguration fooConfiguration = configProxyFactory.newProxy(FooConfiguration.class, "otherprefix.foo");
* }
*
*
* 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
*/
public class ConfigProxyFactory {
/**
* The decoder is used for the purpose of decoding any @DefaultValue annotation
*/
private final Decoder decoder;
private final PropertyRepository propertyRepository;
private final Config config;
@Inject
public ConfigProxyFactory(Config config, Decoder decoder, PropertyFactory factory) {
this.decoder = decoder;
this.config = config;
this.propertyRepository = factory;
}
@Deprecated
public ConfigProxyFactory(Config config, PropertyFactory factory) {
this.decoder = config.getDecoder();
this.config = config;
this.propertyRepository = factory;
}
@Deprecated
public ConfigProxyFactory(Config config) {
this.decoder = config.getDecoder();
this.config = config;
this.propertyRepository = DefaultPropertyFactory.from(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 newProxy(final Class type) {
return newProxy(type, null);
}
private String derivePrefix(Configuration annot, String prefix) {
if (prefix == null && annot != null) {
prefix = annot.prefix();
if (prefix == null) {
prefix = "";
}
}
if (prefix == null)
return "";
if (prefix.endsWith(".") || prefix.isEmpty())
return prefix;
return prefix + ".";
}
public T newProxy(final Class type, final String initialPrefix) {
Configuration annot = type.getAnnotation(Configuration.class);
return newProxy(type, initialPrefix, annot == null ? false : annot.immutable());
}
/**
* Encapsulated the invocation of a single method of the interface
*
* @param
*/
static interface MethodInvoker {
/**
* Invoke the method with the provided arguments
* @param args
* @return
*/
T invoke(Object[] args);
}
/**
* Abstract method invoker that encapsulates a property
* @param
*/
private static abstract class PropertyMethodInvoker extends AbstractProperty implements MethodInvoker {
private final Supplier next;
public PropertyMethodInvoker(String key, Supplier next) {
super(key);
this.next = next;
}
@Override
public T invoke(Object[] args) {
T result = get();
if (result == null) {
return next.get();
}
return result;
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
T newProxy(final Class type, final String initialPrefix, boolean immutable) {
Configuration annot = type.getAnnotation(Configuration.class);
final String prefix = derivePrefix(annot, initialPrefix);
// Iterate through all declared methods of the class looking for setter methods.
// Each setter will be mapped to a Property for the property name:
// prefix + lowerCamelCaseDerivedPropertyName
final Map> invokers = new HashMap<>();
final Map propertyNames = new HashMap<>();
final InvocationHandler handler = (proxy, method, args) -> {
MethodInvoker> invoker = invokers.get(method);
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());
}
};
final T proxyObject = (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, handler);
for (Method m : type.getMethods()) {
try {
final MethodInvoker> invoker;
final String verb;
if (m.getName().startsWith("get")) {
verb = "get";
} else if (m.getName().startsWith("is")) {
verb = "is";
} else {
verb = "";
}
final Class> returnType = m.getReturnType();
Supplier defaultSupplier = () -> null;
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 = createDefaultMethodSupplier(m, type, proxyObject);
}
final PropertyName nameAnnot = m.getAnnotation(PropertyName.class);
final String propName = nameAnnot != null && nameAnnot.name() != null
? prefix + nameAnnot.name()
: prefix + Character.toLowerCase(m.getName().charAt(verb.length())) + m.getName().substring(verb.length() + 1);
propertyNames.put(m, propName);
if (returnType.equals(Map.class)) {
invoker = createMapProperty(propName, (ParameterizedType)m.getGenericReturnType(), LinkedHashMap::new, defaultSupplier);
} else if (returnType.equals(Set.class)) {
invoker = createCollectionProperty(propName, (ParameterizedType)m.getGenericReturnType(), LinkedHashSet::new, defaultSupplier);
} else if (returnType.equals(SortedSet.class)) {
invoker = createCollectionProperty(propName, (ParameterizedType)m.getGenericReturnType(), TreeSet::new, defaultSupplier);
} else if (returnType.equals(List.class)) {
invoker = createCollectionProperty(propName, (ParameterizedType)m.getGenericReturnType(), ArrayList::new, defaultSupplier);
} else if (returnType.equals(LinkedList.class)) {
invoker = createCollectionProperty(propName, (ParameterizedType)m.getGenericReturnType(), LinkedList::new, defaultSupplier);
} else if (returnType.isInterface()) {
invoker = createInterfaceProperty(propName, newProxy(returnType, propName, immutable));
} else if (m.getParameterTypes() != null && m.getParameterTypes().length > 0) {
if (nameAnnot == null) {
throw new IllegalArgumentException("Missing @PropertyName annotation on " + m.getDeclaringClass().getName() + "#" + m.getName());
}
invoker = createParameterizedProperty(returnType, propName, nameAnnot.name(), defaultSupplier);
} else {
invoker = createScalarProperty(returnType, propName, defaultSupplier);
}
if (immutable) {
Object value = invoker.invoke(new Object[]{});
invokers.put(m, (args) -> value);
} else {
invokers.put(m, invoker);
}
} catch (Exception e) {
throw new RuntimeException("Error proxying method " + m.getName(), e);
}
}
return proxyObject;
}
private static Supplier memoize(T value) {
return () -> value;
}
private static Supplier createDefaultMethodSupplier(Method method, Class type, T proxyObject) {
final MethodHandle methodHandle;
try {
if (SystemUtils.IS_JAVA_1_8) {
Constructor 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(), new Class[0]),
type)
.bindTo(proxyObject);
}
} catch (Throwable e) {
throw new RuntimeException("Failed to create temporary object for " + type.getName(), e);
}
return () -> {
try {
//noinspection unchecked
return (T) methodHandle.invokeWithArguments();
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private MethodInvoker createCollectionProperty(String propName, ParameterizedType type, Supplier> collectionFactory, Supplier next) {
final Class valueType = (Class) type.getActualTypeArguments()[0];
final Property prop = propertyRepository
.get(propName, String.class)
.map(s -> {
Collection list = collectionFactory.get();
if (!s.isEmpty()) {
Arrays.asList(s.split("\\s*,\\s*")).forEach(v -> {
if (!v.isEmpty() || valueType == String.class) {
list.add(decoder.decode(valueType, v));
}
});
}
return (T) list;
})
.orElse(next.get());
return args -> Optional.ofNullable(prop.get()).orElseGet((Supplier extends T>) collectionFactory);
}
private MethodInvoker createMapProperty(final String propName, final ParameterizedType type, Supplier mapFactory, Supplier next) {
final Class> keyType = (Class>)type.getActualTypeArguments()[0];
final Class> valueType = (Class>)type.getActualTypeArguments()[1];
final Property prop = propertyRepository
.get(propName, String.class)
.map(s -> {
T result = mapFactory.get();
Arrays
.stream(s.split("\\s*,\\s*"))
.filter(pair -> !pair.isEmpty())
.map(pair -> pair.split("\\s*=\\s*"))
.forEach(kv -> result.put(decoder.decode(keyType, kv[0]), decoder.decode(valueType, kv[1])));
return (T)Collections.unmodifiableMap(result);
}
).orElse((T) Collections.unmodifiableMap(next.get()));
return args -> Optional.ofNullable(prop.get()).orElseGet(mapFactory);
}
protected Supplier defaultValueFromString(Class type, String defaultValue) {
return () -> decoder.decode(type, defaultValue);
}
protected MethodInvoker createInterfaceProperty(String propName, final T proxy, Supplier next) {
return new PropertyMethodInvoker(propName, next) {
@Override
public T get() {
return proxy;
}
};
}
protected MethodInvoker createInterfaceProperty(String propName, final T proxy) {
return (args) -> proxy;
}
protected MethodInvoker createScalarProperty(final Class type, final String propName, Supplier next) {
final Property prop = propertyRepository.get(propName, type);
return args -> Optional.ofNullable(prop.get()).orElseGet(next);
}
protected MethodInvoker createParameterizedProperty(final Class returnType, final String propName, final String nameAnnot, Supplier next) {
return new MethodInvoker() {
@Override
public T invoke(Object[] args) {
// Determine the actual property name by replacing with arguments using the argument index
// to the method. For example,
// @PropertyName(name="foo.${1}.${0}")
// String getFooValue(String arg0, Integer arg1)
//
// called as getFooValue("bar", 1) would look for the property 'foo.1.bar'
Map values = new HashMap<>();
for (int i = 0; i < args.length; i++) {
values.put("" + i, args[i]);
}
String propName = new StrSubstitutor(values, "${", "}", '$').replace(nameAnnot);
T result = getPropertyWithDefault(returnType, propName);
if (result == null) {
result = next.get();
}
return result;
}
R getPropertyWithDefault(Class type, String propName) {
return propertyRepository.get(propName, type).get();
}
};
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy