blob: 9996c919c3e37270a1e0e854339d7f4b37b0d6ae [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.dubbo.config;
import org.apache.dubbo.common.URL;
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.extension.ExtensionLoader;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.ClassUtils;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.apache.dubbo.common.utils.FieldUtils;
import org.apache.dubbo.common.utils.MethodUtils;
import org.apache.dubbo.common.utils.ReflectUtils;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.config.context.ConfigManager;
import org.apache.dubbo.config.context.ConfigMode;
import org.apache.dubbo.config.support.Nested;
import org.apache.dubbo.config.support.Parameter;
import org.apache.dubbo.rpc.model.ApplicationModel;
import org.apache.dubbo.rpc.model.ModuleModel;
import org.apache.dubbo.rpc.model.ScopeModel;
import org.apache.dubbo.rpc.model.ScopeModelUtil;
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.Field;
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.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 static org.apache.dubbo.common.utils.ClassUtils.isSimpleType;
import static org.apache.dubbo.common.utils.ReflectUtils.findMethodByMethodSignature;
import static org.apache.dubbo.config.Constants.PARAMETERS;
/**
* Utility methods and public methods for parsing configuration
*
* @export
*/
public abstract class AbstractConfig implements Serializable {
protected static final Logger logger = LoggerFactory.getLogger(AbstractConfig.class);
private static final long serialVersionUID = 4267533505537413570L;
/**
* tag name cache, speed up get tag name frequently
*/
private static final Map<Class, String> tagNameCache = new ConcurrentHashMap<>();
/**
* attributed getter method cache for equals(), hashCode() and toString()
*/
private static final Map<Class, List<Method>> attributedMethodCache = new ConcurrentHashMap<>();
/**
* The suffix container
*/
private static final String[] SUFFIXES = new String[]{"Config", "Bean", "ConfigBase"};
/**
* The config id
*/
private String id;
protected final AtomicBoolean refreshed = new AtomicBoolean(false);
/**
* Is default config or not
*/
protected Boolean isDefault;
/**
* The scope model of this config instance.
* <p>
* <b>NOTE:</b> the model maybe changed during config processing,
* the extension spi instance needs to be reinitialized after changing the model!
*/
protected ScopeModel scopeModel;
public AbstractConfig() {
this(ApplicationModel.defaultModel());
}
public AbstractConfig(ScopeModel scopeModel) {
this.setScopeModel(scopeModel);
}
public static String getTagName(Class<?> cls) {
return tagNameCache.computeIfAbsent(cls, (key) -> {
String tag = cls.getSimpleName();
for (String suffix : SUFFIXES) {
if (tag.endsWith(suffix)) {
tag = tag.substring(0, tag.length() - suffix.length());
break;
}
}
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<String, String> parameters, Object config) {
appendParameters(parameters, config, null);
}
@SuppressWarnings("unchecked")
public static void appendParameters(Map<String, String> parameters, Object config, String prefix) {
appendParameters0(parameters, config, prefix, true);
}
/**
* Put attributes of specify 'config' into 'parameters' argument
*
* @param parameters
* @param config
*/
public static void appendAttributes(Map<String, String> parameters, Object config) {
appendParameters0(parameters, config, null, false);
}
public static void appendAttributes(Map<String, String> parameters, Object config, String prefix) {
appendParameters0(parameters, config, prefix, false);
}
private static void appendParameters0(Map<String, String> parameters, Object config, String prefix, boolean asParameters) {
if (config == null) {
return;
}
// If asParameters=false, it means append attributes, ignore @Parameter annotation's attributes 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 {
String name = method.getName();
if (MethodUtils.isGetter(method)) {
if (method.getReturnType() == Object.class) {
continue;
}
String key;
Parameter parameter = method.getAnnotation(Parameter.class);
if (asParameters) {
if (parameter != null && parameter.excluded()) {
continue;
}
// get parameter key
if (parameter != null && parameter.key().length() > 0) {
key = parameter.key();
} else {
key = calculatePropertyFromGetter(name);
}
} else { // as attributes
// filter non attribute
if (parameter != null && !parameter.attribute()) {
continue;
}
// get attribute name
String propertyName = calculateAttributeFromGetter(name);
// convert camelCase/snake_case to kebab-case
key = StringUtils.convertToSplitName(propertyName, "-");
}
Object value = method.invoke(config);
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<String> set = StringUtils.splitToSet(str, ',');
str = StringUtils.join(set, ",");
}
}
if (prefix != null && prefix.length() > 0) {
key = prefix + "." + key;
}
parameters.put(key, str);
} else if (asParameters && parameter != null && parameter.required()) {
throw new IllegalStateException(config.getClass().getSimpleName() + "." + key + " == null");
}
} else if (isParametersGetter(method)) {
Map<String, String> map = (Map<String, String>) 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);
}
}
} else if (isNestedGetter(config, method)) {
Object inner = method.invoke(config);
String fieldName = MethodUtils.extractFieldName(method);
String nestedPrefix = prefix == null ? fieldName : prefix + "." + fieldName;
appendParameters0(parameters, inner, nestedPrefix, asParameters);
}
} catch (Exception e) {
throw new IllegalStateException("Append parameters failed: " + e.getMessage(), e);
}
}
}
protected static String extractPropertyName(String setter) {
String propertyName = setter.substring("set".length());
propertyName = propertyName.substring(0, 1).toLowerCase() + propertyName.substring(1);
return propertyName;
}
private static String calculatePropertyToGetter(String name) {
return "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
}
private static String calculatePropertyToSetter(String name) {
return "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
}
private static String calculatePropertyFromGetter(String name) {
int i = name.startsWith("get") ? 3 : 2;
return StringUtils.camelToSplitName(name.substring(i, i + 1).toLowerCase() + name.substring(i + 1), ".");
}
private static String calculateAttributeFromGetter(String getter) {
int i = getter.startsWith("get") ? 3 : 2;
return getter.substring(i, i + 1).toLowerCase() + getter.substring(i + 1);
}
private static void invokeSetParameters(Class c, Object o, Map map) {
try {
Method method = findMethodByMethodSignature(c, "setParameters", new String[]{Map.class.getName()});
if (method != null && isParametersSetter(method)) {
method.invoke(o, map);
}
} catch (Throwable t) {
// ignore
}
}
private static Map<String, String> invokeGetParameters(Class c, Object o) {
try {
Method method = findMethodByMethodSignature(c, "getParameters", null);
if (method != null && isParametersGetter(method)) {
return (Map<String, String>) method.invoke(o);
}
} catch (Throwable t) {
// ignore
}
return null;
}
private static boolean isParametersGetter(Method method) {
String name = method.getName();
return ("getParameters".equals(name)
&& Modifier.isPublic(method.getModifiers())
&& method.getParameterTypes().length == 0
&& method.getReturnType() == Map.class);
}
private static boolean isParametersSetter(Method method) {
return ("setParameters".equals(method.getName())
&& Modifier.isPublic(method.getModifiers())
&& method.getParameterCount() == 1
&& Map.class == method.getParameterTypes()[0]
&& method.getReturnType() == void.class);
}
private static boolean isNestedGetter(Object obj, Method method) {
String name = method.getName();
boolean isGetter = (name.startsWith("get") || name.startsWith("is"))
&& !"get".equals(name) && !"is".equals(name)
&& !"getClass".equals(name) && !"getObject".equals(name)
&& Modifier.isPublic(method.getModifiers())
&& method.getParameterTypes().length == 0
&& (!method.getReturnType().isPrimitive() && !isSimpleType(method.getReturnType()));
if (!isGetter) {
return false;
} else {
// Extract fieldName only when necessary.
String fieldName = MethodUtils.extractFieldName(method);
Field field = FieldUtils.getDeclaredField(obj.getClass(), fieldName);
return field != null && field.isAnnotationPresent(Nested.class);
}
}
private static boolean isNestedSetter(Object obj, Method method) {
boolean isSetter = method.getName().startsWith("set")
&& !"set".equals(method.getName())
&& Modifier.isPublic(method.getModifiers())
&& method.getParameterCount() == 1
&& method.getParameterTypes()[0] != null
&& (!method.getParameterTypes()[0].isPrimitive() && !isSimpleType(method.getParameterTypes()[0]));
if (!isSetter) {
return false;
} else {
// Extract fieldName only when necessary.
String fieldName = MethodUtils.extractFieldName(method);
Field field = FieldUtils.getDeclaredField(obj.getClass(), fieldName);
return field != null && field.isAnnotationPresent(Nested.class);
}
}
/**
* @param parameters the raw parameters
* @param prefix the prefix
* @return the parameters whose raw key will replace "-" to "."
* @revised 2.7.8 "private" to be "protected"
*/
protected static Map<String, String> convert(Map<String, String> parameters, String prefix) {
if (parameters == null || parameters.isEmpty()) {
return new HashMap<>();
}
Map<String, String> result = new HashMap<>();
String pre = (StringUtils.isNotEmpty(prefix) ? prefix + "." : "");
for (Map.Entry<String, String> entry : parameters.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
result.put(pre + key, value);
// For compatibility, key like "registry-type" will have a duplicate key "registry.type"
if (Arrays.binarySearch(Constants.DOT_COMPATIBLE_KEYS, key) >= 0) {
result.put(pre + key.replace('-', '.'), value);
}
}
return result;
}
public ApplicationModel getApplicationModel() {
if (scopeModel instanceof ApplicationModel) {
return (ApplicationModel) scopeModel;
} else if (scopeModel instanceof ModuleModel) {
return ((ModuleModel) scopeModel).getApplicationModel();
} else {
throw new IllegalStateException("scope model is invalid: " + scopeModel);
}
}
public ScopeModel getScopeModel() {
return scopeModel;
}
public final void setScopeModel(ScopeModel scopeModel) {
if (this.scopeModel != scopeModel) {
checkScopeModel(scopeModel);
ScopeModel oldScopeModel = this.scopeModel;
this.scopeModel = scopeModel;
// reinitialize spi extension and change referenced config's scope model
this.postProcessAfterScopeModelChanged(oldScopeModel, this.scopeModel);
}
}
protected void checkScopeModel(ScopeModel scopeModel) {
if (scopeModel == null) {
throw new IllegalArgumentException("scopeModel cannot be null");
}
if (!(scopeModel instanceof ApplicationModel)) {
throw new IllegalArgumentException("Invalid scope model, expect to be a ApplicationModel but got: " + scopeModel);
}
}
/**
* Subclass should override this method to initialize its SPI extensions and change referenced config's scope model.
* <p>
* For example:
* <pre>
* protected void postProcessAfterScopeModelChanged() {
* super.postProcessAfterScopeModelChanged();
* // re-initialize spi extension
* this.protocol = this.getExtensionLoader(Protocol.class).getAdaptiveExtension();
* // change referenced config's scope model
* if (this.providerConfig != null && this.providerConfig.getScopeModel() != scopeModel) {
* this.providerConfig.setScopeModel(scopeModel);
* }
* }
* </pre>
*
* @param oldScopeModel
* @param newScopeModel
*/
protected void postProcessAfterScopeModelChanged(ScopeModel oldScopeModel, ScopeModel newScopeModel) {
// remove this config from old ConfigManager
// if (oldScopeModel != null && oldScopeModel instanceof ApplicationModel) {
// ((ApplicationModel)oldScopeModel).getApplicationConfigManager().removeConfig(this);
// }
}
protected <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (scopeModel == null) {
throw new IllegalStateException("scopeModel is not initialized");
}
return scopeModel.getExtensionLoader(type);
}
@Parameter(excluded = true)
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
/**
* Copy attributes from annotation
*
* @param annotationClass
* @param annotation
*/
protected void appendAnnotation(Class<?> annotationClass, Object annotation) {
Method[] methods = annotationClass.getMethods();
for (Method method : methods) {
if (method.getDeclaringClass() != Object.class
&& method.getDeclaringClass() != Annotation.class
&& method.getReturnType() != void.class
&& method.getParameterTypes().length == 0
&& Modifier.isPublic(method.getModifiers())
&& !Modifier.isStatic(method.getModifiers())) {
try {
String property = method.getName();
if ("interfaceClass".equals(property) || "interfaceName".equals(property)) {
property = "interface";
}
String setter = calculatePropertyToSetter(property);
Object value = method.invoke(annotation);
if (value != null && !value.equals(method.getDefaultValue())) {
Class<?> parameterType = ReflectUtils.getBoxedClass(method.getReturnType());
if ("filter".equals(property) || "listener".equals(property)) {
parameterType = String.class;
value = StringUtils.join((String[]) value, ",");
} else if ("parameters".equals(property)) {
parameterType = Map.class;
value = CollectionUtils.toStringMap((String[]) value);
}
try {
Method setterMethod = getClass().getMethod(setter, parameterType);
setterMethod.invoke(this, value);
} catch (NoSuchMethodException e) {
// ignore
}
}
} catch (Throwable e) {
logger.error(e.getMessage(), e);
}
}
}
}
/**
* <p>
* <b>The new instance of the AbstractConfig subclass should return empty metadata.</b>
* The purpose is to get the attributes set by the user instead of the default value when the {@link #refresh()} method handles attribute overrides.
* </p>
*
* <p><b>The default value of the field should be set in the {@link #checkDefault()} method</b>,
* which will be called at the end of {@link #refresh()}, so that it will not affect the behavior of attribute overrides.</p>
*
* <p></p>
* Should be called after Config was fully initialized.
* <p>
* Notice! This method should include all properties in the returning map, treat @Parameter differently compared to appendParameters?
* </p>
* // 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<String, String> getMetaData() {
return getMetaData(null);
}
public Map<String, String> getMetaData(String prefix) {
Map<String, String> metaData = new HashMap<>();
appendAttributes(metaData, this, prefix);
return metaData;
}
private static BeanInfo getBeanInfo(Class cls) {
BeanInfo beanInfo;
try {
beanInfo = Introspector.getBeanInfo(cls);
} catch (IntrospectionException e) {
throw new IllegalStateException(e.getMessage(), e);
}
return beanInfo;
}
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<String> getPrefixes() {
List<String> 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)) {
// dubbo.{tag-name}s.{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);
}
public ConfigMode getConfigMode() {
return getApplicationModel().getApplicationConfigManager().getConfigMode();
}
public void overrideWithConfig(AbstractConfig newOne, boolean overrideAll) {
if (!Objects.equals(this.getClass(), newOne.getClass())) {
// ignore if two config is not the same class
return;
}
List<Method> methods = MethodUtils.getMethods(this.getClass(), method -> method.getDeclaringClass() != Object.class);
for (Method method : methods) {
try {
Method getterMethod;
try {
String propertyName = extractPropertyName(method.getName());
String getterName = calculatePropertyToGetter(propertyName);
getterMethod = this.getClass().getDeclaredMethod(getterName);
} catch (Exception ignore) {
continue;
}
if (MethodUtils.isSetter(method)) {
Object oldOne = getterMethod.invoke(this);
// if old one is null or need to override
if (overrideAll || oldOne == null) {
Object newResult = getterMethod.invoke(newOne);
// if new one is non-null and new one is not equals old one
if (newResult != null && !Objects.equals(newResult, oldOne)) {
method.invoke(this, newResult);
}
}
} else if (isParametersSetter(method)) {
Object oldOne = getterMethod.invoke(this);
Object newResult = getterMethod.invoke(newOne);
Map<String, String> oldMap = null;
if (oldOne instanceof Map) {
oldMap = (Map) oldOne;
}
Map<String, String> newMap = null;
if (newResult instanceof Map) {
newMap = (Map) newResult;
}
// if new map is null, skip
if (newMap == null) {
continue;
}
// if old map is null, override with new map
if (oldMap == null) {
invokeSetParameters(newMap, this);
continue;
}
// if mode is OVERRIDE_IF_ABSENT, put all old map entries to new map, will override the same key
// if mode is OVERRIDE_ALL, put all keyed entries not in new map from old map to new map (ignore the same key appeared in old map)
if (overrideAll) {
oldMap.forEach(newMap::putIfAbsent);
} else {
newMap.putAll(oldMap);
}
invokeSetParameters(newMap, this);
} else if (isNestedSetter(this, method)) {
// not support
}
} catch (Throwable t) {
logger.error("Failed to override field value of config bean: " + this, t);
throw new IllegalStateException("Failed to override field value of config bean: " + this, t);
}
}
}
/**
* Dubbo config property override
*/
public void refresh() {
try {
// check and init before do refresh
preProcessRefresh();
Environment environment = getScopeModel().getModelEnvironment();
List<Map<String, String>> configurationMaps = environment.getConfigurationMaps();
// Search props starts with PREFIX in order
String preferredPrefix = null;
List<String> prefixes = getPrefixes();
for (String prefix : prefixes) {
if (ConfigurationUtils.hasSubProperties(configurationMaps, prefix)) {
preferredPrefix = prefix;
break;
}
}
if (preferredPrefix == null) {
preferredPrefix = prefixes.get(0);
}
// Extract sub props (which key was starts with preferredPrefix)
Collection<Map<String, String>> instanceConfigMaps = environment.getConfigurationMaps(this, preferredPrefix);
Map<String, String> subProperties = ConfigurationUtils.getSubProperties(instanceConfigMaps, preferredPrefix);
InmemoryConfiguration subPropsConfiguration = new InmemoryConfiguration(subProperties);
if (logger.isDebugEnabled()) {
String idOrName = "";
if (StringUtils.hasText(this.getId())) {
idOrName = "[id=" + this.getId() + "]";
} else {
String name = ReflectUtils.getProperty(this, "getName");
if (StringUtils.hasText(name)) {
idOrName = "[name=" + name + "]";
}
}
logger.debug("Refreshing " + this.getClass().getSimpleName() + idOrName +
" with prefix [" + preferredPrefix +
"], extracted props: " + subProperties);
}
assignProperties(this, environment, subProperties, subPropsConfiguration);
// process extra refresh of subclass, e.g. refresh method configs
processExtraRefresh(preferredPrefix, subPropsConfiguration);
} catch (Exception e) {
logger.error("Failed to override field value of config bean: " + this, e);
throw new IllegalStateException("Failed to override field value of config bean: " + this, e);
}
postProcessRefresh();
refreshed.set(true);
}
private void assignProperties(Object obj, Environment environment, Map<String, String> properties, InmemoryConfiguration configuration) {
// if old one (this) contains non-null value, do not override
boolean overrideIfAbsent = getConfigMode() == ConfigMode.OVERRIDE_IF_ABSENT;
// even if old one (this) contains non-null value, do override
boolean overrideAll = getConfigMode() == ConfigMode.OVERRIDE_ALL;
// loop methods, get override value and set the new value back to method
List<Method> methods = MethodUtils.getMethods(obj.getClass(), method -> method.getDeclaringClass() != Object.class);
Method[] methodsList = this.getClass().getDeclaredMethods();
for (Method method : methods) {
if (MethodUtils.isSetter(method)) {
String propertyName = extractPropertyName(method.getName());
// if config mode is OVERRIDE_IF_ABSENT and property has set, skip
if (overrideIfAbsent && isPropertySet(methodsList, propertyName)) {
continue;
}
// convert camelCase/snake_case to kebab-case
String kebabPropertyName = StringUtils.convertToSplitName(propertyName, "-");
try {
String value = StringUtils.trim(configuration.getString(kebabPropertyName));
// isTypeMatch() is called to avoid duplicate and incorrect update, for example, we have two 'setGeneric' methods in ReferenceConfig.
if (StringUtils.hasText(value)
&& ClassUtils.isTypeMatch(method.getParameterTypes()[0], value)
&& !isIgnoredAttribute(obj.getClass(), propertyName)) {
value = environment.resolvePlaceholders(value);
method.invoke(obj, ClassUtils.convertPrimitive(ScopeModelUtil.getFrameworkModel(getScopeModel()), method.getParameterTypes()[0], value));
}
} catch (Exception e) {
logger.info("Failed to override the property " + method.getName() + " in " +
obj.getClass().getSimpleName() +
", please make sure every property has getter/setter method provided.");
}
} else if (isParametersSetter(method)) {
String propertyName = extractPropertyName(method.getName());
String value = StringUtils.trim(configuration.getString(propertyName));
Map<String, String> parameterMap;
if (StringUtils.hasText(value)) {
parameterMap = StringUtils.parseParameters(value);
} else {
// in this case, maybe parameters.item3=value3.
parameterMap = ConfigurationUtils.getSubProperties(properties, PARAMETERS);
}
Map<String, String> newMap = convert(parameterMap, "");
if (CollectionUtils.isEmptyMap(newMap)) {
continue;
}
// get old map from original obj
Map<String, String> oldMap = null;
try {
String getterName = calculatePropertyToGetter(propertyName);
Method getterMethod = this.getClass().getDeclaredMethod(getterName);
Object oldOne = getterMethod.invoke(this);
if (oldOne instanceof Map) {
oldMap = (Map) oldOne;
}
} catch (Exception ignore) {
}
// if old map is null, directly set params
if (oldMap == null) {
invokeSetParameters(newMap, obj);
continue;
}
// if mode is OVERRIDE_IF_ABSENT, put all old map entries to new map, will override the same key
// if mode is OVERRIDE_ALL, put all keyed entries not in new map from old map to new map (ignore the same key appeared in old map)
// if mode is others, override with new map
if (overrideIfAbsent) {
newMap.putAll(oldMap);
} else if (overrideAll) {
oldMap.forEach(newMap::putIfAbsent);
}
invokeSetParameters(newMap, obj);
} else if (isNestedSetter(obj, method)) {
try {
Class<?> clazz = method.getParameterTypes()[0];
Object inner = clazz.getDeclaredConstructor().newInstance();
String fieldName = MethodUtils.extractFieldName(method);
Map<String, String> subProperties = ConfigurationUtils.getSubProperties(properties, fieldName);
InmemoryConfiguration subPropsConfiguration = new InmemoryConfiguration(subProperties);
assignProperties(inner, environment, subProperties, subPropsConfiguration);
method.invoke(obj, inner);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Cannot assign nested class when refreshing config: " + obj.getClass().getName(), e);
}
}
}
}
private boolean isPropertySet(Method[] methods, String propertyName) {
try {
String getterName = calculatePropertyToGetter(propertyName);
Method getterMethod = findGetMethod(methods,getterName);
if (getterMethod == null) {
return false;
}
Object oldOne = getterMethod.invoke(this);
if (oldOne != null) {
return true;
}
} catch (Exception ignore) {
}
return false;
}
private Method findGetMethod(Method[] methods, String methodName) {
for (Method method : methods) {
if (method.getName().equals(methodName) && method.getParameterCount() == 0) {
return method;
}
}
return null;
}
private void invokeSetParameters(Map<String, String> values, Object obj) {
if (CollectionUtils.isEmptyMap(values)) {
return;
}
Map<String, String> map = new HashMap<>();
Map<String, String> getParametersMap = invokeGetParameters(obj.getClass(), obj);
if (getParametersMap != null && !getParametersMap.isEmpty()) {
map.putAll(getParametersMap);
}
map.putAll(values);
invokeSetParameters(obj.getClass(), obj, map);
}
private boolean isIgnoredAttribute(Class<?> clazz, String propertyName) {
Method getter = null;
String capitalizePropertyName = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
try {
getter = clazz.getMethod("get" + capitalizePropertyName);
} catch (NoSuchMethodException e) {
try {
getter = clazz.getMethod("is" + capitalizePropertyName);
} catch (NoSuchMethodException ex) {
// ignore
}
}
if (getter == null) {
// no getter method
return true;
}
Parameter parameter = getter.getAnnotation(Parameter.class);
// not an attribute
return parameter != null && !parameter.attribute();
}
protected void processExtraRefresh(String preferredPrefix, InmemoryConfiguration subPropsConfiguration) {
// process extra refresh
}
protected void preProcessRefresh() {
// pre-process refresh
}
protected void postProcessRefresh() {
// post-process refresh
checkDefault();
}
/**
* Check and set default value for some fields.
* <p>
* This method will be called at the end of {@link #refresh()}, as a post-initializer.
* </p>
* <p>NOTE: </p>
* <p>
* To distinguish between user-set property values and default property values,
* do not initialize default value at field declare statement. <b>If the field has a default value,
* it should be set in the checkDefault() method</b>, which will be called at the end of {@link #refresh()},
* so that it will not affect the behavior of attribute overrides.</p>
*
* @see AbstractConfig#getMetaData()
* @see AbstractConfig#appendAttributes(Map, Object)
*/
protected void checkDefault() {
}
@Parameter(excluded = true, attribute = false)
public boolean isRefreshed() {
return refreshed.get();
}
/**
* FIXME check @Parameter(required=true) and any conditions that need to match.
*/
@Parameter(excluded = true, attribute = false)
public boolean isValid() {
return true;
}
@Parameter(excluded = true, attribute = false)
public Boolean isDefault() {
return isDefault;
}
public void setDefault(Boolean isDefault) {
this.isDefault = isDefault;
}
@Override
public String toString() {
try {
StringBuilder buf = new StringBuilder();
buf.append("<dubbo:");
buf.append(getTagName(getClass()));
for (Method method : getAttributedMethods()) {
try {
String name = method.getName();
String key = calculateAttributeFromGetter(name);
Object value = method.invoke(this);
if (value != null) {
buf.append(' ');
buf.append(key);
buf.append("=\"");
buf.append(key.equals("password") ? "******" : value);
buf.append('\"');
}
} catch (Exception e) {
logger.warn(e.getMessage(), e);
}
}
buf.append(" />");
return buf.toString();
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
return super.toString();
}
}
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
if (obj == this) {
return true;
}
for (Method method : getAttributedMethods()) {
// ignore compare 'id' value
if ("getId".equals(method.getName())) {
continue;
}
try {
Object value1 = method.invoke(this);
Object value2 = method.invoke(obj);
if (!Objects.equals(value1, value2)) {
return false;
}
} catch (Exception e) {
throw new IllegalStateException("compare config instances failed", e);
}
}
return true;
}
@Override
public int hashCode() {
int hashCode = 1;
for (Method method : getAttributedMethods()) {
// ignore compare 'id' value
if ("getId".equals(method.getName())) {
continue;
}
try {
Object value = method.invoke(this);
if (value != null) {
hashCode = 31 * hashCode + value.hashCode();
}
} catch (Exception ignored) {
//ignored
}
}
if (hashCode == 0) {
hashCode = 1;
}
return hashCode;
}
private List<Method> getAttributedMethods() {
Class<? extends AbstractConfig> cls = this.getClass();
return attributedMethodCache.computeIfAbsent(cls, (key) -> computeAttributedMethods());
}
/**
* compute attributed getter methods, subclass can override this method to add/remove attributed methods
*
* @return
*/
protected List<Method> computeAttributedMethods() {
Class<? extends AbstractConfig> cls = this.getClass();
BeanInfo beanInfo = getBeanInfo(cls);
List<Method> methods = new ArrayList<>(beanInfo.getMethodDescriptors().length);
for (MethodDescriptor methodDescriptor : beanInfo.getMethodDescriptors()) {
Method method = methodDescriptor.getMethod();
if (MethodUtils.isGetter(method) || isParametersGetter(method)) {
// filter non attribute
Parameter parameter = method.getAnnotation(Parameter.class);
if (parameter != null && !parameter.attribute()) {
continue;
}
String propertyName = calculateAttributeFromGetter(method.getName());
// filter non-writable property, exclude non property methods, fix #4225
if (!isWritableProperty(beanInfo, propertyName)) {
continue;
}
methods.add(method);
}
}
return methods;
}
protected ConfigManager getConfigManager() {
return getApplicationModel().getApplicationConfigManager();
}
}