Skip to content

Commit

Permalink
Merge pull request #972 from nobodyiam/auto-update-value-merge
Browse files Browse the repository at this point in the history
Auto Updating Spring Placeholder Values
  • Loading branch information
lepdou authored Feb 24, 2018
2 parents 7edfb1a + 3efd4a3 commit 5823daa
Show file tree
Hide file tree
Showing 42 changed files with 2,841 additions and 166 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.ctrip.framework.apollo.spi.DefaultConfigFactory;
import com.ctrip.framework.apollo.spi.DefaultConfigFactoryManager;
import com.ctrip.framework.apollo.spi.DefaultConfigRegistry;
import com.ctrip.framework.apollo.spring.config.ConfigPropertySourceFactory;
import com.ctrip.framework.apollo.spring.property.PlaceholderHelper;
import com.ctrip.framework.apollo.tracer.Tracer;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.ctrip.framework.apollo.util.http.HttpUtil;
Expand Down Expand Up @@ -60,6 +62,8 @@ protected void configure() {
bind(HttpUtil.class).in(Singleton.class);
bind(ConfigServiceLocator.class).in(Singleton.class);
bind(RemoteConfigLongPollService.class).in(Singleton.class);
bind(PlaceholderHelper.class).in(Singleton.class);
bind(ConfigPropertySourceFactory.class).in(Singleton.class);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ctrip.framework.apollo.spring.annotation;

import com.ctrip.framework.apollo.spring.property.SpringValueDefinitionProcessor;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
Expand Down Expand Up @@ -30,5 +31,8 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B

BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(),
ApolloAnnotationProcessor.class);

BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(), SpringValueProcessor.class);
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueDefinitionProcessor.class.getName(), SpringValueDefinitionProcessor.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
package com.ctrip.framework.apollo.spring.annotation;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
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.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.env.Environment;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;

import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.build.ApolloInjector;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.config.ConfigPropertySource;
import com.ctrip.framework.apollo.spring.config.ConfigPropertySourceFactory;
import com.ctrip.framework.apollo.spring.property.PlaceholderHelper;
import com.ctrip.framework.apollo.spring.property.SpringValue;
import com.ctrip.framework.apollo.spring.property.SpringValueDefinition;
import com.ctrip.framework.apollo.spring.property.SpringValueDefinitionProcessor;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;

/**
* Spring value processor of field or method which has @Value and xml config placeholders.
*
* @author github.com/zhegexiaohuozi seimimaster@gmail.com
* @since 2017/12/20.
*/
public class SpringValueProcessor implements BeanPostProcessor, PriorityOrdered, EnvironmentAware,
BeanFactoryAware, BeanFactoryPostProcessor {

private static final Logger logger = LoggerFactory.getLogger(SpringValueProcessor.class);

private final Multimap<String, SpringValue> monitor = LinkedListMultimap.create();
private final ConfigUtil configUtil;
private final PlaceholderHelper placeholderHelper;
private final ConfigPropertySourceFactory configPropertySourceFactory;
private final boolean typeConverterHasConvertIfNecessaryWithFieldParameter;

private Environment environment;
private ConfigurableBeanFactory beanFactory;
private TypeConverter typeConverter;

private static Multimap<String, SpringValueDefinition> beanName2SpringValueDefinitions = LinkedListMultimap.create();

public SpringValueProcessor() {
configUtil = ApolloInjector.getInstance(ConfigUtil.class);
placeholderHelper = ApolloInjector.getInstance(PlaceholderHelper.class);
configPropertySourceFactory = ApolloInjector.getInstance(ConfigPropertySourceFactory.class);
typeConverterHasConvertIfNecessaryWithFieldParameter = testTypeConverterHasConvertIfNecessaryWithFieldParameter();
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (ConfigurableBeanFactory) beanFactory;
this.typeConverter = this.beanFactory.getTypeConverter();
}

@Override
public void setEnvironment(Environment env) {
this.environment = env;
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) {
beanName2SpringValueDefinitions = SpringValueDefinitionProcessor.getBeanName2SpringValueDefinitions();
registerConfigChangeListener();
}
}

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) {
Class clazz = bean.getClass();
processFields(bean, beanName, findAllField(clazz));
processMethods(bean, beanName, findAllMethod(clazz));
processBeanPropertyValues(bean, beanName);
}
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

private void processFields(Object bean, String beanName, List<Field> declaredFields) {
for (Field field : declaredFields) {
// register @Value on field
Value value = field.getAnnotation(Value.class);
if (value == null) {
continue;
}
Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());

if (keys.isEmpty()) {
continue;
}

for (String key : keys) {
SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field);
monitor.put(key, springValue);
logger.debug("Monitoring {}", springValue);
}
}
}

