> allSuperClasses = ClassUtils.getAllSuperClasses(type);
+ for (Class> aClass : allSuperClasses) {
+ for (Field field : aClass.getDeclaredFields()) {
+ fieldNames.add(field.getName());
+ }
+ }
+ return fieldNames;
+ }
+
}
diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/StringUtils.java b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/StringUtils.java
index fc444750c5..d60ea13720 100644
--- a/dubbo-common/src/main/java/org/apache/dubbo/common/utils/StringUtils.java
+++ b/dubbo-common/src/main/java/org/apache/dubbo/common/utils/StringUtils.java
@@ -425,6 +425,15 @@ public final class StringUtils {
return true;
}
+ /**
+ * Check the cs String whether contains non whitespace characters.
+ * @param cs
+ * @return
+ */
+ public static boolean hasText(CharSequence cs) {
+ return !isBlank(cs);
+ }
+
/**
* is empty string.
*
@@ -894,6 +903,15 @@ public final class StringUtils {
if (isEmpty(camelName)) {
return camelName;
}
+ if (!isWord(camelName)) {
+ // convert Ab-Cd-Ef to ab-cd-ef
+ if (isSplitCase(camelName, split.charAt(0))) {
+ return camelName.toLowerCase();
+ }
+ // not camel case
+ return camelName;
+ }
+
StringBuilder buf = null;
for (int i = 0; i < camelName.length(); i++) {
char ch = camelName.charAt(i);
@@ -912,7 +930,64 @@ public final class StringUtils {
buf.append(ch);
}
}
- return buf == null ? camelName : buf.toString();
+ return buf == null ? camelName.toLowerCase() : buf.toString().toLowerCase();
+ }
+
+ private static boolean isSplitCase(String str, char separator) {
+ if (str == null) {
+ return false;
+ }
+ return str.chars().allMatch(ch -> (ch == separator) || isWord((char)ch) );
+ }
+
+ private static boolean isWord(String str) {
+ if (str == null) {
+ return false;
+ }
+ return str.chars().allMatch(ch -> isWord((char)ch));
+ }
+
+ private static boolean isWord(char ch) {
+ if ((ch >= 'A' && ch <= 'Z') ||
+ (ch >= 'a' && ch <= 'z') ||
+ (ch >= '0' && ch <= '9')) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Convert snake_case or SNAKE_CASE to kebab-case.
+ *
+ * NOTE: Return itself if it's not a snake case.
+ * @param snakeName
+ * @param split
+ * @return
+ */
+ public static String snakeToSplitName(String snakeName, String split) {
+ String lowerCase = snakeName.toLowerCase();
+ if (isSnakeCase(snakeName)) {
+ return replace(lowerCase, "_", split);
+ }
+ return snakeName;
+ }
+
+ protected static boolean isSnakeCase(String str) {
+ return str.contains("_") || str.equals(str.toLowerCase()) || str.equals(str.toUpperCase());
+ }
+
+ /**
+ * Convert camelCase or snake_case/SNAKE_CASE to kebab-case
+ * @param str
+ * @param split
+ * @return
+ */
+ public static String convertToSplitName(String str, String split) {
+ if (isSnakeCase(str)) {
+ return snakeToSplitName(str, split);
+ } else {
+ return camelToSplitName(str, split);
+ }
}
public static String toArgumentString(Object[] args) {
@@ -1051,11 +1126,14 @@ public final class StringUtils {
}
/**
+ * Decode parameters string to map
* @param rawParameters format like '[{a:b},{c:d}]'
* @return
*/
public static Map parseParameters(String rawParameters) {
-
+ if (StringUtils.isBlank(rawParameters)) {
+ return Collections.emptyMap();
+ }
Matcher matcher = PARAMETERS_PATTERN.matcher(rawParameters);
if (!matcher.matches()) {
return Collections.emptyMap();
@@ -1074,6 +1152,32 @@ public final class StringUtils {
return parameters;
}
+ /**
+ * Encode parameters map to string, like '[{a:b},{c:d}]'
+ * @param params
+ * @return
+ */
+ public static String encodeParameters(Map params) {
+ if (params == null || params.isEmpty()) {
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("[");
+ params.forEach((key,value) -> {
+ // {key:value},
+ if (hasText(value)) {
+ sb.append("{").append(key).append(":").append(value).append("},");
+ }
+ });
+ // delete last separator ','
+ if (sb.charAt(sb.length() - 1) == ',') {
+ sb.deleteCharAt(sb.length()-1);
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
public static int decodeHexNibble(final char c) {
// Character.digit() is not used here, as it addresses a larger
// set of characters (both ASCII and full-width latin letters).
@@ -1106,4 +1210,18 @@ public final class StringUtils {
String another = arrayToDelimitedString(others, COMMA_SEPARATOR);
return isEmpty(another) ? one : one + COMMA_SEPARATOR + another;
}
+
+ /**
+ * Test str whether starts with the prefix ignore case.
+ * @param str
+ * @param prefix
+ * @return
+ */
+ public static boolean startsWithIgnoreCase(String str, String prefix) {
+ if (str == null || prefix == null || str.length() < prefix.length()) {
+ return false;
+ }
+ // return str.substring(0, prefix.length()).equalsIgnoreCase(prefix);
+ return str.regionMatches(true, 0, prefix, 0, prefix.length());
+ }
}
diff --git a/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractConfig.java b/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractConfig.java
index 97ae6a180c..7d9007f539 100644
--- a/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractConfig.java
+++ b/dubbo-common/src/main/java/org/apache/dubbo/config/AbstractConfig.java
@@ -17,8 +17,9 @@
package org.apache.dubbo.config;
import org.apache.dubbo.common.URL;
-import org.apache.dubbo.common.config.CompositeConfiguration;
+import org.apache.dubbo.common.config.ConfigurationUtils;
import org.apache.dubbo.common.config.Environment;
+import org.apache.dubbo.common.config.InmemoryConfiguration;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
@@ -33,16 +34,25 @@ import org.apache.dubbo.rpc.model.ApplicationModel;
import org.apache.dubbo.rpc.model.AsyncMethodInfo;
import javax.annotation.PostConstruct;
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.MethodDescriptor;
+import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
@@ -59,31 +69,19 @@ public abstract class AbstractConfig implements Serializable {
private static final long serialVersionUID = 4267533505537413570L;
/**
- * The legacy properties container
+ * The field names cache of config class
*/
- private static final Map LEGACY_PROPERTIES = new HashMap();
+ private static final Map> fieldNamesCache = new ConcurrentHashMap<>();
/**
* The suffix container
*/
private static final String[] SUFFIXES = new String[]{"Config", "Bean", "ConfigBase"};
- static {
- LEGACY_PROPERTIES.put("dubbo.protocol.name", "dubbo.service.protocol");
- LEGACY_PROPERTIES.put("dubbo.protocol.host", "dubbo.service.server.host");
- LEGACY_PROPERTIES.put("dubbo.protocol.port", "dubbo.service.server.port");
- LEGACY_PROPERTIES.put("dubbo.protocol.threads", "dubbo.service.max.thread.pool.size");
- LEGACY_PROPERTIES.put("dubbo.consumer.timeout", "dubbo.service.invoke.timeout");
- LEGACY_PROPERTIES.put("dubbo.consumer.retries", "dubbo.service.max.retry.providers");
- LEGACY_PROPERTIES.put("dubbo.consumer.check", "dubbo.service.allow.no.provider");
- LEGACY_PROPERTIES.put("dubbo.service.url", "dubbo.service.address");
- }
-
/**
* The config id
*/
- protected String id;
- protected String prefix;
+ private String id;
protected final AtomicBoolean refreshed = new AtomicBoolean(false);
@@ -92,16 +90,6 @@ public abstract class AbstractConfig implements Serializable {
*/
protected Boolean isDefault;
- private static String convertLegacyValue(String key, String value) {
- if (value != null && value.length() > 0) {
- if ("dubbo.service.max.retry.providers".equals(key)) {
- return String.valueOf(Integer.parseInt(value) - 1);
- } else if ("dubbo.service.allow.no.provider".equals(key)) {
- return String.valueOf(!Boolean.parseBoolean(value));
- }
- }
- return value;
- }
public static String getTagName(Class> cls) {
String tag = cls.getSimpleName();
@@ -114,57 +102,25 @@ public abstract class AbstractConfig implements Serializable {
return StringUtils.camelToSplitName(tag, "-");
}
+ public static String getPluralTagName(Class> cls) {
+ String tagName = getTagName(cls);
+ if (tagName.endsWith("y")) {
+ // e.g. registry -> registries
+ return tagName.substring(0, tagName.length() - 1) + "ies";
+ } else if (tagName.endsWith("s")) {
+ // e.g. metrics -> metricses
+ return tagName + "es";
+ }
+ return tagName + "s";
+ }
+
public static void appendParameters(Map parameters, Object config) {
appendParameters(parameters, config, null);
}
@SuppressWarnings("unchecked")
public static void appendParameters(Map parameters, Object config, String prefix) {
- if (config == null) {
- return;
- }
- Method[] methods = config.getClass().getMethods();
- for (Method method : methods) {
- try {
- String name = method.getName();
- if (MethodUtils.isGetter(method)) {
- Parameter parameter = method.getAnnotation(Parameter.class);
- if (method.getReturnType() == Object.class || parameter != null && parameter.excluded()) {
- continue;
- }
- String key;
- if (parameter != null && parameter.key().length() > 0) {
- key = parameter.key();
- } else {
- key = calculatePropertyFromGetter(name);
- }
- Object value = method.invoke(config);
- String str = String.valueOf(value).trim();
- if (value != null && str.length() > 0) {
- if (parameter != null && parameter.escaped()) {
- str = URL.encode(str);
- }
- if (parameter != null && parameter.append()) {
- String pre = parameters.get(key);
- if (pre != null && pre.length() > 0) {
- str = pre + "," + str;
- }
- }
- if (prefix != null && prefix.length() > 0) {
- key = prefix + "." + key;
- }
- parameters.put(key, str);
- } else if (parameter != null && parameter.required()) {
- throw new IllegalStateException(config.getClass().getSimpleName() + "." + key + " == null");
- }
- } else if (isParametersGetter(method)) {
- Map map = (Map) method.invoke(config, new Object[0]);
- parameters.putAll(convert(map, prefix));
- }
- } catch (Exception e) {
- throw new IllegalStateException(e.getMessage(), e);
- }
- }
+ appendParameters0(parameters, config, prefix, true);
}
/**
@@ -172,41 +128,83 @@ public abstract class AbstractConfig implements Serializable {
* @param parameters
* @param config
*/
- @Deprecated
- protected static void appendAttributes(Map parameters, Object config) {
- appendAttributes(parameters, config, null);
+ public static void appendAttributes(Map parameters, Object config) {
+ appendParameters0(parameters, config, null, false);
}
- @Deprecated
- protected static void appendAttributes(Map parameters, Object config, String prefix) {
+ private static void appendParameters0(Map parameters, Object config, String prefix, boolean asParameters) {
if (config == null) {
return;
}
- Method[] methods = config.getClass().getMethods();
- for (Method method : methods) {
+ // If asParameters=false, it means as attributes, ignore @Parameter annotation except 'append' and 'attribute'
+
+ // How to select the appropriate one from multiple getter methods of the property?
+ // e.g. Using String getGeneric() or Boolean isGeneric()? Judge by field type ?
+ // Currently use @Parameter.attribute() to determine whether it is an attribute.
+
+ BeanInfo beanInfo = getBeanInfo(config.getClass());
+ for (MethodDescriptor methodDescriptor : beanInfo.getMethodDescriptors()) {
+ Method method = methodDescriptor.getMethod();
try {
- Parameter parameter = method.getAnnotation(Parameter.class);
- if (parameter == null || !parameter.attribute()) {
- continue;
- }
String name = method.getName();
if (MethodUtils.isGetter(method)) {
- String key;
- if (parameter.key().length() > 0) {
- key = parameter.key();
- } else {
- key = calculateAttributeFromGetter(name);
+ if (method.getReturnType() == Object.class ) {
+ continue;
+ }
+ String key = calculatePropertyFromGetter(name);
+ Parameter parameter = method.getAnnotation(Parameter.class);
+ if (asParameters) {
+ if (parameter != null && parameter.excluded()) {
+ continue;
+ }
+ if (parameter != null && parameter.key().length() > 0) {
+ key = parameter.key();
+ }
+ } else { // as attributes
+ // filter non attribute
+ if (parameter != null && !parameter.attribute()) {
+ continue;
+ }
}
Object value = method.invoke(config);
- if (value != null) {
+ String str = String.valueOf(value).trim();
+ if (value != null && str.length() > 0) {
+ if (asParameters && parameter != null && parameter.escaped()) {
+ str = URL.encode(str);
+ }
+ if (parameter != null && parameter.append()) {
+ String pre = parameters.get(key);
+ if (pre != null && pre.length() > 0) {
+ str = pre + "," + str;
+ //Remove duplicate values
+ Set set = StringUtils.splitToSet(str, ',');
+ str = StringUtils.join(set, ",");
+ }
+ }
if (prefix != null && prefix.length() > 0) {
key = prefix + "." + key;
}
- parameters.put(key, value);
+ parameters.put(key, str);
+ } else if (asParameters && parameter != null && parameter.required()) {
+ throw new IllegalStateException(config.getClass().getSimpleName() + "." + key + " == null");
+ }
+ } else if (isParametersGetter(method)) {
+ Map map = (Map) method.invoke(config);
+ map = convert(map, prefix);
+ if (asParameters) {
+ // put all parameters to url
+ parameters.putAll(map);
+ } else {
+ // encode parameters to string for config overriding, see AbstractConfig#refresh()
+ String key = calculatePropertyFromGetter(name);
+ String encodeParameters = StringUtils.encodeParameters(map);
+ if (encodeParameters != null) {
+ parameters.put(key, encodeParameters);
+ }
}
}
} catch (Exception e) {
- throw new IllegalStateException(e.getMessage(), e);
+ throw new IllegalStateException("Append parameters failed: " + e.getMessage(), e);
}
}
}
@@ -264,20 +262,9 @@ public abstract class AbstractConfig implements Serializable {
}).collect(Collectors.toSet());
}
- private static String extractPropertyName(Class> clazz, Method setter) throws Exception {
- String propertyName = setter.getName().substring("set".length());
- Method getter = null;
- try {
- getter = clazz.getMethod("get" + propertyName);
- } catch (NoSuchMethodException e) {
- getter = clazz.getMethod("is" + propertyName);
- }
- Parameter parameter = getter.getAnnotation(Parameter.class);
- if (parameter != null && StringUtils.isNotEmpty(parameter.key()) && parameter.useKeyAsProperty()) {
- propertyName = parameter.key();
- } else {
- propertyName = propertyName.substring(0, 1).toLowerCase() + propertyName.substring(1);
- }
+ private static String extractPropertyName(String setter) throws Exception {
+ String propertyName = setter.substring("set".length());
+ propertyName = propertyName.substring(0, 1).toLowerCase() + propertyName.substring(1);
return propertyName;
}
@@ -348,14 +335,14 @@ public abstract class AbstractConfig implements Serializable {
String value = entry.getValue();
result.put(pre + key, value);
// For compatibility, key like "registry-type" will has a duplicate key "registry.type"
- if (Arrays.binarySearch(Constants.DOT_COMPATIBLE_KEYS, key) != -1) {
+ if (Arrays.binarySearch(Constants.DOT_COMPATIBLE_KEYS, key) >= 0) {
result.put(pre + key.replace('-', '.'), value);
}
}
return result;
}
- @Parameter(excluded = true)
+ @Parameter(excluded = true, attribute = true)
public String getId() {
return id;
}
@@ -415,92 +402,130 @@ public abstract class AbstractConfig implements Serializable {
}
/**
- * Should be called after Config was fully initialized.
- * // FIXME: this method should be completely replaced by appendParameters
*
- * @return
- * @see AbstractConfig#appendParameters(Map, Object, String)
*
- * Notice! This method should include all properties in the returning map, treat @Parameter differently compared to appendParameters.
+ * The new instance of the AbstractConfig subclass should return empty metadata.
+ * The purpose is is to get the attributes set by the user instead of the default value when the {@link #refresh()} method handles attribute overrides.
+ *
+ *
+ * The default value of the field should be set in the {@link #checkDefault()} method,
+ * which will be called at the end of {@link #refresh()}, so that it will not affect the behavior of attribute overrides.
+ *
+ *
+ * Should be called after Config was fully initialized.
+ *
+ * Notice! This method should include all properties in the returning map, treat @Parameter differently compared to appendParameters?
+ *
+ * // FIXME: this method should be completely replaced by appendParameters?
+ * // -- Url parameter may use key, but props override only use property name. So replace it with appendAttributes().
+ *
+ * @see AbstractConfig#checkDefault()
+ * @see AbstractConfig#appendParameters(Map, Object, String)
*/
public Map getMetaData() {
Map metaData = new HashMap<>();
- Method[] methods = this.getClass().getMethods();
- for (Method method : methods) {
- try {
- String name = method.getName();
- if (MethodUtils.isMetaMethod(method)) {
- String key;
- Parameter parameter = method.getAnnotation(Parameter.class);
- if (parameter != null && parameter.key().length() > 0 && parameter.useKeyAsProperty()) {
- key = parameter.key();
- } else {
- key = calculateAttributeFromGetter(name);
- }
- // treat url and configuration differently, the value should always present in configuration though it may not need to present in url.
- //if (method.getReturnType() == Object.class || parameter != null && parameter.excluded()) {
- if (method.getReturnType() == Object.class) {
- metaData.put(key, null);
- continue;
- }
-
- /**
- * Attributes annotated as deprecated should not override newly added replacement.
- */
- if (MethodUtils.isDeprecated(method) && metaData.get(key) != null) {
- continue;
- }
-
- Object value = method.invoke(this);
- String str = String.valueOf(value).trim();
- if (value != null && str.length() > 0) {
- metaData.put(key, str);
- } else {
- metaData.put(key, null);
- }
- } else if (isParametersGetter(method)) {
- Map map = (Map) method.invoke(this, new Object[0]);
- metaData.putAll(convert(map, ""));
- }
- } catch (Exception e) {
- throw new IllegalStateException(e.getMessage(), e);
- }
- }
+ appendAttributes(metaData, this);
return metaData;
}
- @Parameter(excluded = true)
- public String getPrefix() {
- return StringUtils.isNotEmpty(prefix) ? prefix : (CommonConstants.DUBBO + "." + getTagName(this.getClass()));
+ protected static BeanInfo getBeanInfo(Class cls) {
+ BeanInfo beanInfo = null;
+ try {
+ beanInfo = Introspector.getBeanInfo(cls);
+ } catch (IntrospectionException e) {
+ throw new IllegalStateException(e.getMessage(), e);
+ }
+ return beanInfo;
}
- public void setPrefix(String prefix) {
- this.prefix = prefix;
+ private static boolean isWritableProperty(BeanInfo beanInfo, String key) {
+ for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
+ if (key.equals(propertyDescriptor.getName())) {
+ return propertyDescriptor.getWriteMethod() != null;
+ }
+ }
+ return false;
}
+ @Parameter(excluded = true, attribute = false)
+ public List getPrefixes() {
+ List prefixes = new ArrayList<>();
+ if (StringUtils.hasText(this.getId())) {
+ // dubbo.{tag-name}s.id
+ prefixes.add(CommonConstants.DUBBO + "." + getPluralTagName(this.getClass()) + "." + this.getId());
+ }
+
+ // check name
+ String name = ReflectUtils.getProperty(this, "getName");
+ if (StringUtils.hasText(name)) {
+ String prefix = CommonConstants.DUBBO + "." + getPluralTagName(this.getClass()) + "." + name;
+ if (!prefixes.contains(prefix)) {
+ prefixes.add(prefix);
+ }
+ }
+
+ // dubbo.{tag-name}
+ prefixes.add(getTypePrefix(this.getClass()));
+ return prefixes;
+ }
+
+ public static String getTypePrefix(Class extends AbstractConfig> cls) {
+ return CommonConstants.DUBBO + "." + getTagName(cls);
+ }
+
+ /**
+ * Dubbo config property override
+ */
public void refresh() {
refreshed.set(true);
- Environment env = ApplicationModel.getEnvironment();
try {
- CompositeConfiguration compositeConfiguration = env.getPrefixedConfiguration(this);
+ // check and init before do refresh
+ preProcessRefresh();
+
+ Environment environment = ApplicationModel.getEnvironment();
+ List