diff --git a/archaius2-core/build.gradle b/archaius2-core/build.gradle
index a11fec65..ac656b06 100644
--- a/archaius2-core/build.gradle
+++ b/archaius2-core/build.gradle
@@ -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'
}
diff --git a/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java b/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java
index 04516926..ceb391c0 100644
--- a/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java
+++ b/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java
@@ -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;
@@ -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.
- *
+ *
* For example,
*
* {@code
- * {@literal @}Configuration(prefix="foo")
+ * @Configuration(prefix="foo")
* interface FooConfiguration {
* int getTimeout(); // maps to "foo.timeout"
*
@@ -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.
- *
+ *
* For example,
*
* {@code
- * {@literal @}Configuration(prefix="foo")
+ * @Configuration(prefix="foo")
* interface FooConfiguration {
* @DefaultValue("1000")
* int getReadTimeout(); // maps to "foo.timeout"
@@ -78,7 +73,7 @@
*
*
* 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.
*
*
* {@code
@@ -86,14 +81,15 @@
* }
*
*
- * 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);
@@ -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;
@@ -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 newProxy(final Class type) {
return newProxy(type, null);
@@ -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 newProxy(final Class 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
+ * Encapsulate the invocation of a single method of the interface
*/
interface MethodInvoker {
/**
* Invoke the method with the provided arguments
- * @param args
- * @return
*/
T invoke(Object[] args);
}
@@ -199,30 +192,16 @@ T newProxy(final Class 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());
}
};
@@ -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 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()
@@ -303,54 +267,34 @@ else if ("toString".equals(method.getName())) {
return proxyObject;
}
+ private static Function createAnnotatedMethodSupplier(Method m, Class 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 Function memoize(T value) {
return (ignored) -> value;
}
- private static Function 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(), 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 Function createDefaultMethodSupplier(Method method, Class type, T proxyObject) {
+ return DefaultMethodInvokerFactory.getFactory().findOrCreateDefaultMethodInvoker(type, method, proxyObject);
}
protected MethodInvoker createInterfaceProperty(String propName, final T proxy) {
@@ -412,13 +356,27 @@ R getPropertyWithDefault(Class 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 String proxyToString(Class type, String prefix, Map> invokers, Map 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();
}
}
diff --git a/archaius2-core/src/main/java/com/netflix/archaius/DefaultMethodInvokerFactory.java b/archaius2-core/src/main/java/com/netflix/archaius/DefaultMethodInvokerFactory.java
new file mode 100644
index 00000000..9e53d740
--- /dev/null
+++ b/archaius2-core/src/main/java/com/netflix/archaius/DefaultMethodInvokerFactory.java
@@ -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 invokerBound.method(...)
.
+ *
+ * {@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 default
method in METHOD_OWNER.
+ * @param invokerBound An instance of the METHOD_OWNER interface.
+ */
+ abstract Function findOrCreateDefaultMethodInvoker(Class methodOwner, Method method, T invokerBound);
+
+ /**
+ * Shared "fall back" implementation using reflection to invoke the method handle.
+ */
+ Function 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 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
+ Function findOrCreateDefaultMethodInvoker(Class 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 getter = asFunction(lookup, methodHandle);
+ //noinspection unchecked,DataFlowIssue
+ return (args) -> (R) getter.apply(invokerBound);
+
+ } else if (methodHandle.type().parameterCount() == 2) {
+ BiFunction 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
+ * instance -> instance.M()
,
+ * where instance
is an object of an adequate type.
+ */
+ @SuppressWarnings("unchecked")
+ private static Function 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) site.getTarget().invokeExact();
+ } catch (Throwable t) {
+ maybeWrapThenRethrow(t);
+ return null; // Unreachable, but the compiler doesn't know.
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static BiFunction 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) 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
+ Function findOrCreateDefaultMethodInvoker(Class 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
+ Function findOrCreateDefaultMethodInvoker(Class methodOwner, Method method, T invokerBound) {
+ final MethodHandle methodHandle;
+
+ try {
+ Constructor 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);
+ }
+}
diff --git a/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java b/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java
index 56fbedde..f07674f2 100644
--- a/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java
+++ b/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java
@@ -28,6 +28,7 @@
import com.netflix.archaius.config.DefaultSettableConfig;
import com.netflix.archaius.config.EmptyConfig;
+@SuppressWarnings("deprecation")
public class ProxyFactoryTest {
public enum TestEnum {
NONE,
@@ -46,6 +47,7 @@ public interface ImmutableConfig {
String getValueWithoutDefault2();
}
+ @SuppressWarnings("unused")
public interface BaseConfig {
@DefaultValue("basedefault")
String getStr();
@@ -60,6 +62,7 @@ default long getLongValueWithDefault() {
}
}
+ @SuppressWarnings("UnusedReturnValue")
public interface RootConfig extends BaseConfig {
@DefaultValue("default")
@Override
@@ -92,7 +95,7 @@ public interface SubConfig {
}
public static class SubConfigFromString {
- private String[] parts;
+ private final String[] parts;
public SubConfigFromString(String value) {
this.parts = value.split(":");
@@ -203,11 +206,11 @@ public void testAllPropertiesSet() {
a.getRequiredValue();
Assert.fail("should have failed with no value for requiredValue");
}
- catch (Exception e) {
+ catch (Exception expected) {
}
}
- static interface WithArguments {
+ interface WithArguments {
@PropertyName(name="${0}.abc.${1}")
@DefaultValue("default")
String getProperty(String part0, int part1);
@@ -229,7 +232,7 @@ public void testWithArguments() {
}
@Configuration(prefix = "foo.bar")
- static interface WithArgumentsAndPrefix {
+ interface WithArgumentsAndPrefix {
@PropertyName(name="baz.${0}.abc.${1}")
@DefaultValue("default")
String getPropertyWithoutPrefix(String part0, int part1);
@@ -259,10 +262,16 @@ public void testWithArgumentsAndPrefix() {
}
+ @SuppressWarnings("unused")
public interface WithArgumentsAndDefaultMethod {
@PropertyName(name="${0}.abc.${1}")
- default String getProperty(String part0, int part1) {
- return "default";
+ default String getPropertyWith2Placeholders(String part0, int part1) {
+ return "defaultFor2";
+ }
+
+ @PropertyName(name="${0}.def")
+ default String getPropertyWith1Placeholder(String part0) {
+ return "defaultFor1";
}
}
@@ -271,14 +280,18 @@ public void testWithArgumentsAndDefaultMethod() {
SettableConfig config = new DefaultSettableConfig();
config.setProperty("a.abc.1", "value1");
config.setProperty("b.abc.2", "value2");
+ config.setProperty("c.def", "value_c");
PropertyFactory factory = DefaultPropertyFactory.from(config);
ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory);
WithArgumentsAndDefaultMethod withArgsAndDefM = proxy.newProxy(WithArgumentsAndDefaultMethod.class);
- Assert.assertEquals("value1", withArgsAndDefM.getProperty("a", 1));
- Assert.assertEquals("value2", withArgsAndDefM.getProperty("b", 2));
- Assert.assertEquals("default", withArgsAndDefM.getProperty("a", 2));
+ Assert.assertEquals("value1", withArgsAndDefM.getPropertyWith2Placeholders("a", 1));
+ Assert.assertEquals("value2", withArgsAndDefM.getPropertyWith2Placeholders("b", 2));
+ Assert.assertEquals("defaultFor2", withArgsAndDefM.getPropertyWith2Placeholders("a", 2));
+
+ Assert.assertEquals("value_c", withArgsAndDefM.getPropertyWith1Placeholder("c"));
+ Assert.assertEquals("defaultFor1", withArgsAndDefM.getPropertyWith1Placeholder("q"));
}
public interface ConfigWithMaps {
@@ -398,7 +411,7 @@ public void emptyNonStringValuesIgnoredInCollections() {
Assert.assertEquals(Arrays.asList(1,2,4), new ArrayList<>(withCollections.getSortedSet()));
}
- public static interface ConfigWithStringCollections {
+ public interface ConfigWithStringCollections {
List getList();
Set getSet();
@@ -455,6 +468,7 @@ public void testCollectionsWithoutValue() {
Assert.assertTrue(withCollections.getSortedSet().isEmpty());
}
+ @SuppressWarnings("unused")
public interface ConfigWithCollectionsWithDefaultValueAnnotation {
@DefaultValue("")
LinkedList getLinkedList();
@@ -485,12 +499,13 @@ public void interfaceDefaultCollections() {
ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory);
ConfigWithDefaultStringCollections withCollections = proxy.newProxy(ConfigWithDefaultStringCollections.class);
- Assert.assertEquals(Arrays.asList("default"), new ArrayList<>(withCollections.getList()));
- Assert.assertEquals(Arrays.asList("default"), new ArrayList<>(withCollections.getSet()));
- Assert.assertEquals(Arrays.asList("default"), new ArrayList<>(withCollections.getSortedSet()));
+ Assert.assertEquals(Collections.singletonList("default"), new ArrayList<>(withCollections.getList()));
+ Assert.assertEquals(Collections.singletonList("default"), new ArrayList<>(withCollections.getSet()));
+ Assert.assertEquals(Collections.singletonList("default"), new ArrayList<>(withCollections.getSortedSet()));
}
- public static interface FailingError {
+ @SuppressWarnings("UnusedReturnValue")
+ public interface FailingError {
default String getValue() { throw new IllegalStateException("error"); }
}
@@ -506,18 +521,22 @@ public void interfaceWithBadDefault() {
@Test
public void testObjectMethods() {
+ // These tests just ensure that toString, equals and hashCode have implementations that don't fail.
SettableConfig config = new DefaultSettableConfig();
PropertyFactory factory = DefaultPropertyFactory.from(config);
ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory);
WithArguments withArgs = proxy.newProxy(WithArguments.class);
Assert.assertEquals("WithArguments[${0}.abc.${1}='default']", withArgs.toString());
+ //noinspection ObviousNullCheck
Assert.assertNotNull(withArgs.hashCode());
- Assert.assertTrue(withArgs.equals(withArgs));
+ //noinspection EqualsWithItself
+ Assert.assertEquals(withArgs, withArgs);
}
@Test
public void testObjectMethods_ClassWithArgumentsAndDefaultMethod() {
+ // These tests just ensure that toString, equals and hashCode have implementations that don't fail.
SettableConfig config = new DefaultSettableConfig();
PropertyFactory factory = DefaultPropertyFactory.from(config);
ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory);
@@ -525,8 +544,11 @@ public void testObjectMethods_ClassWithArgumentsAndDefaultMethod() {
// Printing 'null' here is a compromise. The default method in the interface is being called with a bad signature.
// There's nothing the proxy could return here that isn't a lie, but at least this is a mostly harmless lie.
- Assert.assertEquals("WithArgumentsAndDefaultMethod[${0}.abc.${1}='null']", withArgs.toString());
+ // This test depends implicitly on the iteration order for HashMap, which could change on future Java releases.
+ Assert.assertEquals("WithArgumentsAndDefaultMethod[${0}.def='null',${0}.abc.${1}='null']", withArgs.toString());
+ //noinspection ObviousNullCheck
Assert.assertNotNull(withArgs.hashCode());
- Assert.assertTrue(withArgs.equals(withArgs));
+ //noinspection EqualsWithItself
+ Assert.assertEquals(withArgs, withArgs);
}
}
diff --git a/archaius2-persisted2/build.gradle b/archaius2-persisted2/build.gradle
index c50f44c6..252f5e5a 100644
--- a/archaius2-persisted2/build.gradle
+++ b/archaius2-persisted2/build.gradle
@@ -21,7 +21,7 @@ dependencies {
implementation 'com.fasterxml.jackson.core:jackson-core:2.4.3'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.4.3'
implementation 'javax.annotation:javax.annotation-api:1.3.2'
- implementation 'org.apache.commons:commons-lang3:3.3.2'
+ implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.slf4j:slf4j-api:1.7.36'
testImplementation project(':archaius2-guice')
diff --git a/archaius2-test/build.gradle b/archaius2-test/build.gradle
index b8ba3bb9..84bf8d61 100644
--- a/archaius2-test/build.gradle
+++ b/archaius2-test/build.gradle
@@ -4,7 +4,7 @@ apply plugin: 'java-library'
dependencies {
api project(':archaius2-core')
api 'junit:junit:4.13.2'
- implementation 'org.apache.commons:commons-lang3:3.3.2'
+ implementation 'org.apache.commons:commons-lang3:3.12.0'
}
eclipse {