|
42 | 42 | /** |
43 | 43 | * Factory for binding a configuration interface to properties in a {@link PropertyFactory} |
44 | 44 | * 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. |
46 | 46 | * <p> |
47 | 47 | * For example, |
48 | 48 | * <pre> |
|
52 | 52 | * int getTimeout(); // maps to "foo.timeout" |
53 | 53 | * |
54 | 54 | * String getName(); // maps to "foo.name" |
| 55 | + * |
| 56 | + * @PropertyName(name="bar") |
| 57 | + * String getSomeOtherName(); // maps to "foo.bar" |
55 | 58 | * } |
56 | 59 | * } |
57 | 60 | * </pre> |
58 | 61 | * |
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 |
60 | 63 | * that the default value type is a string to allow for interpolation. Alternatively, methods can |
61 | 64 | * provide a default method implementation. Note that {@literal @}DefaultValue cannot be added to a default |
62 | 65 | * method as it would introduce ambiguity as to which mechanism wins. |
|
82 | 85 | * } |
83 | 86 | * </pre> |
84 | 87 | * |
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 |
86 | 89 | * {@literal @}Configuration annotation simply pass in a prefix in the call to newProxy. |
87 | 90 | * |
88 | 91 | * <pre> |
|
92 | 95 | * </pre> |
93 | 96 | * |
94 | 97 | * 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. |
96 | 102 | * <p> |
97 | 103 | * Note that an application should normally have just one instance of ConfigProxyFactory |
98 | 104 | * 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 |
245 | 251 |
|
246 | 252 | if (immutable) { |
247 | 253 | // 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! |
249 | 256 | Object value = methodInvokerHolder.invoker.invoke(new Object[]{}); |
250 | 257 | invokers.put(method, (args) -> value); |
251 | 258 | } else { |
@@ -296,24 +303,16 @@ private String derivePrefix(Configuration annot, String prefix) { |
296 | 303 | } |
297 | 304 |
|
298 | 305 | @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) { |
300 | 307 | try { |
301 | 308 |
|
302 | 309 | final Class<?> returnType = m.getReturnType(); |
303 | 310 | final PropertyName nameAnnot = m.getAnnotation(PropertyName.class); |
304 | 311 | final String propName = getPropertyName(prefix, m, nameAnnot); |
305 | 312 |
|
306 | 313 | // 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); |
317 | 316 |
|
318 | 317 | // This object encapsulates the way to get the value for the current property. |
319 | 318 | final PropertyValueGetter propertyValueGetter; |
@@ -354,6 +353,42 @@ private <T> MethodInvokerHolder buildInvokerForMethod(Class<T> type, String pref |
354 | 353 | } |
355 | 354 | } |
356 | 355 |
|
| 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 | + |
357 | 392 | /** |
358 | 393 | * Compute the name of the property that will be returned by this method. |
359 | 394 | */ |
@@ -394,29 +429,29 @@ private static <T> Function<Object[], T> memoize(T value) { |
394 | 429 | } |
395 | 430 |
|
396 | 431 | /** 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) { |
398 | 433 | final MethodHandle methodHandle; |
399 | 434 |
|
400 | 435 | try { |
401 | 436 | if (SystemUtils.IS_JAVA_1_8) { |
402 | 437 | Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class |
403 | 438 | .getDeclaredConstructor(Class.class, int.class); |
404 | 439 | 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) |
407 | 442 | .bindTo(proxyObject); |
408 | 443 | } |
409 | 444 | else { |
410 | 445 | // Java 9 onwards |
411 | 446 | methodHandle = MethodHandles.lookup() |
412 | | - .findSpecial(type, |
| 447 | + .findSpecial(proxyObjectType, |
413 | 448 | method.getName(), |
414 | 449 | MethodType.methodType(method.getReturnType(), method.getParameterTypes()), |
415 | | - type) |
| 450 | + proxyObjectType) |
416 | 451 | .bindTo(proxyObject); |
417 | 452 | } |
418 | 453 | } 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); |
420 | 455 | } |
421 | 456 |
|
422 | 457 | return (args) -> { |
|
0 commit comments