From 7274de5e2192d8883d34bbcaacb4db6d58a7c58c Mon Sep 17 00:00:00 2001 From: elandau Date: Thu, 9 May 2019 10:35:03 -0700 Subject: [PATCH] config: custom types with valueOf Supporting collections and custom types with dynamic config requires a great deal of custom code. This can be greatly simplified by supporting deserialization using a valueOf static method of any type. --- .../config/DefaultClientConfigImpl.java | 25 +++++++++---- .../AbstractReloadableClientConfig.java | 31 ++++++++++++++-- .../client/config/ClientConfigTest.java | 37 +++++++++++++++++++ 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/ribbon-archaius/src/main/java/com/netflix/client/config/DefaultClientConfigImpl.java b/ribbon-archaius/src/main/java/com/netflix/client/config/DefaultClientConfigImpl.java index 13186056..e59b3d1a 100644 --- a/ribbon-archaius/src/main/java/com/netflix/client/config/DefaultClientConfigImpl.java +++ b/ribbon-archaius/src/main/java/com/netflix/client/config/DefaultClientConfigImpl.java @@ -21,6 +21,8 @@ import com.netflix.config.ConfigurationManager; import org.apache.commons.configuration.event.ConfigurationEvent; import org.apache.commons.configuration.event.ConfigurationListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Arrays; import java.util.Optional; @@ -73,6 +75,7 @@ values are specified in this class as constants. * */ public class DefaultClientConfigImpl extends AbstractReloadableClientConfig { + private static final Logger LOG = LoggerFactory.getLogger(DefaultClientConfigImpl.class); @Deprecated public static final Boolean DEFAULT_PRIORITIZE_VIP_ADDRESS_BASED_SERVERS = Boolean.TRUE; @@ -616,11 +619,9 @@ public String getVersion(){ @Override protected Optional loadProperty(String key, Class type) { - if (String.class.equals(type)) { - return Optional.ofNullable(ConfigurationManager.getConfigInstance().getStringArray(key)) - .filter(ar -> ar.length > 0) - .map(ar -> (T)Arrays.stream(ar).collect(Collectors.joining(","))); - } else if (Integer.class.equals(type)) { + LOG.debug("Loading property {}", key); + + if (Integer.class.equals(type)) { return Optional.ofNullable((T) ConfigurationManager.getConfigInstance().getInteger(key, null)); } else if (Boolean.class.equals(type)) { return Optional.ofNullable((T) ConfigurationManager.getConfigInstance().getBoolean(key, null)); @@ -632,9 +633,19 @@ protected Optional loadProperty(String key, Class type) { return Optional.ofNullable((T) ConfigurationManager.getConfigInstance().getDouble(key, null)); } else if (TimeUnit.class.equals(type)) { return Optional.ofNullable((T) TimeUnit.valueOf(ConfigurationManager.getConfigInstance().getString(key, null))); + } else { + return Optional.ofNullable(ConfigurationManager.getConfigInstance().getStringArray(key)) + .filter(ar -> ar.length > 0) + .map(ar -> Arrays.stream(ar).collect(Collectors.joining(","))) + .map(value -> { + if (type.equals(String.class)) { + return (T)value; + } else { + return resolveWithValueOf(type, value) + .orElseThrow(() -> new IllegalArgumentException("Unable to convert value to desired type " + type)); + } + }); } - - throw new IllegalArgumentException("Unable to convert value to desired type " + type); } public DefaultClientConfigImpl withProperty(IClientConfigKey key, Object value) { diff --git a/ribbon-core/src/main/java/com/netflix/client/config/AbstractReloadableClientConfig.java b/ribbon-core/src/main/java/com/netflix/client/config/AbstractReloadableClientConfig.java index a52b039f..f41bac6e 100644 --- a/ribbon-core/src/main/java/com/netflix/client/config/AbstractReloadableClientConfig.java +++ b/ribbon-core/src/main/java/com/netflix/client/config/AbstractReloadableClientConfig.java @@ -6,6 +6,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -250,15 +251,38 @@ private Optional resolveFinalProperty(IClientConfigKey key) { /** * Returns the internal property to the desiredn type */ + private static Map, Optional> valueOfMethods = new ConcurrentHashMap<>(); + + public static Optional resolveWithValueOf(Class type, String value) { + return valueOfMethods.computeIfAbsent(type, ignore -> { + try { + return Optional.of(type.getDeclaredMethod("valueOf", String.class)); + } catch (NoSuchMethodException e) { + return Optional.empty(); + } catch (Exception e) { + LOG.warn("Unable to determine if type " + type + " has a valueOf() static method", e); + return Optional.empty(); + } + }).map(method -> { + try { + return (T)method.invoke(null, value); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + protected Optional resolveDefaultProperty(IClientConfigKey key) { return Optional.ofNullable(defaultProperties.get(key.key())) .map(value -> { - final Class type = key.type(); + final Class type = key.type(); // Unfortunately there's some legacy code setting string values for typed keys. Here are do our best to parse // and store the typed value if (!value.getClass().equals(type)) { try { - if (value.getClass().equals(String.class)) { + if (type.equals(String.class)) { + return (T) value.toString(); + } else if (value.getClass().equals(String.class)) { final String strValue = (String) value; if (Integer.class.equals(type)) { return (T) Integer.valueOf(strValue); @@ -273,7 +297,8 @@ protected Optional resolveDefaultProperty(IClientConfigKey key) { } else if (TimeUnit.class.equals(type)) { return (T) TimeUnit.valueOf(strValue); } else { - throw new IllegalArgumentException("Unsupported value type `" + type + "'"); + return resolveWithValueOf(type, strValue) + .orElseThrow(() -> new IllegalArgumentException("Unsupported value type `" + type + "'")); } } else { throw new IllegalArgumentException("Incompatible value type `" + value.getClass() + "` while expecting '" + type + "`"); diff --git a/ribbon-core/src/test/java/com/netflix/client/config/ClientConfigTest.java b/ribbon-core/src/test/java/com/netflix/client/config/ClientConfigTest.java index 72e591fa..c9920045 100644 --- a/ribbon-core/src/test/java/com/netflix/client/config/ClientConfigTest.java +++ b/ribbon-core/src/test/java/com/netflix/client/config/ClientConfigTest.java @@ -154,5 +154,42 @@ public void testFallback_primarySet() { Assert.assertEquals(200, prop.get().intValue()); } + + static class CustomValueOf { + private final String value; + + public static CustomValueOf valueOf(String value) { + return new CustomValueOf(value); + } + + public CustomValueOf(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + public static IClientConfigKey CUSTOM_KEY = new CommonClientConfigKey("CustomValueOf", new CustomValueOf("default")) {}; + + @Test + public void testValueOfWithDefault() { + DefaultClientConfigImpl clientConfig = new DefaultClientConfigImpl(); + + CustomValueOf prop = clientConfig.getOrDefault(CUSTOM_KEY); + Assert.assertEquals("default", prop.getValue()); + } + + @Test + public void testValueOf() { + ConfigurationManager.getConfigInstance().setProperty("testValueOf.ribbon.CustomValueOf", "value"); + + DefaultClientConfigImpl clientConfig = new DefaultClientConfigImpl(); + clientConfig.setClientName("testValueOf"); + + Property prop = clientConfig.getDynamicProperty(CUSTOM_KEY); + Assert.assertEquals("value", prop.get().getValue()); + } }