Optimized configuration refresh mechanism. (#527)

pull/531/head
Haotian Zhang 2 years ago committed by GitHub
parent 4e8d4bba1e
commit bfb307515d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -20,3 +20,4 @@
- [BeanFactoryUtils returns all beans including beans defined in ancestor bean factories](https://github.com/Tencent/spring-cloud-tencent/pull/516)
- [Feature: Add router actuate endpoint](https://github.com/Tencent/spring-cloud-tencent/pull/519)
- [Code optimization for rpc-enhancement module](https://github.com/Tencent/spring-cloud-tencent/pull/525)
- [Feature: Optimized configuration update](https://github.com/Tencent/spring-cloud-tencent/pull/527)

@ -20,12 +20,21 @@ package com.tencent.cloud.polaris.config;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceAutoRefresher;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceManager;
import com.tencent.cloud.polaris.config.adapter.SmartConfigurationPropertiesRebinder;
import com.tencent.cloud.polaris.config.annotation.PolarisConfigAnnotationProcessor;
import com.tencent.cloud.polaris.config.condition.ConditionalOnNonDefaultBehavior;
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.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.cloud.context.properties.ConfigurationPropertiesBeans;
import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -42,10 +51,13 @@ public class PolarisConfigAutoConfiguration {
@Bean
public PolarisPropertySourceAutoRefresher polarisPropertySourceAutoRefresher(
PolarisConfigProperties polarisConfigProperties, PolarisPropertySourceManager polarisPropertySourceManager,
PolarisConfigProperties polarisConfigProperties,
PolarisPropertySourceManager polarisPropertySourceManager,
SpringValueRegistry springValueRegistry,
PlaceholderHelper placeholderHelper,
ContextRefresher contextRefresher) {
return new PolarisPropertySourceAutoRefresher(polarisConfigProperties, polarisPropertySourceManager,
contextRefresher);
return new PolarisPropertySourceAutoRefresher(polarisConfigProperties,
polarisPropertySourceManager, springValueRegistry, placeholderHelper, contextRefresher);
}
@Bean
@ -57,4 +69,30 @@ 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);
}
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
@ConditionalOnNonDefaultBehavior
public ConfigurationPropertiesRebinder smartConfigurationPropertiesRebinder(
ConfigurationPropertiesBeans beans) {
// If using default behavior, not use SmartConfigurationPropertiesRebinder.
// Minimize te possibility of making mistakes.
return new SmartConfigurationPropertiesRebinder(beans);
}
}

@ -19,6 +19,8 @@ package com.tencent.cloud.polaris.config;
import com.tencent.cloud.polaris.config.adapter.PolarisConfigFileLocator;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceManager;
import com.tencent.cloud.polaris.config.adapter.SmartConfigurationPropertiesRebinder;
import com.tencent.cloud.polaris.config.condition.ConditionalOnNonDefaultBehavior;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
@ -27,7 +29,11 @@ import com.tencent.polaris.client.api.SDKContext;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import com.tencent.polaris.configuration.factory.ConfigFileServiceFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.cloud.context.properties.ConfigurationPropertiesBeans;
import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@ -79,4 +85,14 @@ public class PolarisConfigBootstrapAutoConfiguration {
PolarisContextProperties polarisContextProperties) {
return new ConfigurationModifier(polarisConfigProperties, polarisContextProperties);
}
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
@ConditionalOnNonDefaultBehavior
public ConfigurationPropertiesRebinder smartConfigurationPropertiesRebinder(
ConfigurationPropertiesBeans beans) {
// If using default behavior, not use SmartConfigurationPropertiesRebinder.
// Minimize te possibility of making mistakes.
return new SmartConfigurationPropertiesRebinder(beans);
}
}

@ -18,20 +18,34 @@
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.polaris.configuration.api.core.ConfigKVFileChangeEvent;
import com.tencent.cloud.polaris.config.enums.RefreshType;
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.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.NonNull;
import org.springframework.util.CollectionUtils;
@ -41,7 +55,8 @@ import org.springframework.util.CollectionUtils;
*
* @author lepdou 2022-03-28
*/
public class PolarisPropertySourceAutoRefresher implements ApplicationListener<ApplicationReadyEvent> {
public class PolarisPropertySourceAutoRefresher
implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware, BeanFactoryAware {
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisPropertySourceAutoRefresher.class);
@ -50,14 +65,33 @@ public class PolarisPropertySourceAutoRefresher implements ApplicationListener<A
private final PolarisPropertySourceManager polarisPropertySourceManager;
private final ContextRefresher contextRefresher;
private final AtomicBoolean registered = new AtomicBoolean(false);
public PolarisPropertySourceAutoRefresher(PolarisConfigProperties polarisConfigProperties,
PolarisPropertySourceManager polarisPropertySourceManager, ContextRefresher contextRefresher) {
private final SpringValueRegistry springValueRegistry;
private final PlaceholderHelper placeholderHelper;
private ConfigurableApplicationContext context;
private TypeConverter typeConverter;
private ConfigurableBeanFactory beanFactory;
public PolarisPropertySourceAutoRefresher(
PolarisConfigProperties polarisConfigProperties,
PolarisPropertySourceManager polarisPropertySourceManager,
SpringValueRegistry springValueRegistry,
PlaceholderHelper placeholderHelper,
ContextRefresher contextRefresher) {
this.polarisConfigProperties = polarisConfigProperties;
this.polarisPropertySourceManager = polarisPropertySourceManager;
this.springValueRegistry = springValueRegistry;
this.placeholderHelper = placeholderHelper;
this.contextRefresher = contextRefresher;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = (ConfigurableApplicationContext) applicationContext;
this.beanFactory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
this.typeConverter = this.beanFactory.getTypeConverter();
}
@Override
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
registerPolarisConfigPublishEvent();
@ -79,38 +113,105 @@ public class PolarisPropertySourceAutoRefresher implements ApplicationListener<A
// register polaris config publish event
for (PolarisPropertySource polarisPropertySource : polarisPropertySources) {
polarisPropertySource.getConfigKVFile().addChangeListener(new ConfigKVFileChangeListener() {
@Override
public void onChange(ConfigKVFileChangeEvent configKVFileChangeEvent) {
LOGGER.info(
"[SCT Config] received polaris config change event and will refresh spring context."
+ " namespace = {}, group = {}, fileName = {}",
polarisPropertySource.getNamespace(), polarisPropertySource.getGroup(),
polarisPropertySource.getFileName());
Map<String, Object> 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;
polarisPropertySource.getConfigKVFile()
.addChangeListener((ConfigKVFileChangeListener) configKVFileChangeEvent -> {
LOGGER.info(
"[SCT Config] received polaris config change event and will refresh spring context."
+ " namespace = {}, group = {}, fileName = {}",
polarisPropertySource.getNamespace(),
polarisPropertySource.getGroup(),
polarisPropertySource.getFileName());
Map<String, Object> 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;
}
if (polarisConfigProperties.getRefreshType() == RefreshType.REFLECT) {
Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, changedKey);
if (targetValues == null || targetValues.isEmpty()) {
continue;
}
// update the attribute with @Value annotation
for (SpringValue val : targetValues) {
updateSpringValue(val);
}
}
}
if (polarisConfigProperties.getRefreshType() == RefreshType.REFLECT) {
// update @ConfigurationProperties beans
context.publishEvent(new EnvironmentChangeEvent(context, configKVFileChangeEvent.changedKeys()));
}
}
else {
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;
}
// rebuild beans with @RefreshScope annotation
contextRefresher.refresh();
}
});
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;
}
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (ConfigurableBeanFactory) beanFactory;
}
}

