From b5e06b906aa68bbc76fa83d8dfaa2a9386c7c320 Mon Sep 17 00:00:00 2001 From: weihu Date: Mon, 11 Jul 2022 09:37:22 +0800 Subject: [PATCH] update refresh scope --- .../PolarisConfigAutoConfiguration.java | 25 +- .../PolarisPropertySourceAutoRefresher.java | 96 ++++++-- .../config/config/ConfigPropertySource.java | 41 ++++ .../config/ConfigPropertySourceFactory.java | 27 +++ .../config/PropertySourcesProcessor.java | 158 +++++++++++++ .../PolarisAnnotationProcessor.java | 223 ++++++++++++++++++ .../spring/annotation/PolarisConfig.java | 13 + .../spring/annotation/PolarisProcessor.java | 67 ++++++ .../annotation/SpringValueProcessor.java | 167 +++++++++++++ .../spring/property/PlaceholderHelper.java | 188 +++++++++++++++ .../config/spring/property/SpringValue.java | 151 ++++++++++++ .../property/SpringValueDefinition.java | 48 ++++ .../SpringValueDefinitionProcessor.java | 114 +++++++++ .../spring/property/SpringValueRegistry.java | 102 ++++++++ .../config/adapter/MockedConfigChange.java | 19 ++ ...arisPropertiesSourceAutoRefresherTest.java | 65 ++--- .../cloud/common/util/JacksonUtils.java | 10 + 17 files changed, 1461 insertions(+), 53 deletions(-) create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/ConfigPropertySource.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/ConfigPropertySourceFactory.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PropertySourcesProcessor.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisAnnotationProcessor.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisConfig.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisProcessor.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/SpringValueProcessor.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/PlaceholderHelper.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValue.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueDefinition.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueDefinitionProcessor.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueRegistry.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/MockedConfigChange.java diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java index fb70489c7..b82a2ddcf 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java @@ -23,10 +23,12 @@ import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceManager; import com.tencent.cloud.polaris.config.annotation.PolarisConfigAnnotationProcessor; import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; import com.tencent.cloud.polaris.config.listener.PolarisConfigChangeEventListener; +import com.tencent.cloud.polaris.config.spring.annotation.SpringValueProcessor; +import com.tencent.cloud.polaris.config.spring.property.PlaceholderHelper; +import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -44,9 +46,10 @@ public class PolarisConfigAutoConfiguration { public PolarisPropertySourceAutoRefresher polarisPropertySourceAutoRefresher( PolarisConfigProperties polarisConfigProperties, PolarisPropertySourceManager polarisPropertySourceManager, - ContextRefresher contextRefresher) { + SpringValueRegistry springValueRegistry, + PlaceholderHelper placeholderHelper) { return new PolarisPropertySourceAutoRefresher(polarisConfigProperties, - polarisPropertySourceManager, contextRefresher); + polarisPropertySourceManager, springValueRegistry, placeholderHelper); } @Bean @@ -58,4 +61,20 @@ public class PolarisConfigAutoConfiguration { public PolarisConfigChangeEventListener polarisConfigChangeEventListener() { return new PolarisConfigChangeEventListener(); } + + @Bean + public SpringValueRegistry springValueRegistry() { + return new SpringValueRegistry(); + } + + @Bean + public PlaceholderHelper placeholderHelper() { + return new PlaceholderHelper(); + } + + @Bean + public SpringValueProcessor springValueProcessor(PlaceholderHelper placeholderHelper, SpringValueRegistry springValueRegistry, PolarisConfigProperties polarisConfigProperties) { + return new SpringValueProcessor(placeholderHelper, springValueRegistry, polarisConfigProperties); + } + } diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertySourceAutoRefresher.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertySourceAutoRefresher.java index 25af8ff32..7708e8b0f 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertySourceAutoRefresher.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertySourceAutoRefresher.java @@ -18,19 +18,24 @@ package com.tencent.cloud.polaris.config.adapter; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import com.tencent.cloud.common.util.JacksonUtils; import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; +import com.tencent.cloud.polaris.config.spring.property.PlaceholderHelper; +import com.tencent.cloud.polaris.config.spring.property.SpringValue; +import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeListener; -import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; @@ -50,17 +55,25 @@ public class PolarisPropertySourceAutoRefresher private final PolarisConfigProperties polarisConfigProperties; private final PolarisPropertySourceManager polarisPropertySourceManager; - private final ContextRefresher contextRefresher; + private final AtomicBoolean registered = new AtomicBoolean(false); + + private TypeConverter typeConverter; + private final SpringValueRegistry springValueRegistry; + private ConfigurableBeanFactory beanFactory; + private final PlaceholderHelper placeholderHelper; + private ApplicationContext applicationContext; public PolarisPropertySourceAutoRefresher( PolarisConfigProperties polarisConfigProperties, PolarisPropertySourceManager polarisPropertySourceManager, - ContextRefresher contextRefresher) { + SpringValueRegistry springValueRegistry, + PlaceholderHelper placeholderHelper) { this.polarisConfigProperties = polarisConfigProperties; this.polarisPropertySourceManager = polarisPropertySourceManager; - this.contextRefresher = contextRefresher; + this.springValueRegistry = springValueRegistry; + this.placeholderHelper = placeholderHelper; } @Override @@ -101,25 +114,66 @@ public class PolarisPropertySourceAutoRefresher Map source = polarisPropertySource.getSource(); for (String changedKey : configKVFileChangeEvent.changedKeys()) { - ConfigPropertyChangeInfo configPropertyChangeInfo = configKVFileChangeEvent - .getChangeInfo(changedKey); - - LOGGER.info("[SCT Config] changed property = {}", configPropertyChangeInfo); - - switch (configPropertyChangeInfo.getChangeType()) { - case MODIFIED: - case ADDED: - source.put(changedKey, configPropertyChangeInfo.getNewValue()); - break; - case DELETED: - source.remove(changedKey); - break; + Collection targetValues = springValueRegistry.get(beanFactory, changedKey); + if (targetValues == null || targetValues.isEmpty()) { + continue; + } + // 2. update the value + for (SpringValue val : targetValues) { + updateSpringValue(val); } } - - // rebuild beans with @RefreshScope annotation - contextRefresher.refresh(); }); } } + + private void updateSpringValue(SpringValue springValue) { + try { + Object value = resolvePropertyValue(springValue); + springValue.update(value); + + LOGGER.info("Auto update polaris changed value successfully, new value: {}, {}", value, + springValue); + } + catch (Throwable ex) { + LOGGER.error("Auto update polaris changed value failed, {}", springValue.toString(), ex); + } + } + + + /** + * Logic transplanted from DefaultListableBeanFactory. + * + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency(org.springframework.beans.factory.config.DependencyDescriptor, + * java.lang.String, java.util.Set, org.springframework.beans.TypeConverter) + */ + private Object resolvePropertyValue(SpringValue springValue) { + // value will never be null + Object value = placeholderHelper + .resolvePropertyValue(beanFactory, springValue.getBeanName(), springValue.getPlaceholder()); + + if (springValue.isJson()) { + value = parseJsonValue((String) value, springValue.getTargetType()); + } + else { + value = springValue.isField() ? this.typeConverter.convertIfNecessary(value, springValue.getTargetType(), springValue.getField()) : + this.typeConverter.convertIfNecessary(value, springValue.getTargetType(), + springValue.getMethodParameter()); + } + return value; + } + + private Object parseJsonValue(String json, Class targetType) { + try { + return JacksonUtils.json2JavaBean(json, targetType); + } + catch (Throwable ex) { + LOGGER.error("Parsing json '{}' to type {} failed!", json, targetType, ex); + throw ex; + } + } + + + + } diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/ConfigPropertySource.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/ConfigPropertySource.java new file mode 100644 index 000000000..3de48c614 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/ConfigPropertySource.java @@ -0,0 +1,41 @@ +package com.tencent.cloud.polaris.config.config; + +import java.util.Set; + +import com.tencent.cloud.polaris.config.listener.ConfigChangeListener; + +/** + *@author : wh + *@date : 2022/7/10 23:10 + *@description: + */ +public class ConfigPropertySource extends EnumerablePropertySource { + private static final String[] EMPTY_ARRAY = new String[0]; + + ConfigPropertySource(String name, Config source) { + super(name, source); + } + + @Override + public boolean containsProperty(String name) { + return this.source.getProperty(name, null) != null; + } + + @Override + public String[] getPropertyNames() { + Set propertyNames = this.source.getPropertyNames(); + if (propertyNames.isEmpty()) { + return EMPTY_ARRAY; + } + return propertyNames.toArray(new String[propertyNames.size()]); + } + + @Override + public Object getProperty(String name) { + return this.source.getProperty(name, null); + } + + public void addChangeListener(ConfigChangeListener listener) { + this.source.addChangeListener(listener); + } +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/ConfigPropertySourceFactory.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/ConfigPropertySourceFactory.java new file mode 100644 index 000000000..fc2531b82 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/ConfigPropertySourceFactory.java @@ -0,0 +1,27 @@ +package com.tencent.cloud.polaris.config.config; + +import java.util.List; + +import com.google.common.collect.Lists; + +/** + *@author : wh + *@date : 2022/7/10 23:10 + *@description: + */ +public class ConfigPropertySourceFactory { + + private final List configPropertySources = Lists.newLinkedList(); + + public ConfigPropertySource getConfigPropertySource(String name, Config source) { + ConfigPropertySource configPropertySource = new ConfigPropertySource(name, source); + + configPropertySources.add(configPropertySource); + + return configPropertySource; + } + + public List getAllConfigPropertySources() { + return Lists.newLinkedList(configPropertySources); + } +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PropertySourcesProcessor.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PropertySourcesProcessor.java new file mode 100644 index 000000000..eefc72b5e --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PropertySourcesProcessor.java @@ -0,0 +1,158 @@ +package com.tencent.cloud.polaris.config.config; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import com.tencent.cloud.polaris.config.listener.ConfigChangeListener; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; + +/** + *@author : wh + *@date : 2022/7/10 23:09 + *@description: + */ +public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, + ApplicationEventPublisherAware, PriorityOrdered { + private static final Multimap NAMESPACE_NAMES = LinkedHashMultimap.create(); + private static final Set AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES = Sets.newConcurrentHashSet(); + + private final ConfigPropertySourceFactory configPropertySourceFactory = SpringInjector + .getInstance(ConfigPropertySourceFactory.class); + private ConfigUtil configUtil; + private ConfigurableEnvironment environment; + private ApplicationEventPublisher applicationEventPublisher; + + public static boolean addNamespaces(Collection namespaces, int order) { + return NAMESPACE_NAMES.putAll(order, namespaces); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + this.configUtil = ApolloInjector.getInstance(ConfigUtil.class); + initializePropertySources(); + initializeAutoUpdatePropertiesFeature(beanFactory); + } + + private void initializePropertySources() { + if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME)) { + //already initialized + return; + } + CompositePropertySource composite; + if (configUtil.isPropertyNamesCacheEnabled()) { + composite = new CachedCompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME); + } else { + composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME); + } + + //sort by order asc + ImmutableSortedSet orders = ImmutableSortedSet.copyOf(NAMESPACE_NAMES.keySet()); + Iterator iterator = orders.iterator(); + + while (iterator.hasNext()) { + int order = iterator.next(); + for (String namespace : NAMESPACE_NAMES.get(order)) { + Config config = ConfigService.getConfig(namespace); + + composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config)); + } + } + + // clean up + NAMESPACE_NAMES.clear(); + + // add after the bootstrap property source or to the first + if (environment.getPropertySources() + .contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) { + + if (configUtil.isOverrideSystemProperties()) { + // ensure ApolloBootstrapPropertySources is still the first + ensureBootstrapPropertyPrecedence(environment); + } + + environment.getPropertySources() + .addAfter(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME, composite); + } else { + if (!configUtil.isOverrideSystemProperties()) { + if (environment.getPropertySources().contains(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) { + environment.getPropertySources().addAfter(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, composite); + return; + } + } + environment.getPropertySources().addFirst(composite); + } + } + + private void ensureBootstrapPropertyPrecedence(ConfigurableEnvironment environment) { + MutablePropertySources propertySources = environment.getPropertySources(); + + PropertySource bootstrapPropertySource = propertySources + .get(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME); + + // not exists or already in the first place + if (bootstrapPropertySource == null || propertySources.precedenceOf(bootstrapPropertySource) == 0) { + return; + } + + propertySources.remove(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME); + propertySources.addFirst(bootstrapPropertySource); + } + + private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) { + if (!AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.add(beanFactory)) { + return; + } + + ConfigChangeListener configChangeEventPublisher = changeEvent -> + applicationEventPublisher.publishEvent(new ApolloConfigChangeEvent(changeEvent)); + + List configPropertySources = configPropertySourceFactory.getAllConfigPropertySources(); + for (ConfigPropertySource configPropertySource : configPropertySources) { + configPropertySource.addChangeListener(configChangeEventPublisher); + } + } + + @Override + public void setEnvironment(Environment environment) { + //it is safe enough to cast as all known environment is derived from ConfigurableEnvironment + this.environment = (ConfigurableEnvironment) environment; + } + + @Override + public int getOrder() { + //make it as early as possible + return Ordered.HIGHEST_PRECEDENCE; + } + + // for test only + static void reset() { + NAMESPACE_NAMES.clear(); + AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.clear(); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisAnnotationProcessor.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisAnnotationProcessor.java new file mode 100644 index 000000000..c0de10db9 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisAnnotationProcessor.java @@ -0,0 +1,223 @@ +/* +package com.tencent.cloud.polaris.config.spring.annotation; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Set; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import com.google.gson.Gson; +import com.tencent.cloud.polaris.config.listener.ConfigChangeEvent; +import com.tencent.cloud.polaris.config.listener.ConfigChangeListener; +import com.tencent.cloud.polaris.config.spring.property.PlaceholderHelper; +import com.tencent.cloud.polaris.config.spring.property.SpringValue; +import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.Environment; +import org.springframework.util.ReflectionUtils; + +*/ +/** + *@author : wh + *@date : 2022/7/10 14:38 + *@description: + *//* + +public class PolarisAnnotationProcessor extends PolarisProcessor implements BeanFactoryAware, + EnvironmentAware { + + private static final Logger logger = LoggerFactory.getLogger(PolarisAnnotationProcessor.class); + private static final Gson GSON = new Gson(); + + private final ConfigUtil configUtil; + private final PlaceholderHelper placeholderHelper; + private final SpringValueRegistry springValueRegistry; + + */ +/** + * resolve the expression. + *//* + + private ConfigurableBeanFactory configurableBeanFactory; + + private Environment environment; + + public PolarisAnnotationProcessor(PlaceholderHelper placeholderHelper, + SpringValueRegistry springValueRegistry) { + + configUtil = ApolloInjector.getInstance(ConfigUtil.class); + this.placeholderHelper = placeholderHelper; + this.springValueRegistry = springValueRegistry; + } + + @Override + protected void processField(Object bean, String beanName, Field field) { + this.processApolloConfig(bean, field); + this.processApolloJsonValue(bean, beanName, field); + } + + @Override + protected void processMethod(final Object bean, String beanName, final Method method) { + this.processApolloConfigChangeListener(bean, method); + this.processApolloJsonValue(bean, beanName, method); + } + + private void processApolloConfig(Object bean, Field field) { + ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class); + if (annotation == null) { + return; + } + + Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()), + "Invalid type: %s for field: %s, should be Config", field.getType(), field); + + final String namespace = annotation.value(); + final String resolvedNamespace = this.environment.resolveRequiredPlaceholders(namespace); + Config config = ConfigService.getConfig(resolvedNamespace); + + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, bean, config); + } + + private void processApolloConfigChangeListener(final Object bean, final Method method) { + ApolloConfigChangeListener annotation = AnnotationUtils + .findAnnotation(method, ApolloConfigChangeListener.class); + if (annotation == null) { + return; + } + Class[] parameterTypes = method.getParameterTypes(); + Preconditions.checkArgument(parameterTypes.length == 1, + "Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length, + method); + Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]), + "Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0], + method); + + ReflectionUtils.makeAccessible(method); + String[] namespaces = annotation.value(); + String[] annotatedInterestedKeys = annotation.interestedKeys(); + String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes(); + ConfigChangeListener configChangeListener = new ConfigChangeListener() { + @Override + public void onChange(ConfigChangeEvent changeEvent) { + ReflectionUtils.invokeMethod(method, bean, changeEvent); + } + }; + + Set interestedKeys = + annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null; + Set interestedKeyPrefixes = + annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes) + : null; + + for (String namespace : namespaces) { + final String resolvedNamespace = this.environment.resolveRequiredPlaceholders(namespace); + Config config = ConfigService.getConfig(resolvedNamespace); + + if (interestedKeys == null && interestedKeyPrefixes == null) { + config.addChangeListener(configChangeListener); + } + else { + config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes); + } + } + } + + + private void processApolloJsonValue(Object bean, String beanName, Field field) { + ApolloJsonValue apolloJsonValue = AnnotationUtils.getAnnotation(field, ApolloJsonValue.class); + if (apolloJsonValue == null) { + return; + } + String placeholder = apolloJsonValue.value(); + Object propertyValue = placeholderHelper + .resolvePropertyValue(this.configurableBeanFactory, beanName, placeholder); + + // propertyValue will never be null, as @ApolloJsonValue will not allow that + if (!(propertyValue instanceof String)) { + return; + } + + boolean accessible = field.isAccessible(); + field.setAccessible(true); + ReflectionUtils + .setField(field, bean, parseJsonValue((String) propertyValue, field.getGenericType())); + field.setAccessible(accessible); + + if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) { + Set keys = placeholderHelper.extractPlaceholderKeys(placeholder); + for (String key : keys) { + SpringValue springValue = new SpringValue(key, placeholder, bean, beanName, field, true); + springValueRegistry.register(this.configurableBeanFactory, key, springValue); + logger.debug("Monitoring {}", springValue); + } + } + } + + private void processApolloJsonValue(Object bean, String beanName, Method method) { + ApolloJsonValue apolloJsonValue = AnnotationUtils.getAnnotation(method, ApolloJsonValue.class); + if (apolloJsonValue == null) { + return; + } + String placeHolder = apolloJsonValue.value(); + + Object propertyValue = placeholderHelper + .resolvePropertyValue(this.configurableBeanFactory, beanName, placeHolder); + + // propertyValue will never be null, as @ApolloJsonValue will not allow that + if (!(propertyValue instanceof String)) { + return; + } + + Type[] types = method.getGenericParameterTypes(); + Preconditions.checkArgument(types.length == 1, + "Ignore @Value setter {}.{}, expecting 1 parameter, actual {} parameters", + bean.getClass().getName(), method.getName(), method.getParameterTypes().length); + + boolean accessible = method.isAccessible(); + method.setAccessible(true); + ReflectionUtils.invokeMethod(method, bean, parseJsonValue((String) propertyValue, types[0])); + method.setAccessible(accessible); + + if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) { + Set keys = placeholderHelper.extractPlaceholderKeys(placeHolder); + for (String key : keys) { + SpringValue springValue = new SpringValue(key, apolloJsonValue.value(), bean, beanName, + method, true); + springValueRegistry.register(this.configurableBeanFactory, key, springValue); + logger.debug("Monitoring {}", springValue); + } + } + } + + private Object parseJsonValue(String json, Type targetType) { + try { + return GSON.fromJson(json, targetType); + } + catch (Throwable ex) { + logger.error("Parsing json '{}' to type {} failed!", json, targetType, ex); + throw ex; + } + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.configurableBeanFactory = (ConfigurableBeanFactory) beanFactory; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } +} +*/ diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisConfig.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisConfig.java new file mode 100644 index 000000000..921e9d59b --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisConfig.java @@ -0,0 +1,13 @@ +/* +package com.tencent.cloud.polaris.config.spring.annotation; + +*/ +/** + *@author : wh + *@date : 2022/7/10 14:38 + *@description: + *//* + +public class PolarisConfig { +} +*/ diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisProcessor.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisProcessor.java new file mode 100644 index 000000000..bfdd9db6a --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/PolarisProcessor.java @@ -0,0 +1,67 @@ +package com.tencent.cloud.polaris.config.spring.annotation; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.util.ReflectionUtils; + +/** + *@author : wh + *@date : 2022/7/10 14:35 + *@description: + */ +public abstract class PolarisProcessor implements BeanPostProcessor, PriorityOrdered { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + Class clazz = bean.getClass(); + for (Field field : findAllField(clazz)) { + processField(bean, beanName, field); + } + for (Method method : findAllMethod(clazz)) { + processMethod(bean, beanName, method); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + /** + * subclass should implement this method to process field + */ + protected abstract void processField(Object bean, String beanName, Field field); + + /** + * subclass should implement this method to process method + */ + protected abstract void processMethod(Object bean, String beanName, Method method); + + + @Override + public int getOrder() { + //make it as late as possible + return Ordered.LOWEST_PRECEDENCE; + } + + private List findAllField(Class clazz) { + final List res = new LinkedList<>(); + ReflectionUtils.doWithFields(clazz, field -> res.add(field)); + return res; + } + + private List findAllMethod(Class clazz) { + final List res = new LinkedList<>(); + ReflectionUtils.doWithMethods(clazz, method -> res.add(method)); + return res; + } +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/SpringValueProcessor.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/SpringValueProcessor.java new file mode 100644 index 000000000..f9f255691 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/annotation/SpringValueProcessor.java @@ -0,0 +1,167 @@ +package com.tencent.cloud.polaris.config.spring.annotation; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Set; + +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; +import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; +import com.tencent.cloud.polaris.config.spring.property.PlaceholderHelper; +import com.tencent.cloud.polaris.config.spring.property.SpringValue; +import com.tencent.cloud.polaris.config.spring.property.SpringValueDefinition; +import com.tencent.cloud.polaris.config.spring.property.SpringValueDefinitionProcessor; +import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.Bean; + +/** + *@author : wh + *@date : 2022/7/10 14:15 + *@description: + */ +public class SpringValueProcessor extends PolarisProcessor implements BeanFactoryPostProcessor, BeanFactoryAware { + + private static final Logger logger = LoggerFactory.getLogger(SpringValueProcessor.class); + + private final PolarisConfigProperties polarisConfigProperties; + private final PlaceholderHelper placeholderHelper; + private final SpringValueRegistry springValueRegistry; + + private BeanFactory beanFactory; + private Multimap beanName2SpringValueDefinitions; + + public SpringValueProcessor(PlaceholderHelper placeholderHelper, + SpringValueRegistry springValueRegistry, + PolarisConfigProperties polarisConfigProperties) { + this.placeholderHelper = placeholderHelper; + this.polarisConfigProperties = polarisConfigProperties; + this.springValueRegistry = springValueRegistry; + beanName2SpringValueDefinitions = LinkedListMultimap.create(); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + if (polarisConfigProperties.isAutoRefresh() && beanFactory instanceof BeanDefinitionRegistry) { + beanName2SpringValueDefinitions = SpringValueDefinitionProcessor + .getBeanName2SpringValueDefinitions((BeanDefinitionRegistry) beanFactory); + } + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + if (polarisConfigProperties.isAutoRefresh()) { + super.postProcessBeforeInitialization(bean, beanName); + processBeanPropertyValues(bean, beanName); + } + return bean; + } + + + @Override + protected void processField(Object bean, String beanName, Field field) { + // register @Value on field + Value value = field.getAnnotation(Value.class); + if (value == null) { + return; + } + + doRegister(bean, beanName, field, value); + } + + @Override + protected void processMethod(Object bean, String beanName, Method method) { + //register @Value on method + Value value = method.getAnnotation(Value.class); + if (value == null) { + return; + } + //skip Configuration bean methods + if (method.getAnnotation(Bean.class) != null) { + return; + } + if (method.getParameterTypes().length != 1) { + logger.error("Ignore @Value setter {}.{}, expecting 1 parameter, actual {} parameters", + bean.getClass().getName(), method.getName(), method.getParameterTypes().length); + return; + } + + doRegister(bean, beanName, method, value); + } + + private void doRegister(Object bean, String beanName, Member member, Value value) { + Set keys = placeholderHelper.extractPlaceholderKeys(value.value()); + if (keys.isEmpty()) { + return; + } + + for (String key : keys) { + SpringValue springValue; + if (member instanceof Field) { + Field field = (Field) member; + springValue = new SpringValue(key, value.value(), bean, beanName, field, false); + } + else if (member instanceof Method) { + Method method = (Method) member; + springValue = new SpringValue(key, value.value(), bean, beanName, method, false); + } + else { + logger.error("Apollo @Value annotation currently only support to be used on methods and fields, " + + "but is used on {}", member.getClass()); + return; + } + springValueRegistry.register(beanFactory, key, springValue); + logger.info("Monitoring {}", springValue); + } + } + + private void processBeanPropertyValues(Object bean, String beanName) { + Collection propertySpringValues = beanName2SpringValueDefinitions + .get(beanName); + if (propertySpringValues == null || propertySpringValues.isEmpty()) { + return; + } + + for (SpringValueDefinition definition : propertySpringValues) { + try { + PropertyDescriptor pd = BeanUtils + .getPropertyDescriptor(bean.getClass(), definition.getPropertyName()); + Method method = pd.getWriteMethod(); + if (method == null) { + continue; + } + SpringValue springValue = new SpringValue(definition.getKey(), definition.getPlaceholder(), + bean, beanName, method, false); + springValueRegistry.register(beanFactory, definition.getKey(), springValue); + logger.debug("Monitoring {}", springValue); + } + catch (Throwable ex) { + logger.error("Failed to enable auto update feature for {}.{}", bean.getClass(), + definition.getPropertyName()); + } + } + + // clear + beanName2SpringValueDefinitions.removeAll(beanName); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/PlaceholderHelper.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/PlaceholderHelper.java new file mode 100644 index 000000000..f789bef49 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/PlaceholderHelper.java @@ -0,0 +1,188 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.polaris.config.spring.property; + +import java.util.Set; +import java.util.Stack; + +import com.google.common.base.Strings; +import com.google.common.collect.Sets; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.util.StringUtils; + +/** + *@author : wh + *@date : 2022/7/10 14:26 + *@description: + */ +public class PlaceholderHelper { + + private static final String PLACEHOLDER_PREFIX = "${"; + private static final String PLACEHOLDER_SUFFIX = "}"; + private static final String VALUE_SEPARATOR = ":"; + private static final String SIMPLE_PLACEHOLDER_PREFIX = "{"; + private static final String EXPRESSION_PREFIX = "#{"; + private static final String EXPRESSION_SUFFIX = "}"; + + /** + * Resolve placeholder property values, e.g. + *
+ *
+ * "${somePropertyValue}" -> "the actual property value" + */ + public Object resolvePropertyValue(ConfigurableBeanFactory beanFactory, String beanName, String placeholder) { + // resolve string value + String strVal = beanFactory.resolveEmbeddedValue(placeholder); + + BeanDefinition bd = (beanFactory.containsBean(beanName) ? beanFactory + .getMergedBeanDefinition(beanName) : null); + + // resolve expressions like "#{systemProperties.myProp}" + return evaluateBeanDefinitionString(beanFactory, strVal, bd); + } + + private Object evaluateBeanDefinitionString(ConfigurableBeanFactory beanFactory, String value, + BeanDefinition beanDefinition) { + if (beanFactory.getBeanExpressionResolver() == null) { + return value; + } + Scope scope = (beanDefinition != null ? beanFactory + .getRegisteredScope(beanDefinition.getScope()) : null); + return beanFactory.getBeanExpressionResolver() + .evaluate(value, new BeanExpressionContext(beanFactory, scope)); + } + + /** + * Extract keys from placeholder, e.g. + *
    + *
  • ${some.key} => "some.key"
  • + *
  • ${some.key:${some.other.key:100}} => "some.key", "some.other.key"
  • + *
  • ${${some.key}} => "some.key"
  • + *
  • ${${some.key:other.key}} => "some.key"
  • + *
  • ${${some.key}:${another.key}} => "some.key", "another.key"
  • + *
  • #{new java.text.SimpleDateFormat('${some.key}').parse('${another.key}')} => "some.key", "another.key"
  • + *
+ */ + public Set extractPlaceholderKeys(String propertyString) { + Set placeholderKeys = Sets.newHashSet(); + + if (Strings.isNullOrEmpty(propertyString) || (!isNormalizedPlaceholder(propertyString) && !isExpressionWithPlaceholder(propertyString))) { + return placeholderKeys; + } + + Stack stack = new Stack<>(); + stack.push(propertyString); + + while (!stack.isEmpty()) { + String strVal = stack.pop(); + int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX); + if (startIndex == -1) { + placeholderKeys.add(strVal); + continue; + } + int endIndex = findPlaceholderEndIndex(strVal, startIndex); + if (endIndex == -1) { + // invalid placeholder? + continue; + } + + String placeholderCandidate = strVal.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex); + + // ${some.key:other.key} + if (placeholderCandidate.startsWith(PLACEHOLDER_PREFIX)) { + stack.push(placeholderCandidate); + } + else { + // some.key:${some.other.key:100} + int separatorIndex = placeholderCandidate.indexOf(VALUE_SEPARATOR); + + if (separatorIndex == -1) { + stack.push(placeholderCandidate); + } + else { + stack.push(placeholderCandidate.substring(0, separatorIndex)); + String defaultValuePart = + normalizeToPlaceholder(placeholderCandidate.substring(separatorIndex + VALUE_SEPARATOR.length())); + if (!Strings.isNullOrEmpty(defaultValuePart)) { + stack.push(defaultValuePart); + } + } + } + + // has remaining part, e.g. ${a}.${b} + if (endIndex + PLACEHOLDER_SUFFIX.length() < strVal.length() - 1) { + String remainingPart = normalizeToPlaceholder(strVal.substring(endIndex + PLACEHOLDER_SUFFIX.length())); + if (!Strings.isNullOrEmpty(remainingPart)) { + stack.push(remainingPart); + } + } + } + + return placeholderKeys; + } + + private boolean isNormalizedPlaceholder(String propertyString) { + return propertyString.startsWith(PLACEHOLDER_PREFIX) && propertyString.contains(PLACEHOLDER_SUFFIX); + } + + private boolean isExpressionWithPlaceholder(String propertyString) { + return propertyString.startsWith(EXPRESSION_PREFIX) && propertyString.contains(EXPRESSION_SUFFIX) + && propertyString.contains(PLACEHOLDER_PREFIX) && propertyString.contains(PLACEHOLDER_SUFFIX); + } + + private String normalizeToPlaceholder(String strVal) { + int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX); + if (startIndex == -1) { + return null; + } + int endIndex = strVal.lastIndexOf(PLACEHOLDER_SUFFIX); + if (endIndex == -1) { + return null; + } + + return strVal.substring(startIndex, endIndex + PLACEHOLDER_SUFFIX.length()); + } + + private int findPlaceholderEndIndex(CharSequence buf, int startIndex) { + int index = startIndex + PLACEHOLDER_PREFIX.length(); + int withinNestedPlaceholder = 0; + while (index < buf.length()) { + if (StringUtils.substringMatch(buf, index, PLACEHOLDER_SUFFIX)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + PLACEHOLDER_SUFFIX.length(); + } + else { + return index; + } + } + else if (StringUtils.substringMatch(buf, index, SIMPLE_PLACEHOLDER_PREFIX)) { + withinNestedPlaceholder++; + index = index + SIMPLE_PLACEHOLDER_PREFIX.length(); + } + else { + index++; + } + } + return -1; + } +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValue.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValue.java new file mode 100644 index 000000000..ce67bd13a --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValue.java @@ -0,0 +1,151 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.polaris.config.spring.property; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +import org.springframework.core.MethodParameter; + +/** + *@author : wh + *@date : 2022/7/10 14:23 + *@description: + */ +public class SpringValue { + + private MethodParameter methodParameter; + private Field field; + private final WeakReference beanRef; + private final String beanName; + private final String key; + private final String placeholder; + private final Class targetType; + private Type genericType; + private final boolean isJson; + + public SpringValue(String key, String placeholder, Object bean, String beanName, Field field, boolean isJson) { + this.beanRef = new WeakReference<>(bean); + this.beanName = beanName; + this.field = field; + this.key = key; + this.placeholder = placeholder; + this.targetType = field.getType(); + this.isJson = isJson; + if (isJson) { + this.genericType = field.getGenericType(); + } + } + + public SpringValue(String key, String placeholder, Object bean, String beanName, Method method, boolean isJson) { + this.beanRef = new WeakReference<>(bean); + this.beanName = beanName; + this.methodParameter = new MethodParameter(method, 0); + this.key = key; + this.placeholder = placeholder; + Class[] paramTps = method.getParameterTypes(); + this.targetType = paramTps[0]; + this.isJson = isJson; + if (isJson) { + this.genericType = method.getGenericParameterTypes()[0]; + } + } + + public void update(Object newVal) throws IllegalAccessException, InvocationTargetException { + if (isField()) { + injectField(newVal); + } + else { + injectMethod(newVal); + } + } + + private void injectField(Object newVal) throws IllegalAccessException { + Object bean = beanRef.get(); + if (bean == null) { + return; + } + boolean accessible = field.isAccessible(); + field.setAccessible(true); + field.set(bean, newVal); + field.setAccessible(accessible); + } + + private void injectMethod(Object newVal) + throws InvocationTargetException, IllegalAccessException { + Object bean = beanRef.get(); + if (bean == null) { + return; + } + methodParameter.getMethod().invoke(bean, newVal); + } + + public String getBeanName() { + return beanName; + } + + public Class getTargetType() { + return targetType; + } + + public String getPlaceholder() { + return this.placeholder; + } + + public MethodParameter getMethodParameter() { + return methodParameter; + } + + public boolean isField() { + return this.field != null; + } + + public Field getField() { + return field; + } + + public Type getGenericType() { + return genericType; + } + + public boolean isJson() { + return isJson; + } + + boolean isTargetBeanValid() { + return beanRef.get() != null; + } + + @Override + public String toString() { + Object bean = beanRef.get(); + if (bean == null) { + return ""; + } + if (isField()) { + return String + .format("key: %s, beanName: %s, field: %s.%s", key, beanName, bean.getClass() + .getName(), field.getName()); + } + return String.format("key: %s, beanName: %s, method: %s.%s", key, beanName, bean.getClass().getName(), + methodParameter.getMethod().getName()); + } +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueDefinition.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueDefinition.java new file mode 100644 index 000000000..f5fa55e1f --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueDefinition.java @@ -0,0 +1,48 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.polaris.config.spring.property; + +/** + *@author : wh + *@date : 2022/7/10 14:24 + *@description: + */ +public class SpringValueDefinition { + + private final String key; + private final String placeholder; + private final String propertyName; + + public SpringValueDefinition(String key, String placeholder, String propertyName) { + this.key = key; + this.placeholder = placeholder; + this.propertyName = propertyName; + } + + public String getKey() { + return key; + } + + public String getPlaceholder() { + return placeholder; + } + + public String getPropertyName() { + return propertyName; + } +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueDefinitionProcessor.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueDefinitionProcessor.java new file mode 100644 index 000000000..e750710b6 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueDefinitionProcessor.java @@ -0,0 +1,114 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.polaris.config.spring.property; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; + +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; + +/** + *@author : wh + *@date : 2022/7/10 14:25 + *@description: + */ +public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPostProcessor { + private static final Map> beanName2SpringValueDefinitions = + Maps.newConcurrentMap(); + private static final Set PROPERTY_VALUES_PROCESSED_BEAN_FACTORIES = Sets.newConcurrentHashSet(); + + private final PlaceholderHelper placeholderHelper; + + private PolarisConfigProperties polarisConfigProperties; + + public SpringValueDefinitionProcessor(PlaceholderHelper placeholderHelper, PolarisConfigProperties polarisConfigProperties) { + this.polarisConfigProperties = polarisConfigProperties; + this.placeholderHelper = placeholderHelper; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + if (polarisConfigProperties.isAutoRefresh()) { + processPropertyValues(registry); + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + + } + + public static Multimap getBeanName2SpringValueDefinitions(BeanDefinitionRegistry registry) { + Multimap springValueDefinitions = beanName2SpringValueDefinitions.get(registry); + if (springValueDefinitions == null) { + springValueDefinitions = LinkedListMultimap.create(); + } + + return springValueDefinitions; + } + + private void processPropertyValues(BeanDefinitionRegistry beanRegistry) { + if (!PROPERTY_VALUES_PROCESSED_BEAN_FACTORIES.add(beanRegistry)) { + // already initialized + return; + } + + if (!beanName2SpringValueDefinitions.containsKey(beanRegistry)) { + beanName2SpringValueDefinitions.put(beanRegistry, LinkedListMultimap.create()); + } + + Multimap springValueDefinitions = beanName2SpringValueDefinitions.get(beanRegistry); + + String[] beanNames = beanRegistry.getBeanDefinitionNames(); + for (String beanName : beanNames) { + BeanDefinition beanDefinition = beanRegistry.getBeanDefinition(beanName); + MutablePropertyValues mutablePropertyValues = beanDefinition.getPropertyValues(); + List propertyValues = mutablePropertyValues.getPropertyValueList(); + for (PropertyValue propertyValue : propertyValues) { + Object value = propertyValue.getValue(); + if (!(value instanceof TypedStringValue)) { + continue; + } + String placeholder = ((TypedStringValue) value).getValue(); + Set keys = placeholderHelper.extractPlaceholderKeys(placeholder); + + if (keys.isEmpty()) { + continue; + } + + for (String key : keys) { + springValueDefinitions.put(beanName, new SpringValueDefinition(key, placeholder, propertyValue.getName())); + } + } + } + } +} \ No newline at end of file diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueRegistry.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueRegistry.java new file mode 100644 index 000000000..e27d88f26 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/spring/property/SpringValueRegistry.java @@ -0,0 +1,102 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.polaris.config.spring.property; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.tencent.polaris.client.util.NamedThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.BeanFactory; + +/** + *@author : wh + *@date : 2022/7/10 14:29 + *@description: + */ +public class SpringValueRegistry { + private static final Logger logger = LoggerFactory.getLogger(SpringValueRegistry.class); + + private static final long CLEAN_INTERVAL_IN_SECONDS = 5; + private final Map> registry = Maps.newConcurrentMap(); + private final AtomicBoolean initialized = new AtomicBoolean(false); + private final Object LOCK = new Object(); + + public void register(BeanFactory beanFactory, String key, SpringValue springValue) { + if (!registry.containsKey(beanFactory)) { + synchronized (LOCK) { + if (!registry.containsKey(beanFactory)) { + registry.put(beanFactory, Multimaps.synchronizedListMultimap(LinkedListMultimap.create())); + } + } + } + + registry.get(beanFactory).put(key, springValue); + + // lazy initialize + if (initialized.compareAndSet(false, true)) { + initialize(); + } + } + + public Collection get(BeanFactory beanFactory, String key) { + Multimap beanFactorySpringValues = registry.get(beanFactory); + if (beanFactorySpringValues == null) { + return null; + } + return beanFactorySpringValues.get(key); + } + + private void initialize() { + Executors.newSingleThreadScheduledExecutor( + new NamedThreadFactory("polaris-spring-value-registry")).scheduleAtFixedRate( + () -> { + try { + scanAndClean(); + } + catch (Throwable ex) { + logger.error(ex.getMessage(), ex); + } + }, CLEAN_INTERVAL_IN_SECONDS, CLEAN_INTERVAL_IN_SECONDS, TimeUnit.SECONDS); + } + + private void scanAndClean() { + Iterator> iterator = registry.values().iterator(); + while (!Thread.currentThread().isInterrupted() && iterator.hasNext()) { + Multimap springValues = iterator.next(); + Iterator> springValueIterator = springValues.entries().iterator(); + while (springValueIterator.hasNext()) { + Map.Entry springValue = springValueIterator.next(); + if (!springValue.getValue().isTargetBeanValid()) { + // clear unused spring values + springValueIterator.remove(); + } + } + } + } +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/MockedConfigChange.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/MockedConfigChange.java new file mode 100644 index 000000000..1de1b4bf7 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/MockedConfigChange.java @@ -0,0 +1,19 @@ +package com.tencent.cloud.polaris.config.adapter; + +/** + *@author : wh + *@date : 2022/7/10 14:50 + *@description: + */ +public class MockedConfigChange { + + private String k1; + + String getK1() { + return k1; + } + + void setK1(String k1) { + this.k1 = k1; + } +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertiesSourceAutoRefresherTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertiesSourceAutoRefresherTest.java index 779ddfb47..b25a6cfb4 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertiesSourceAutoRefresherTest.java +++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/PolarisPropertiesSourceAutoRefresherTest.java @@ -18,11 +18,17 @@ package com.tencent.cloud.polaris.config.adapter; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.Map; import com.google.common.collect.Lists; import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; +import com.tencent.cloud.polaris.config.spring.property.PlaceholderHelper; +import com.tencent.cloud.polaris.config.spring.property.SpringValue; +import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry; import com.tencent.polaris.configuration.api.core.ChangeType; import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeEvent; import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; @@ -32,8 +38,13 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.context.ConfigurableApplicationContext; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -55,10 +66,33 @@ public class PolarisPropertiesSourceAutoRefresherTest { @Mock private ContextRefresher contextRefresher; + @Mock + private SpringValueRegistry springValueRegistry; + + @Mock + private PlaceholderHelper placeholderHelper; + @Test - public void testConfigFileChanged() { + public void testConfigFileChanged() throws Exception{ PolarisPropertySourceAutoRefresher refresher = new PolarisPropertySourceAutoRefresher(polarisConfigProperties, - polarisPropertySourceManager, contextRefresher); + polarisPropertySourceManager, springValueRegistry, placeholderHelper); + + ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class); + ConfigurableListableBeanFactory beanFactory = mock(ConfigurableListableBeanFactory.class); + TypeConverter typeConverter = mock(TypeConverter.class); + when(beanFactory.getTypeConverter()).thenReturn(typeConverter); + when(applicationContext.getBeanFactory()).thenReturn(beanFactory); + refresher.setApplicationContext(applicationContext); + when(typeConverter.convertIfNecessary(any(), any(), (Field) any())).thenReturn("v11"); + Collection springValues = new ArrayList<>(); + MockedConfigChange mockedConfigChange = new MockedConfigChange(); + mockedConfigChange.setK1("v1"); + Field field = mockedConfigChange.getClass().getDeclaredField("k1"); + SpringValue springValue = new SpringValue("v1", "placeholder", mockedConfigChange, "mockedConfigChange", field, false); + + springValues.add(springValue); + + when(springValueRegistry.get(any(), any())).thenReturn(springValues); when(polarisConfigProperties.isAutoRefresh()).thenReturn(true); @@ -91,31 +125,4 @@ public class PolarisPropertiesSourceAutoRefresherTest { Assert.assertEquals("v4", polarisPropertySource.getProperty("k4")); verify(contextRefresher).refresh(); } - - @Test - public void testNewConfigFile() { - PolarisPropertySourceAutoRefresher refresher = new PolarisPropertySourceAutoRefresher(polarisConfigProperties, - polarisPropertySourceManager, contextRefresher); - - when(polarisConfigProperties.isAutoRefresh()).thenReturn(true); - - Map emptyContent = new HashMap<>(); - MockedConfigKVFile file = new MockedConfigKVFile(emptyContent); - PolarisPropertySource polarisPropertySource = new PolarisPropertySource(testNamespace, testServiceName, testFileName, - file, emptyContent); - - when(polarisPropertySourceManager.getAllPropertySources()).thenReturn(Lists.newArrayList(polarisPropertySource)); - - ConfigPropertyChangeInfo changeInfo = new ConfigPropertyChangeInfo("k1", null, "v1", ChangeType.ADDED); - Map changeInfos = new HashMap<>(); - changeInfos.put("k1", changeInfo); - - ConfigKVFileChangeEvent event = new ConfigKVFileChangeEvent(changeInfos); - refresher.onApplicationEvent(null); - - file.fireChangeListener(event); - - Assert.assertEquals("v1", polarisPropertySource.getProperty("k1")); - verify(contextRefresher).refresh(); - } } diff --git a/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/util/JacksonUtils.java b/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/util/JacksonUtils.java index 4ed4d2ea1..ea0483707 100644 --- a/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/util/JacksonUtils.java +++ b/spring-cloud-tencent-commons/src/main/java/com/tencent/cloud/common/util/JacksonUtils.java @@ -84,4 +84,14 @@ public final class JacksonUtils { throw new RuntimeException("Json to map failed.", e); } } + + public static T json2JavaBean(String content, Class valueType) { + try { + return OM.readValue(content, valueType); + } + catch (Exception e) { + LOG.error("json {} to class {} failed. ", content, valueType, e); + throw new RuntimeException("json to class failed.", e); + } + } }