Skip to content

Commit 70da5b1

Browse files
Merge pull request #738 from Netflix/rollback-exception-propagation
Go back to the old behavior of returning null on ParseExceptions
2 parents 3dd372c + b0eb502 commit 70da5b1

File tree

5 files changed

+177
-116
lines changed

5 files changed

+177
-116
lines changed

archaius2-api/src/main/java/com/netflix/archaius/api/annotations/Configuration.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,46 +22,55 @@
2222
import java.lang.annotation.Target;
2323

2424
/**
25-
* Marks a field as a configuration item. Governator will auto-assign the value based
26-
* on the {@link #value()} of the annotation via the set {@link ConfigurationProvider}.
25+
* When applied to an interface, marks it as a candidate for binding via a ConfigProxyFactory (from the archaius2-core
26+
* module). For this case, the only relevant attributes are {@link #prefix()}, which sets a shared prefix for all the
27+
* properties bound to the interface's methods, and {@link #immutable()}, which when set to true creates a static proxy
28+
* that always returns the config values as they were at the moment that the proxy object is created.
29+
* <p>
30+
* Note that an interface can be bound via the ConfigProxyFactory even if it does NOT have this annotation.
31+
* <p>
32+
* When applied to a field, marks it as a configuration item, to be injected with the value of the specified property
33+
* key. This usage is deprecated in favor of using your DI-framework options for injecting configuration values.
34+
* @see PropertyName
35+
* @see DefaultValue
2736
*/
2837
@Documented
2938
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
3039
@Target({ElementType.TYPE})
3140
public @interface Configuration
3241
{
3342
/**
34-
* @return name/key of the config to assign
43+
* name/key of the config to assign
3544
*/
3645
String prefix() default "";
3746

3847
/**
39-
* @return field names to use for replacement
48+
* field names to use for replacement
4049
*/
4150
String[] params() default {};
4251

4352
/**
44-
* @return user displayable description of this configuration
53+
* user displayable description of this configuration
4554
*/
4655
String documentation() default "";
4756

4857
/**
49-
* @return true to allow mapping configuration to fields
58+
* true to allow mapping configuration to fields
5059
*/
5160
boolean allowFields() default false;
5261

5362
/**
54-
* @return true to allow mapping configuration to setters
63+
* true to allow mapping configuration to setters
5564
*/
5665
boolean allowSetters() default true;
5766

5867
/**
59-
* @return Method to call after configuration is bound
68+
* Method to call after configuration is bound
6069
*/
6170
String postConfigure() default "";
6271

6372
/**
64-
* @return If true then properties cannot change once set otherwise methods will be
73+
* If true then properties cannot change once set otherwise methods will be
6574
* bound to dynamic properties via PropertyFactory.
6675
*/
6776
boolean immutable() default false;

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

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
/**
4343
* Factory for binding a configuration interface to properties in a {@link PropertyFactory}
4444
* instance. Getter methods on the interface are mapped by naming convention
45-
* by the property name may be overridden using the @PropertyName annotation.
45+
* by the property name or may be overridden using the @{@link PropertyName} annotation.
4646
* <p>
4747
* For example,
4848
* <pre>
@@ -52,11 +52,14 @@
5252
* int getTimeout(); // maps to "foo.timeout"
5353
*
5454
* String getName(); // maps to "foo.name"
55+
*
56+
* @PropertyName(name="bar")
57+
* String getSomeOtherName(); // maps to "foo.bar"
5558
* }
5659
* }
5760
* </pre>
5861
*
59-
* Default values may be set by adding a {@literal @}DefaultValue with a default value string. Note
62+
* Default values may be set by adding a {@literal @}{@link DefaultValue} with a default value string. Note
6063
* that the default value type is a string to allow for interpolation. Alternatively, methods can
6164
* provide a default method implementation. Note that {@literal @}DefaultValue cannot be added to a default
6265
* method as it would introduce ambiguity as to which mechanism wins.
@@ -82,7 +85,7 @@
8285
* }
8386
* </pre>
8487
*
85-
* To override the prefix in {@literal @}Configuration or provide a prefix when there is no
88+
* To override the prefix in {@literal @}{@link Configuration} or provide a prefix when there is no
8689
* {@literal @}Configuration annotation simply pass in a prefix in the call to newProxy.
8790
*
8891
* <pre>
@@ -92,7 +95,10 @@
9295
* </pre>
9396
*
9497
* By default, all properties are dynamic and can therefore change from call to call. To make the
95-
* configuration static set the immutable attributes of @Configuration to true.
98+
* configuration static set {@link Configuration#immutable()} to true. Creation of an immutable configuration
99+
* will fail if the interface contains parametrized methods or methods that return primitive types and do not have a
100+
* value set at the moment of creation, from either the underlying config, a {@link DefaultValue} annotation, or a
101+
* default method implementation.
96102
* <p>
97103
* Note that an application should normally have just one instance of ConfigProxyFactory
98104
* and PropertyFactory since PropertyFactory caches {@link com.netflix.archaius.api.Property} objects.
@@ -245,7 +251,8 @@ <T> T newProxy(final Class<T> type, final String initialPrefix, boolean immutabl
245251

246252
if (immutable) {
247253
// Cache the current value of the property and always return that.
248-
// Note that this will fail for parameterized properties!
254+
// Note that this will fail for parameterized properties and for primitive-valued methods
255+
// with no value set!
249256
Object value = methodInvokerHolder.invoker.invoke(new Object[]{});
250257
invokers.put(method, (args) -> value);
251258
} else {
@@ -296,24 +303,16 @@ private String derivePrefix(Configuration annot, String prefix) {
296303
}
297304

298305
@SuppressWarnings({"unchecked", "rawtypes"})
299-
private <T> MethodInvokerHolder buildInvokerForMethod(Class<T> type, String prefix, Method m, T proxyObject, boolean immutable) {
306+
private <T> MethodInvokerHolder buildInvokerForMethod(Class<T> proxyObjectType, String prefix, Method m, T proxyObject, boolean immutable) {
300307
try {
301308

302309
final Class<?> returnType = m.getReturnType();
303310
final PropertyName nameAnnot = m.getAnnotation(PropertyName.class);
304311
final String propName = getPropertyName(prefix, m, nameAnnot);
305312

306313
// A supplier for the value to be returned when the method's associated property is not set
307-
final Function defaultValueSupplier;
308-
309-
if (m.getAnnotation(DefaultValue.class) != null) {
310-
defaultValueSupplier = createAnnotatedMethodSupplier(m, m.getGenericReturnType(), config, decoder);
311-
} else if (m.isDefault()) {
312-
defaultValueSupplier = createDefaultMethodSupplier(m, type, proxyObject);
313-
} else {
314-
// No default specified in proxied interface. Return "empty" for collection types, null for any other type.
315-
defaultValueSupplier = knownCollections.getOrDefault(returnType, (ignored) -> null);
316-
}
314+
// The proper parametrized type for this would be Function<Object[], returnType>, but we can't say that in Java.
315+
final Function<Object[], ?> defaultValueSupplier = defaultValueSupplierForMethod(proxyObjectType, m, returnType, proxyObject, propName);
317316

318317
// This object encapsulates the way to get the value for the current property.
319318
final PropertyValueGetter propertyValueGetter;
@@ -354,6 +353,42 @@ private <T> MethodInvokerHolder buildInvokerForMethod(Class<T> type, String pref
354353
}
355354
}
356355

356+
/**
357+
* Build a supplier for the default value to be returned when the underlying property for a method is not set.
358+
* Because of the way {@link Property} works, this will ALSO be called if the underlying property is set to null
359+
* OR if it's set to a "bad" value that can't be decoded to the method's return type.
360+
**/
361+
private <PT> Function<Object[], ?> defaultValueSupplierForMethod(Class<PT> proxyObjectType, Method m, Type returnType, PT proxyObject, String propName) {
362+
if (m.getAnnotation(DefaultValue.class) != null) {
363+
// The method has a @DefaultValue annotation. Decode the string from there and return that.
364+
return createAnnotatedMethodSupplier(m, m.getGenericReturnType(), config, decoder);
365+
}
366+
367+
if (m.isDefault()) {
368+
// The method has a default implementation in the interface. Obtain the default value by calling that implementation.
369+
return createDefaultMethodSupplier(m, proxyObjectType, proxyObject);
370+
}
371+
372+
// No default value available.
373+
// For collections, return an empty
374+
if (knownCollections.containsKey(returnType)) {
375+
return knownCollections.get(returnType);
376+
}
377+
378+
// For primitive return types, our historical behavior of returning a null causes an NPE with no message and an
379+
// obscure trace. Instead of that we now use a fake supplier that will still throw the NPE, but adds a message to it.
380+
if (returnType instanceof Class && ((Class<?>) returnType).isPrimitive()) {
381+
return (ignored) -> {
382+
String msg = String.format("Property '%s' is not set or has an invalid value and method %s.%s does not define a default value",
383+
propName, proxyObjectType.getName(), m.getName());
384+
throw new NullPointerException(msg);
385+
};
386+
}
387+
388+
// For any other return type return nulls.
389+
return (ignored) -> null;
390+
}
391+
357392
/**
358393
* Compute the name of the property that will be returned by this method.
359394
*/
@@ -394,29 +429,29 @@ private static <T> Function<Object[], T> memoize(T value) {
394429
}
395430

396431
/** A supplier that calls a default method in the proxied interface and returns its output */
397-
private static <T> Function<Object[], T> createDefaultMethodSupplier(Method method, Class<T> type, T proxyObject) {
432+
private static <T> Function<Object[], T> createDefaultMethodSupplier(Method method, Class<T> proxyObjectType, T proxyObject) {
398433
final MethodHandle methodHandle;
399434

400435
try {
401436
if (SystemUtils.IS_JAVA_1_8) {
402437
Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
403438
.getDeclaredConstructor(Class.class, int.class);
404439
constructor.setAccessible(true);
405-
methodHandle = constructor.newInstance(type, MethodHandles.Lookup.PRIVATE)
406-
.unreflectSpecial(method, type)
440+
methodHandle = constructor.newInstance(proxyObjectType, MethodHandles.Lookup.PRIVATE)
441+
.unreflectSpecial(method, proxyObjectType)
407442
.bindTo(proxyObject);
408443
}
409444
else {
410445
// Java 9 onwards
411446
methodHandle = MethodHandles.lookup()
412-
.findSpecial(type,
447+
.findSpecial(proxyObjectType,
413448
method.getName(),
414449
MethodType.methodType(method.getReturnType(), method.getParameterTypes()),
415-
type)
450+
proxyObjectType)
416451
.bindTo(proxyObject);
417452
}
418453
} catch (ReflectiveOperationException e) {
419-
throw new RuntimeException("Failed to create temporary object for " + type.getName(), e);
454+
throw new RuntimeException("Failed to create temporary object for " + proxyObjectType.getName(), e);
420455
}
421456

422457
return (args) -> {

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

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -186,31 +186,33 @@ public T get() {
186186
// The tricky edge case is if another update came in between the check above to get the version and
187187
// the call to the supplier. In that case we'll tag the updated value with an old version number. That's fine,
188188
// since the next call to get() will see the old version and try again.
189+
CachedValue<T> newValue;
189190
try {
190191
// Get the new value from the supplier. This call could fail.
191-
CachedValue<T> newValue = new CachedValue<>(supplier.get(), currentMasterVersion);
192-
193-
/*
194-
* We successfully got the new value, so now we update the cache. We use an atomic CAS operation to guard
195-
* from edge cases where another thread could have updated to a higher version than we have, in a flow like this:
196-
* Assume currentVersion started at 1., property cache is set to 1 too.
197-
* 1. Upstream update bumps version to 2.
198-
* 2. Thread A reads currentVersion at 2, cachedValue at 1, proceeds to start update, gets interrupted and yields the cpu.
199-
* 3. Thread C bumps version to 3, yields the cpu.
200-
* 4. Thread B is scheduled, reads currentVersion at 3, cachedValue still at 1, proceeds to start update.
201-
* 5. Thread B keeps running, updates cache to 3, yields.
202-
* 6. Thread A resumes, tries to write cache with version 2.
203-
*/
204-
CACHED_VALUE_UPDATER.compareAndSet(this, currentCachedValue, newValue);
205-
206-
return newValue.value;
192+
newValue = new CachedValue<>(supplier.get(), currentMasterVersion);
207193

208194
} catch (RuntimeException e) {
209-
// Oh, no, something went wrong while trying to get the new value. Log the error and rethrow the exception
210-
// so our caller knows there's a problem. We leave the cache unchanged. Next caller will try again.
211-
LOG.error("Unable to get current version of property '{}'", keyAndType.key, e);
212-
throw e;
195+
// Oh, no, something went wrong while trying to get the new value. Log the error and return null.
196+
// Upstream users may return that null unchanged or substitute it by a defaultValue.
197+
// We leave the cache unchanged, which means the next caller will try again.
198+
LOG.error("Unable to update value for property '{}'", keyAndType.key, e);
199+
return null;
213200
}
201+
202+
/*
203+
* We successfully got the new value, so now we update the cache. We use an atomic CAS operation to guard
204+
* from edge cases where another thread could have updated to a higher version than we have, in a flow like this:
205+
* Assume currentVersion started at 1., property cache is set to 1 too.
206+
* 1. Upstream update bumps version to 2.
207+
* 2. Thread A reads currentVersion at 2, cachedValue at 1, proceeds to start update, gets interrupted and yields the cpu.
208+
* 3. Thread C bumps version to 3, yields the cpu.
209+
* 4. Thread B is scheduled, reads currentVersion at 3, cachedValue still at 1, proceeds to start update.
210+
* 5. Thread B keeps running, updates cache to 3, yields.
211+
* 6. Thread A resumes, tries to write cache with version 2.
212+
*/
213+
CACHED_VALUE_UPDATER.compareAndSet(this, currentCachedValue, newValue);
214+
215+
return newValue.value;
214216
}
215217

216218
@Override
@@ -280,28 +282,28 @@ public synchronized void removeListener(PropertyListener<T> listener) {
280282
@Override
281283
public Property<T> orElse(T defaultValue) {
282284
return new PropertyImpl<>(keyAndType, () -> {
283-
T value = supplier.get();
285+
T value = this.get(); // Value from the "parent" property
284286
return value != null ? value : defaultValue;
285287
});
286288
}
287289

288290
@Override
289291
public Property<T> orElseGet(String key) {
290292
if (!keyAndType.hasType()) {
291-
throw new IllegalStateException("Type information lost due to map() operation. All calls to orElse[Get] must be made prior to calling map");
293+
throw new IllegalStateException("Type information lost due to map() operation. All calls to orElseGet() must be made prior to calling map");
292294
}
293295
KeyAndType<T> keyAndType = this.keyAndType.withKey(key);
294296
Property<T> next = DefaultPropertyFactory.this.get(key, keyAndType.type);
295297
return new PropertyImpl<>(keyAndType, () -> {
296-
T value = supplier.get();
298+
T value = this.get(); // Value from the "parent" property
297299
return value != null ? value : next.get();
298300
});
299301
}
300302

301303
@Override
302304
public <S> Property<S> map(Function<T, S> mapper) {
303305
return new PropertyImpl<>(keyAndType.discardType(), () -> {
304-
T value = supplier.get();
306+
T value = this.get(); // Value from the "parent" property
305307
if (value != null) {
306308
return mapper.apply(value);
307309
} else {
@@ -464,12 +466,14 @@ public <T> Property<T> asType(Class<T> type, T defaultValue) {
464466
public <T> Property<T> asType(Function<String, T> mapper, String defaultValue) {
465467
T typedDefaultValue = applyOrThrow(mapper, defaultValue);
466468
return getFromSupplier(propName, null, () -> {
467-
String value = config.getString(propName, null);
468-
if (value != null) {
469-
return applyOrThrow(mapper, value);
470-
}
469+
String stringValue = config.getString(propName, null);
471470

472-
return typedDefaultValue;
471+
try {
472+
return stringValue != null ? applyOrThrow(mapper, stringValue) : typedDefaultValue;
473+
} catch (ParseException pe) {
474+
LOG.error("Error parsing value '{}' for property '{}'", stringValue, propName, pe);
475+
return typedDefaultValue;
476+
}
473477
});
474478
}
475479

0 commit comments

Comments
 (0)