@ -0,0 +1,122 @@
/*
* 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.adapter;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.tencent.cloud.polaris.config.enums.RefreshBehavior;
import org.springframework.beans.BeansException;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.properties.ConfigurationPropertiesBeans;
import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;
import static com.tencent.cloud.polaris.config.condition.NonDefaultBehaviorCondition.POLARIS_CONFIG_REFRESH_BEHAVIOR;
import static com.tencent.cloud.polaris.config.enums.RefreshBehavior.ALL_BEANS;
/**
* Extend {@link ConfigurationPropertiesRebinder}.
* <p>
* Spring team doesn't seem to support single {@link ConfigurationPropertiesBean} refresh.
* <p>
* SmartConfigurationPropertiesRebinder can refresh specific
* {@link ConfigurationPropertiesBean} base on the change keys.
* <p>
* <strong> NOTE: We still use Spring's default behavior (full refresh) as default
* behavior, This feature can be considered an advanced feature, it may not be as stable
* as the default behavior. </strong>
* <code><a href=https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/refresh/SmartConfigurationPropertiesRebinder.java>
* SmartConfigurationPropertiesRebinder</a></code>
*
* @author weihubeats 2022-7-10
*/
public class SmartConfigurationPropertiesRebinder extends ConfigurationPropertiesRebinder {
private static final String BEANS = "beans";
private Map<String, ConfigurationPropertiesBean> beanMap;
private ApplicationContext applicationContext;
private RefreshBehavior refreshBehavior;
public SmartConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
super(beans);
fillBeanMap(beans);
}
@SuppressWarnings("unchecked")
private void fillBeanMap(ConfigurationPropertiesBeans beans) {
this.beanMap = new HashMap<>();
Field field = ReflectionUtils.findField(beans.getClass(), BEANS);
if (field != null) {
field.setAccessible(true);
this.beanMap.putAll((Map<String, ConfigurationPropertiesBean>) Optional
.ofNullable(ReflectionUtils.getField(field, beans))
.orElse(Collections.emptyMap()));
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
super.setApplicationContext(applicationContext);
this.applicationContext = applicationContext;
this.refreshBehavior = this.applicationContext.getEnvironment().getProperty(
POLARIS_CONFIG_REFRESH_BEHAVIOR, RefreshBehavior.class,
ALL_BEANS);
}
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
switch (refreshBehavior) {
case SPECIFIC_BEAN:
rebindSpecificBean(event);
break;
default:
rebind();
break;
}
}
}
private void rebindSpecificBean(EnvironmentChangeEvent event) {
Set<String> refreshedSet = new HashSet<>();
beanMap.forEach((name, bean) -> event.getKeys().forEach(changeKey -> {
String prefix = AnnotationUtils.getValue(bean.getAnnotation()).toString();
// prevent multiple refresh one ConfigurationPropertiesBean.
if (changeKey.startsWith(prefix) && refreshedSet.add(name)) {
rebind(name);
}
}));
}
}