private void processMethods(final Object bean, String beanName, List<Method> declaredMethods) {
for (final Method method : declaredMethods) {
//register @Value on method
Value value = method.getAnnotation(Value.class);
if (value == null) {
continue;
}
//skip Configuration bean methods
if (method.getAnnotation(Bean.class) != null) {
continue;
}
if (method.getParameterTypes().length != 1) {
logger.error("Ignore @Value setter {}.{}, expecting 1 parameter, actual {} parameters",
bean.getClass().getName(), method.getName(), method.getParameterTypes().length);
continue;
}

Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());

if (keys.isEmpty()) {
continue;
}

for (String key : keys) {
SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, method);
monitor.put(key, springValue);
logger.debug("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);
monitor.put(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);
}

private List<Field> findAllField(Class clazz) {
final List<Field> res = new LinkedList<>();
ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
res.add(field);
}
});
return res;
}

private List<Method> findAllMethod(Class clazz) {
final List<Method> res = new LinkedList<>();
ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
res.add(method);
}
});
return res;
}

private void registerConfigChangeListener() {
ConfigChangeListener changeListener = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
Set<String> keys = changeEvent.changedKeys();
if (CollectionUtils.isEmpty(keys)) {
return;
}
for (String key : keys) {
// 1. check whether the changed key is relevant
Collection<SpringValue> targetValues = monitor.get(key);
if (targetValues == null || targetValues.isEmpty()) {
continue;
}

// 2. check whether the value is really changed or not (since spring property sources have hierarchies)
ConfigChange configChange = changeEvent.getChange(key);
if (!Objects.equals(environment.getProperty(key), configChange.getNewValue())) {
continue;
}

// 3. update the value
for (SpringValue val : targetValues) {
updateSpringValue(val);
}
}
}
};

List<ConfigPropertySource> configPropertySources = configPropertySourceFactory.getAllConfigPropertySources();

for (ConfigPropertySource configPropertySource : configPropertySources) {
configPropertySource.addChangeListener(changeListener);
}
}

private void updateSpringValue(SpringValue springValue) {
try {
Object value = resolvePropertyValue(springValue);
springValue.update(value);

logger.debug("Auto update apollo changed value successfully, new value: {}, {}", value,
springValue.toString());
} catch (Throwable ex) {
logger.error("Auto update apollo 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) {
String strVal = beanFactory.resolveEmbeddedValue(springValue.getPlaceholder());
Object value;

BeanDefinition bd = (beanFactory.containsBean(springValue.getBeanName()) ? beanFactory
.getMergedBeanDefinition(springValue.getBeanName()) : null);
value = evaluateBeanDefinitionString(strVal, bd);

if (springValue.isField()) {
// org.springframework.beans.TypeConverter#convertIfNecessary(java.lang.Object, java.lang.Class, java.lang.reflect.Field) is available from Spring 3.2.0+
if (typeConverterHasConvertIfNecessaryWithFieldParameter) {
value = this.typeConverter
.convertIfNecessary(value, springValue.getTargetType(), springValue.getField());
} else {
value = this.typeConverter.convertIfNecessary(value, springValue.getTargetType());
}
} else {
value = this.typeConverter.convertIfNecessary(value, springValue.getTargetType(),
springValue.getMethodParameter());
}

return value;
}

private Object evaluateBeanDefinitionString(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));
}

private boolean testTypeConverterHasConvertIfNecessaryWithFieldParameter() {
try {
TypeConverter.class.getMethod("convertIfNecessary", Object.class, Class.class, Field.class);
} catch (Throwable ex) {
return false;
}

return true;
}

@Override
public int getOrder() {
//make it as late as possible
return Ordered.LOWEST_PRECEDENCE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.build.ApolloInjector;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.spring.config.ConfigPropertySourceFactory;
import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
import com.ctrip.framework.apollo.spring.config.ConfigPropertySource;
import com.google.common.base.Splitter;
Expand Down Expand Up @@ -39,6 +41,9 @@ public class ApolloSpringApplicationRunListener implements SpringApplicationRunL
private static final Logger logger = LoggerFactory.getLogger(ApolloSpringApplicationRunListener.class);
private static final Splitter NAMESPACE_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults();

private final ConfigPropertySourceFactory configPropertySourceFactory = ApolloInjector
.getInstance(ConfigPropertySourceFactory.class);

public ApolloSpringApplicationRunListener(SpringApplication application, String[] args) {
//ignore
}
Expand Down Expand Up @@ -74,7 +79,7 @@ public void contextPrepared(ConfigurableApplicationContext context) {
for (String namespace : namespaceList) {
Config config = ConfigService.getConfig(namespace);

composite.addPropertySource(new ConfigPropertySource(namespace, config));
composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
}

environment.getPropertySources().addFirst(composite);
Expand Down
Loading

0 comments on commit 5823daa

Please sign in to comment.