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.TypeConverter;
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.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.text.StrLookup;
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.Modifier;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.WeakHashMap;
import java.util.function.Function;
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 or may be overridden using the @{@link PropertyName} annotation.
*
* For example,
*
* {@code
* @Configuration(prefix="foo")
* interface FooConfiguration {
* int getTimeout(); // maps to "foo.timeout"
*
* String getName(); // maps to "foo.name"
*
* @PropertyName(name="bar")
* String getSomeOtherName(); // maps to "foo.bar"
* }
* }
*
*
* Default values may be set by adding a {@literal @}{@link 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
* @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 @}{@link Configuration} or provide a prefix when there is no
* {@literal @}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 {@link Configuration#immutable()} to true. Creation of an immutable configuration
* will fail if the interface contains parametrized methods or methods that return primitive types and do not have a
* value set at the moment of creation, from either the underlying config, a {@link DefaultValue} annotation, or a
* default method implementation.
*
* 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 Configuration
*/
@SuppressWarnings("deprecation")
public class ConfigProxyFactory {
private static final Logger LOG = LoggerFactory.getLogger(ConfigProxyFactory.class);
// Users sometimes leak both factories and proxies, leading to hard-to-track-down memory problems.
// We use these maps to keep track of how many instances of each are created and make log noise to help them
// track down the culprits. WeakHashMaps to avoid holding onto objects ourselves.
/**
* Global count of proxy factories, indexed by Config object. An application could legitimately have more
* than one proxy factory per config, if they want to use different Decoders or PropertyFactories.
*/
private static final Map FACTORIES_COUNT = Collections.synchronizedMap(new WeakHashMap<>());
private static final String EXCESSIVE_PROXIES_LIMIT = "archaius.excessiveProxiesLogging.limit";
/**
* Per-factory count of proxies, indexed by implemented interface and prefix. Because this count is kept per-proxy,
* it's also implicitly indexed by Config object :-)
*/
private final Map PROXIES_COUNT = Collections.synchronizedMap(new WeakHashMap<>());
/**
* The decoder is used for the purpose of decoding any @DefaultValue annotation
*/
private final Decoder decoder;
private final PropertyRepository propertyRepository;
private final Config config;
private final int excessiveProxyLimit;
/**
* Build a proxy factory from the provided config, decoder and PropertyFactory. Normal usage from most applications
* is to just set up injection bindings for those 3 objects and let your DI framework find this constructor.
*
* @param config Used to perform string interpolation in values from {@link DefaultValue} annotations. Weird things
* will happen if this is not the same Config that the PropertyFactory exposes!
* @param decoder Used to parse strings from {@link DefaultValue} annotations into the proper types.
* @param factory Used to access the config values that are returned by proxies created by this factory.
*/
@Inject
public ConfigProxyFactory(Config config, Decoder decoder, PropertyFactory factory) {
this.decoder = decoder;
this.config = config;
this.propertyRepository = factory;
excessiveProxyLimit = config.getInteger(EXCESSIVE_PROXIES_LIMIT, 5);
warnWhenTooMany(FACTORIES_COUNT, config, excessiveProxyLimit, () -> String.format("ProxyFactory(Config:%s)", config.hashCode()));
}
/**
* Build a proxy factory for a given Config. Use this ONLY if you need proxies associated with a different Config
* that your DI framework would normally give you.
*
* The constructed factory will use the Config's Decoder and a {@link DefaultPropertyFactory} built from that same
* Config object.
* @see #ConfigProxyFactory(Config, Decoder, PropertyFactory)
*/
@Deprecated
public ConfigProxyFactory(Config config) {
this(config, config.getDecoder(), DefaultPropertyFactory.from(config));
}
/**
* Build a proxy factory for a given Config and PropertyFactory. Use ONLY if you need to use a specialized
* PropertyFactory in your proxies. The constructed proxy factory will use the Config's Decoder.
* @see #ConfigProxyFactory(Config, Decoder, PropertyFactory)
*/
@Deprecated
public ConfigProxyFactory(Config config, PropertyFactory factory) {
this(config, config.getDecoder(), factory);
}
/**
* Create a proxy for the provided interface type for which all getter methods are bound
* to a Property.
*/
public T newProxy(final Class type) {
return newProxy(type, null);
}
/**
* 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 newProxy(final Class type, final String initialPrefix) {
Configuration annot = type.getAnnotation(Configuration.class);
return newProxy(type, initialPrefix, annot != null && annot.immutable());
}
/**
* Encapsulate the invocation of a single method of the interface
*/
protected interface PropertyValueGetter {
/**
* Invoke the method with the provided arguments
*/
T invoke(Object[] args);
}
/**
* Providers of "empty" defaults for the known collection types that we support as proxy method return types.
*/
private static final Map> knownCollections = new HashMap<>();
static {
knownCollections.put(Map.class, (ignored) -> Collections.emptyMap());
knownCollections.put(Set.class, (ignored) -> Collections.emptySet());
knownCollections.put(SortedSet.class, (ignored) -> Collections.emptySortedSet());
knownCollections.put(List.class, (ignored) -> Collections.emptyList());
knownCollections.put(LinkedList.class, (ignored) -> new LinkedList<>());
}
@SuppressWarnings({"unchecked", "rawtypes"})
T newProxy(final Class type, final String initialPrefix, boolean immutable) {
Configuration annot = type.getAnnotation(Configuration.class);
final String prefix = derivePrefix(annot, initialPrefix);
warnWhenTooMany(PROXIES_COUNT, new InterfaceAndPrefix(type, prefix), excessiveProxyLimit, () -> String.format("Proxy(%s, %s)", type, prefix));
// There's a circular dependency between these maps and the proxy object. They must be created first because the
// proxy's invocation handler needs to keep a reference to them, but the proxy must be created before they get
// filled because we may need to call methods on the interface in order to fill the maps :-|
final Map> invokers = new HashMap<>();
final Map propertyNames = new HashMap<>();
final InvocationHandler handler = new ConfigProxyInvocationHandler<>(type, prefix, invokers, propertyNames);
final T proxyObject = (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, handler);
List proxyingExceptions = new LinkedList<>();
// 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
for (Method method : type.getMethods()) {
if (Modifier.isStatic(method.getModifiers())) {
continue;
}
try {
MethodInvokerHolder methodInvokerHolder = buildInvokerForMethod(type, prefix, method, proxyObject, immutable);
propertyNames.put(method, methodInvokerHolder.propertyName);
if (immutable) {
// Cache the current value of the property and always return that.
// Note that this will fail for parameterized properties and for primitive-valued methods
// with no value set!
Object value = methodInvokerHolder.invoker.invoke(new Object[]{});
invokers.put(method, (args) -> value);
} else {
invokers.put(method, methodInvokerHolder.invoker);
}
} catch (RuntimeException e) {
// Capture the exception and continue processing the other methods. We'll throw them all at the end.
proxyingExceptions.add(e);
}
}
if (!proxyingExceptions.isEmpty()) {
String errors = proxyingExceptions.stream()
.map(Throwable::getMessage)
.collect(Collectors.joining("\n\t"));
RuntimeException exception = new RuntimeException(
"Failed to create a configuration proxy for class " + type.getName()
+ ":\n\t" + errors, proxyingExceptions.get(0));
proxyingExceptions.subList(1, proxyingExceptions.size()).forEach(exception::addSuppressed);
throw exception;
}
return proxyObject;
}
/**
* Build the actual prefix to use for config values read by a proxy.
* @param annot The (possibly null) annotation from the proxied interface.
* @param prefix A (possibly null) explicit prefix being passed by the user (or by an upper level proxy,
* in the case of nested interfaces). If present, it always overrides the annotation.
* @return A prefix to be prepended to all the config keys read by the methods in the proxy. If not empty, it will
* always end in a period .
*/
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 + ".";
}
@SuppressWarnings({"unchecked", "rawtypes"})
private MethodInvokerHolder buildInvokerForMethod(Class proxyObjectType, String prefix, Method m, T proxyObject, boolean immutable) {
try {
final Class> returnType = m.getReturnType();
final PropertyName nameAnnot = m.getAnnotation(PropertyName.class);
final String propName = getPropertyName(prefix, m, nameAnnot);
// A supplier for the value to be returned when the method's associated property is not set
// The proper parametrized type for this would be Function