@ -40,7 +40,7 @@ import static com.tencent.cloud.polaris.config.listener.PolarisConfigListenerCon
/**
* {@link PolarisConfigAnnotationProcessor} implementation for spring .
* <p>Refer to the Apollo project implementation
* <p>This source file was reference from
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/annotation/ApolloAnnotationProcessor.java>
* ApolloAnnotationProcessor</a></code>
* @author <a href="mailto:iskp.me@gmail.com">Palmer Xu</a> 2022-06-07

@ -26,7 +26,7 @@ import java.lang.annotation.Target;
/**
* Configuring the change listener annotation.
* <p>Refer to the Apollo project implementation
* <p>This source file was reference from
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/annotation/ApolloAnnotationProcessor.java>
* ApolloAnnotationProcessor</a></code>
* @author Palmer Xu 2022-05-31

@ -0,0 +1,40 @@
/*
* 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.condition;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
/**
* custom annotation.
*
* @author weihubeats 2022-7-13
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(NonDefaultBehaviorCondition.class)
public @interface ConditionalOnNonDefaultBehavior {
}

@ -0,0 +1,56 @@
/*
* 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.condition;
import com.tencent.cloud.polaris.config.enums.RefreshBehavior;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* Extend SpringBootCondition.
*
* @author weihubeats 2022-7-13
*/
public class NonDefaultBehaviorCondition extends SpringBootCondition {
/**
* refresh behavior config.
*/
public static final String POLARIS_CONFIG_REFRESH_BEHAVIOR = "spring.cloud.polaris.config.refresh-behavior";
/**
* refresh behavior config default value.
*/
private static final RefreshBehavior DEFAULT_REFRESH_BEHAVIOR = RefreshBehavior.ALL_BEANS;
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
RefreshBehavior behavior = context.getEnvironment().getProperty(
POLARIS_CONFIG_REFRESH_BEHAVIOR, RefreshBehavior.class,
DEFAULT_REFRESH_BEHAVIOR);
if (DEFAULT_REFRESH_BEHAVIOR == behavior) {
return ConditionOutcome.noMatch("no matched");
}
return ConditionOutcome.match("matched");
}
}

