Skip to content

Commit 3191c48

Browse files
Merge pull request #734 from Netflix/type-error-handling
Improve handling of error messages on decoding errors.
2 parents da35535 + 12509b4 commit 3191c48

File tree

7 files changed

+171
-77
lines changed

7 files changed

+171
-77
lines changed

archaius2-core/src/main/java/com/netflix/archaius/AbstractRegistryDecoder.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ public <T> T decode(Type type, String encoded) {
4848
}
4949
return converter.convert(encoded);
5050
} catch (Exception e) {
51-
throw new ParseException("Error decoding type `" + type.getTypeName() + "`", e);
51+
throw new ParseException("Unable to decode `"
52+
+ encoded
53+
+ "` as type `" + type.getTypeName() + "`: "
54+
+ e, e);
5255
}
5356
}
5457

@@ -73,9 +76,7 @@ private TypeConverter<?> getOrCreateConverter(Type type) {
7376
}
7477

7578
/**
76-
* Iterate through all TypeConverter#Factory's and return the first TypeConverter that matches
77-
* @param type
78-
* @return
79+
* Iterate through all TypeConverter#Factory's and return the first TypeConverter that matches the given type.
7980
*/
8081
private TypeConverter<?> resolve(Type type) {
8182
return factories.stream()
@@ -85,10 +86,8 @@ private TypeConverter<?> resolve(Type type) {
8586
}
8687

8788
/**
88-
* @param type
89-
* @param <T>
90-
* @return Return a converter that uses reflection on either a static valueOf or ctor(String) to convert a string value to the
91-
* type. Will return null if neither is found
89+
* Return a converter that uses reflection on either a static <code>valueOf</code> method or a <code>ctor(String)</code>
90+
* to convert a string value to the requested type. Will return null if neither is found
9291
*/
9392
private static <T> TypeConverter<T> findValueOfTypeConverter(Type type) {
9493
if (!(type instanceof Class)) {
@@ -98,19 +97,20 @@ private static <T> TypeConverter<T> findValueOfTypeConverter(Type type) {
9897
@SuppressWarnings("unchecked")
9998
Class<T> cls = (Class<T>) type;
10099

101-
// Next look a valueOf(String) static method
100+
// Look for a valueOf(String) static method. The code *assumes* that such a method will return a T
102101
Method method;
103102
try {
104103
method = cls.getMethod("valueOf", String.class);
105104
return value -> {
106105
try {
106+
//noinspection unchecked
107107
return (T) method.invoke(null, value);
108108
} catch (Exception e) {
109109
throw new ParseException("Error converting value '" + value + "' to '" + type.getTypeName() + "'", e);
110110
}
111111
};
112112
} catch (NoSuchMethodException e1) {
113-
// Next look for a T(String) constructor
113+
// Next, look for a T(String) constructor
114114
Constructor<T> c;
115115
try {
116116
c = cls.getConstructor(String.class);

archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ <T> T newProxy(final Class<T> type, final String initialPrefix, boolean immutabl
228228
final InvocationHandler handler = new ConfigProxyInvocationHandler<>(type, prefix, invokers, propertyNames);
229229

230230
final T proxyObject = (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, handler);
231+
List<RuntimeException> proxyingExceptions = new LinkedList<>();
231232

232233
// Iterate through all declared methods of the class looking for setter methods.
233234
// Each setter will be mapped to a Property<T> for the property name:
@@ -236,20 +237,37 @@ <T> T newProxy(final Class<T> type, final String initialPrefix, boolean immutabl
236237
if (Modifier.isStatic(method.getModifiers())) {
237238
continue;
238239
}
239-
MethodInvokerHolder methodInvokerHolder = buildInvokerForMethod(type, prefix, method, proxyObject, immutable);
240240

241-
propertyNames.put(method, methodInvokerHolder.propertyName);
241+
try {
242+
MethodInvokerHolder methodInvokerHolder = buildInvokerForMethod(type, prefix, method, proxyObject, immutable);
242243

243-
if (immutable) {
244-
// Cache the current value of the property and always return that.
245-
// Note that this will fail for parameterized properties!
246-
Object value = methodInvokerHolder.invoker.invoke(new Object[]{});
247-
invokers.put(method, (args) -> value);
248-
} else {
249-
invokers.put(method, methodInvokerHolder.invoker);
244+
propertyNames.put(method, methodInvokerHolder.propertyName);
245+
246+
if (immutable) {
247+
// Cache the current value of the property and always return that.
248+
// Note that this will fail for parameterized properties!
249+
Object value = methodInvokerHolder.invoker.invoke(new Object[]{});
250+
invokers.put(method, (args) -> value);
251+
} else {
252+
invokers.put(method, methodInvokerHolder.invoker);
253+
}
254+
} catch (RuntimeException e) {
255+
// Capture the exception and continue processing the other methods. We'll throw them all at the end.
256+
proxyingExceptions.add(e);
250257
}
251258
}
252259

260+
if (!proxyingExceptions.isEmpty()) {
261+
String errors = proxyingExceptions.stream()
262+
.map(Throwable::getMessage)
263+
.collect(Collectors.joining("\n\t"));
264+
RuntimeException exception = new RuntimeException(
265+
"Failed to create a configuration proxy for class " + type.getName()
266+
+ ":\n\t" + errors, proxyingExceptions.get(0));
267+
proxyingExceptions.subList(1, proxyingExceptions.size()).forEach(exception::addSuppressed);
268+
throw exception;
269+
}
270+
253271
return proxyObject;
254272
}
255273

@@ -310,7 +328,7 @@ private <T> MethodInvokerHolder buildInvokerForMethod(Class<T> type, String pref
310328
} else if (m.getParameterCount() > 0) {
311329
// A parameterized property. Note that this requires a @PropertyName annotation to extract the interpolation positions!
312330
if (nameAnnot == null) {
313-
throw new IllegalArgumentException("Missing @PropertyName annotation on " + m.getDeclaringClass().getName() + "#" + m.getName());
331+
throw new IllegalArgumentException("Missing @PropertyName annotation on method with parameters " + m.getName());
314332
}
315333

316334
// A previous version allowed the full name to be specified, even if the prefix was specified. So, for
@@ -322,6 +340,7 @@ private <T> MethodInvokerHolder buildInvokerForMethod(Class<T> type, String pref
322340
propertyNameTemplate = nameAnnot.name();
323341
}
324342

343+
// TODO: Figure out a way to validate the template. It should have params in the form ${0}, ${1}, etc.
325344
propertyValueGetter = createParameterizedProperty(m.getGenericReturnType(), propertyNameTemplate, defaultValueSupplier);
326345

327346
} else {
@@ -330,8 +349,8 @@ private <T> MethodInvokerHolder buildInvokerForMethod(Class<T> type, String pref
330349
}
331350

332351
return new MethodInvokerHolder(propertyValueGetter, propName);
333-
} catch (Exception e) {
334-
throw new RuntimeException("Error proxying method " + m.getName(), e);
352+
} catch (RuntimeException e) {
353+
throw new RuntimeException("Failed to create a proxy for method " + m.getName() + ": " + e, e);
335354
}
336355
}
337356

archaius2-core/src/main/java/com/netflix/archaius/DefaultPropertyFactory.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ public class DefaultPropertyFactory implements PropertyFactory, ConfigListener {
2828

2929
/**
3030
* Create a Property factory that is attached to a specific config
31-
* @param config
32-
* @return
31+
* @param config The source of configuration for this factory.
3332
*/
3433
public static DefaultPropertyFactory from(final Config config) {
3534
return new DefaultPropertyFactory(config);
@@ -64,6 +63,8 @@ public DefaultPropertyFactory(Config config) {
6463
}
6564

6665
@Override
66+
@Deprecated
67+
@SuppressWarnings("deprecation")
6768
public PropertyContainer getProperty(String propName) {
6869
return new PropertyContainer() {
6970
@Override
@@ -130,7 +131,7 @@ public <T> Property<T> asType(Function<String, T> mapper, String defaultValue) {
130131
try {
131132
return mapper.apply(value);
132133
} catch (Exception e) {
133-
LOG.warn("Invalid value '{}' for property '{}'", propName, value);
134+
LOG.error("Invalid value '{}' for property '{}'. Will return the default instead.", propName, value);
134135
}
135136
}
136137

@@ -216,7 +217,7 @@ public T get() {
216217
try {
217218
newValue = supplier.get();
218219
} catch (Exception e) {
219-
LOG.warn("Unable to get current version of property '{}'", keyAndType.key, e);
220+
LOG.error("Unable to get current version of property '{}'", keyAndType.key, e);
220221
}
221222

222223
if (cache.compareAndSet(currentValue, newValue, cacheVersion, latestVersion)) {
@@ -260,16 +261,18 @@ public synchronized void run() {
260261

261262
@Deprecated
262263
@Override
264+
@SuppressWarnings("deprecation")
263265
public void addListener(PropertyListener<T> listener) {
264266
oldSubscriptions.put(listener, onChange(listener));
265267
}
266268

267269
/**
268270
* Remove a listener previously registered by calling addListener
269-
* @param listener
271+
* @param listener The listener to be removed
270272
*/
271273
@Deprecated
272274
@Override
275+
@SuppressWarnings("deprecation")
273276
public void removeListener(PropertyListener<T> listener) {
274277
Subscription subscription = oldSubscriptions.remove(listener);
275278
if (subscription != null) {

archaius2-core/src/main/java/com/netflix/archaius/config/AbstractConfig.java

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.netflix.archaius.config;
1717

1818
import com.netflix.archaius.DefaultDecoder;
19+
import com.netflix.archaius.api.ArchaiusType;
1920
import com.netflix.archaius.api.Config;
2021
import com.netflix.archaius.api.ConfigListener;
2122
import com.netflix.archaius.api.Decoder;
@@ -28,8 +29,6 @@
2829
import java.lang.reflect.Type;
2930
import java.math.BigDecimal;
3031
import java.math.BigInteger;
31-
import java.util.ArrayList;
32-
import java.util.Arrays;
3332
import java.util.Iterator;
3433
import java.util.List;
3534
import java.util.NoSuchElementException;
@@ -62,6 +61,7 @@ public AbstractConfig() {
6261
this(generateUniqueName("unnamed-"));
6362
}
6463

64+
@SuppressWarnings("unused")
6565
protected CopyOnWriteArrayList<ConfigListener> getListeners() {
6666
return listeners;
6767
}
@@ -74,6 +74,7 @@ public String getListDelimiter() {
7474
return listDelimiter;
7575
}
7676

77+
@SuppressWarnings("unused")
7778
public void setListDelimiter(String delimiter) {
7879
listDelimiter = delimiter;
7980
}
@@ -162,13 +163,18 @@ public String getString(String key) {
162163

163164
/**
164165
* Handle notFound when a defaultValue is provided.
165-
* @param defaultValue
166-
* @return
166+
* This implementation simply returns the defaultValue. Subclasses can override this method to provide
167+
* alternative behavior.
167168
*/
168-
protected <T> T notFound(String key, T defaultValue) {
169+
protected <T> T notFound(@SuppressWarnings("unused") String key, T defaultValue) {
169170
return defaultValue;
170171
}
171-
172+
173+
/**
174+
* Handle notFound when no defaultValue is provided.
175+
* This implementation throws a NoSuchElementException. Subclasses can override this method to provide
176+
* alternative behavior.
177+
*/
172178
protected <T> T notFound(String key) {
173179
throw new NoSuchElementException("'" + key + "' not found");
174180
}
@@ -426,38 +432,35 @@ public Byte getByte(String key, Byte defaultValue) {
426432

427433
@Override
428434
public <T> List<T> getList(String key, Class<T> type) {
429-
String value = getString(key);
435+
Object value = getRawProperty(key);
430436
if (value == null) {
431437
return notFound(key);
432438
}
433-
String[] parts = value.split(getListDelimiter());
434-
List<T> result = new ArrayList<>();
435-
for (String part : parts) {
436-
result.add(decoder.decode((Type) type, part));
437-
}
438-
return result;
439+
440+
// TODO: handle the case where value is a collection
441+
return decoder.decode(ArchaiusType.forListOf(type), resolve(value.toString()));
439442
}
440443

444+
/**
445+
* @inheritDoc
446+
* This implementation always parses the underlying property as a comma-delimited string and returns
447+
* a {@code List<String>}.
448+
*/
441449
@Override
442-
@SuppressWarnings("rawtypes") // Required by legacy API
443-
public List getList(String key) {
444-
String value = getString(key);
445-
if (value == null) {
446-
return notFound(key);
447-
}
448-
String[] parts = value.split(getListDelimiter());
449-
return Arrays.asList(parts);
450+
public List<?> getList(String key) {
451+
return getList(key, String.class);
450452
}
451453

452454
@Override
453455
@SuppressWarnings("rawtypes") // Required by legacy API
454456
public List getList(String key, List defaultValue) {
455-
String value = getString(key, null);
457+
Object value = getRawProperty(key);
456458
if (value == null) {
457459
return notFound(key, defaultValue);
458460
}
459-
String[] parts = value.split(",");
460-
return Arrays.asList(parts);
461+
462+
// TODO: handle the case where value is a collection
463+
return decoder.decode(ArchaiusType.forListOf(String.class), resolve(value.toString()));
461464
}
462465

463466
@Override

archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.netflix.archaius;
22

33
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.allOf;
5+
import static org.hamcrest.Matchers.containsString;
46
import static org.hamcrest.Matchers.equalTo;
57
import static org.hamcrest.Matchers.nullValue;
68
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -652,6 +654,7 @@ public void testNestedInterfaceWithCustomDecoder() {
652654
}
653655

654656
@Configuration(prefix = "config")
657+
@SuppressWarnings("unused")
655658
public interface ConfigWithStaticMethods {
656659
@PropertyName(name = "foo")
657660
@DefaultValue("foo-value")
@@ -674,4 +677,33 @@ public void testInterfaceWithStaticMethods() {
674677
ConfigWithStaticMethods configWithStaticMethods = proxyFactory.newProxy(ConfigWithStaticMethods.class);
675678
assertEquals("foo-value-updated", configWithStaticMethods.foo());
676679
}
680+
681+
@Configuration(prefix = "config")
682+
@SuppressWarnings("unused")
683+
public interface ConfigWithBadSettings {
684+
@DefaultValue("Q") // Q is, surprisingly, not an integer
685+
int getInteger();
686+
687+
@DefaultValue("NOTINENUM") // NOTINENUM is not a valid enum element
688+
TestEnum getEnum();
689+
690+
// A parametrized method requires a @PropertyName annotation
691+
int getAnIntWithParam(String param);
692+
}
693+
694+
@Test
695+
public void testInvalidInterface() {
696+
SettableConfig config = new DefaultSettableConfig();
697+
PropertyFactory factory = DefaultPropertyFactory.from(config);
698+
ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory);
699+
700+
RuntimeException e = assertThrows(RuntimeException.class, () -> proxy.newProxy(ConfigWithBadSettings.class));
701+
702+
assertThat(e.getMessage(),
703+
allOf(
704+
containsString("ConfigWithBadSettings"),
705+
containsString("getInteger"),
706+
containsString("getEnum"),
707+
containsString("getAnIntWithParam")));
708+
}
677709
}

0 commit comments

Comments
 (0)