@ -19,6 +19,8 @@ package com.tencent.cloud.polaris.config.config;
import java.util.List;
import com.tencent.cloud.polaris.config.enums.RefreshType;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
@ -50,6 +52,11 @@ public class PolarisConfigProperties {
*/
private boolean autoRefresh = true;
/**
* Attribute refresh type.
*/
private RefreshType refreshType = RefreshType.REFRESH_CONTEXT;
/**
* List of injected configuration files.
*/
@ -87,6 +94,14 @@ public class PolarisConfigProperties {
this.autoRefresh = autoRefresh;
}
public RefreshType getRefreshType() {
return refreshType;
}
public void setRefreshType(RefreshType refreshType) {
this.refreshType = refreshType;
}
public List<ConfigFileGroup> getGroups() {
return groups;
}

@ -0,0 +1,39 @@
/*
* 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.enums;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
/**
* Refresh behavior.
*
* @author weihubeats 2022-7-13
*/
public enum RefreshBehavior {
/**
* Refresh all {@link ConfigurationPropertiesBean}s.
*/
ALL_BEANS,
/**
* Refresh specific {@link ConfigurationPropertiesBean} base on change key.
*/
SPECIFIC_BEAN,
}

@ -0,0 +1,34 @@
/*
* 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.enums;
/**
* Attribute refresh type when config updated.
* @author lepdou 2022-07-26
*/
public enum RefreshType {
/**
* refresh spring context.
*/
REFRESH_CONTEXT,
/**
* Dynamically refresh a bean's properties via reflection calls.
*/
REFLECT
}

@ -49,7 +49,7 @@ import static com.tencent.polaris.configuration.api.core.ChangeType.MODIFIED;
/**
* Polaris Config Listener Context Defined .
* <p>Refer to the Apollo project implementation
* <p>This source file was reference from
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfig.java>
* AbstractConfig</a></code>
* @author Palmer Xu 2022-06-06

@ -0,0 +1,73 @@
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;
/**
* Get spring bean properties and methods.
*
* @author weihubeats 2022-7-10
*/
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.
* @param bean bean
* @param beanName beanName
* @param field field
*/
protected abstract void processField(Object bean, String beanName, Field field);
/**
* subclass should implement this method to process method.
* @param bean bean
* @param beanName beanName
* @param method 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<Field> findAllField(Class clazz) {
final List<Field> res = new LinkedList<>();
ReflectionUtils.doWithFields(clazz, field -> res.add(field));
return res;
}
private List<Method> findAllMethod(Class clazz) {
final List<Method> res = new LinkedList<>();
ReflectionUtils.doWithMethods(clazz, method -> res.add(method));
return res;
}
}

@ -0,0 +1,172 @@
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;
/**
* Spring value processor of field or method which has @Value and xml config placeholders.
* <br/>
* <br/>
* This source file was originally from:
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/annotation/SpringValueProcessor.java>
* SpringValueProcessor</a></code>
*
* @author weihubeats 2022-7-10
*/
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<String, SpringValueDefinition> 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<String> 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("Polaris @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<SpringValueDefinition> 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;
}
}

@ -0,0 +1,195 @@
/*
* 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;
/**
* Placeholder helper functions.
* <br/>
* <br/>
* This source file was originally from:
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/property/PlaceholderHelper.java>
* PlaceholderHelper</a></code>
*
* @author weihubeats 2022-7-10
*/
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.
* @param beanFactory beanFactory
* @param beanName beanName
* @param placeholder placeholder
* @return "${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));
}
/**
*
* @param propertyString propertyString
* @return
* Extract keys from placeholder, e.g.
* <li>${some.key} => "some.key"</li>
* <li>${some.key:${some.other.key:100}} => "some.key", "some.other.key"</li>
* <li>${${some.key}} => "some.key"</li>
* <li>${${some.key:other.key}} => "some.key"</li>
* <li>${${some.key}:${another.key}} => "some.key", "another.key"</li>
* <li>#{new java.text.SimpleDateFormat('${some.key}').parse('${another.key}')} => "some.key", "another.key"</li>
*/
public Set<String> extractPlaceholderKeys(String propertyString) {
Set<String> placeholderKeys = Sets.newHashSet();
if (Strings.isNullOrEmpty(propertyString) || (!isNormalizedPlaceholder(propertyString) && !isExpressionWithPlaceholder(propertyString))) {
return placeholderKeys;
}
Stack<String> 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;
}
}

@ -0,0 +1,156 @@
/*
* 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;
/**
* Spring @Value method info.
* <br/>
* <br/>
* This source file was originally from:
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/property/SpringValue.java>
* SpringValue</a></code>
*
* @author weihubeats 2022-7-10
*/
public class SpringValue {
private MethodParameter methodParameter;
private Field field;
private final WeakReference<Object> 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());
}
}

@ -0,0 +1,54 @@
/*
* 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;
/**
* Spring value.
* <br/>
* <br/>
* This source file was originally from:
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/property/SpringValueDefinition.java>
* SpringValueDefinition</a></code>
*
* @author weihubeats 2022-7-10
*/
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;
}
}

@ -0,0 +1,125 @@
/*
* 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;
/**
* To process xml config placeholders, e.g.
*
* <pre>
* &lt;bean class=&quot;com.demo.bean.XmlBean&quot;&gt;
* &lt;property name=&quot;timeout&quot; value=&quot;${timeout:200}&quot;/&gt;
* &lt;property name=&quot;batch&quot; value=&quot;${batch:100}&quot;/&gt;
* &lt;/bean&gt;
* </pre>
*
* This source file was originally from:
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/property/SpringValueDefinitionProcessor.java>
* SpringValueDefinitionProcessor</a></code>
*
* @author weihubeats 2022-7-10
*/
public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPostProcessor {
private static final Map<BeanDefinitionRegistry, Multimap<String, SpringValueDefinition>> beanName2SpringValueDefinitions =
Maps.newConcurrentMap();
private static final Set<BeanDefinitionRegistry> 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<String, SpringValueDefinition> getBeanName2SpringValueDefinitions(BeanDefinitionRegistry registry) {
Multimap<String, SpringValueDefinition> 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<String, SpringValueDefinition> springValueDefinitions = beanName2SpringValueDefinitions.get(beanRegistry);
String[] beanNames = beanRegistry.getBeanDefinitionNames();
for (String beanName : beanNames) {
BeanDefinition beanDefinition = beanRegistry.getBeanDefinition(beanName);
MutablePropertyValues mutablePropertyValues = beanDefinition.getPropertyValues();
List<PropertyValue> propertyValues = mutablePropertyValues.getPropertyValueList();
for (PropertyValue propertyValue : propertyValues) {
Object value = propertyValue.getValue();
if (!(value instanceof TypedStringValue)) {
continue;
}
String placeholder = ((TypedStringValue) value).getValue();
Set<String> keys = placeholderHelper.extractPlaceholderKeys(placeholder);
if (keys.isEmpty()) {
continue;
}
for (String key : keys) {
springValueDefinitions.put(beanName, new SpringValueDefinition(key, placeholder, propertyValue.getName()));
}
}
}
}
}

@ -0,0 +1,107 @@
/*
* 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;
/**
* Spring value auto registry.
* <br/>
* <br/>
* This source file was originally from:
* <code><a href=https://github.com/apolloconfig/apollo/blob/master/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/property/SpringValueRegistry.java>
* SpringValueRegistry</a></code>
*
* @author weihubeats 2022-7-10
*/
public class SpringValueRegistry {
private static final Logger logger = LoggerFactory.getLogger(SpringValueRegistry.class);
private static final long CLEAN_INTERVAL_IN_SECONDS = 5;
private final Map<BeanFactory, Multimap<String, SpringValue>> 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<SpringValue> get(BeanFactory beanFactory, String key) {
Multimap<String, SpringValue> 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<Multimap<String, SpringValue>> iterator = registry.values().iterator();
while (!Thread.currentThread().isInterrupted() && iterator.hasNext()) {
Multimap<String, SpringValue> springValues = iterator.next();
Iterator<Map.Entry<String, SpringValue>> springValueIterator = springValues.entries().iterator();
while (springValueIterator.hasNext()) {
Map.Entry<String, SpringValue> springValue = springValueIterator.next();
if (!springValue.getValue().isTargetBeanValid()) {
// clear unused spring values
springValueIterator.remove();
}
}
}
}
}

@ -41,6 +41,18 @@
"defaultValue": "true",
"description": "Whether to connect to a remote server, suitable for local development mode.",
"sourceType": "com.tencent.cloud.polaris.config.config.PolarisConfigProperties"
},
{
"name": "spring.cloud.polaris.config.refresh-behavior",
"type": "com.tencent.cloud.polaris.config.enums.RefreshBehavior",
"defaultValue": "all_beans",
"description": "ConfigurationPropertiesBean refresh behavior."
},
{
"name": "spring.cloud.polaris.config.refresh-type",
"type": "com.tencent.cloud.polaris.config.enums.RefreshType",
"defaultValue": "refresh_context",
"description": "Attribute refresh type when config updated. refresh_context or reflect."
}
]
}

@ -0,0 +1,19 @@
package com.tencent.cloud.polaris.config.adapter;
/**
* Mock config kv file for test.
*
* @author weihubeats 2022-7-10
*/
public class MockedConfigChange {
private String k1;
String getK1() {
return k1;
}
void setK1(String k1) {
this.k1 = k1;
}
}

@ -18,11 +18,18 @@
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.enums.RefreshType;
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,9 +39,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.Mockito.verify;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
@ -55,10 +66,34 @@ 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, contextRefresher);
when(polarisConfigProperties.getRefreshType()).thenReturn(RefreshType.REFLECT);
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<SpringValue> 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);
@ -89,33 +124,5 @@ public class PolarisPropertiesSourceAutoRefresherTest {
Assert.assertEquals("v3", polarisPropertySource.getProperty("k3"));
Assert.assertNull(polarisPropertySource.getProperty("k2"));
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<String, Object> 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<String, ConfigPropertyChangeInfo> 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();
}
}

@ -101,4 +101,14 @@ public final class JacksonUtils {
throw new RuntimeException("Json to map failed.", e);
}
}
public static <T> T json2JavaBean(String content, Class<T> 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);
}
}
}

@ -12,6 +12,8 @@ spring:
groups:
- name: ${spring.application.name} # group name
files: [ "config/application.properties", "config/bootstrap.yml" ] # config/application.properties takes precedence over config/bootstrap.yml
refresh-behavior: SPECIFIC_BEAN
refresh-type: REFLECT
management:
endpoints:
web:

Loading…
Cancel
Save