Unverified Commit 40abd759 by Jason Song Committed by GitHub

Merge pull request #989 from zhangzheng88/json-supported

1. Support injecting Json values with @ApolloJsonValue, the supported format is the same as Spring @Value, such as @ApolloJsonValue("${someJsonPropertyKey}") 2. Support auto updating injected values for @ApolloJsonValue
parents a4a1d3a9 5f5f45a3
......@@ -9,6 +9,7 @@ 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.spring.property.SpringValueRegistry;
import com.ctrip.framework.apollo.tracer.Tracer;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.ctrip.framework.apollo.util.http.HttpUtil;
......@@ -64,6 +65,7 @@ public class DefaultInjector implements Injector {
bind(RemoteConfigLongPollService.class).in(Singleton.class);
bind(PlaceholderHelper.class).in(Singleton.class);
bind(ConfigPropertySourceFactory.class).in(Singleton.class);
bind(SpringValueRegistry.class).in(Singleton.class);
}
}
}
package com.ctrip.framework.apollo.spring.annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
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.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.google.common.base.Preconditions;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;
/**
* Apollo Annotation Processor for Spring Application
*
* @author Jason Song(song_s@ctrip.com)
*/
public class ApolloAnnotationProcessor implements BeanPostProcessor, PriorityOrdered {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Class clazz = bean.getClass();
processFields(bean, clazz.getDeclaredFields());
processMethods(bean, clazz.getDeclaredMethods());
return bean;
}
public class ApolloAnnotationProcessor extends ApolloProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
protected void processField(Object bean, String beanName, Field field) {
ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class);
if (annotation == null) {
return;
}
private void processFields(Object bean, Field[] declaredFields) {
for (Field field : declaredFields) {
ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class);
if (annotation == null) {
continue;
}
Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()),
"Invalid type: %s for field: %s, should be Config", field.getType(), field);
Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()),
"Invalid type: %s for field: %s, should be Config", field.getType(), field);
String namespace = annotation.value();
Config config = ConfigService.getConfig(namespace);
String namespace = annotation.value();
Config config = ConfigService.getConfig(namespace);
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, bean, config);
}
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, bean, config);
@Override
protected void processMethod(final Object bean, String beanName, 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);
private void processMethods(final Object bean, Method[] declaredMethods) {
for (final Method method : declaredMethods) {
ApolloConfigChangeListener annotation = AnnotationUtils.findAnnotation(method, ApolloConfigChangeListener.class);
if (annotation == null) {
continue;
ReflectionUtils.makeAccessible(method);
String[] namespaces = annotation.value();
ConfigChangeListener configChangeListener = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
ReflectionUtils.invokeMethod(method, bean, changeEvent);
}
};
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();
for (String namespace : namespaces) {
Config config = ConfigService.getConfig(namespace);
for (String namespace : namespaces) {
Config config = ConfigService.getConfig(namespace);
config.addChangeListener(new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
ReflectionUtils.invokeMethod(method, bean, changeEvent);
}
});
}
config.addChangeListener(configChangeListener);
}
}
@Override
public int getOrder() {
//make it as late as possible
return Ordered.LOWEST_PRECEDENCE;
}
}
......@@ -34,5 +34,8 @@ public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(), SpringValueProcessor.class);
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueDefinitionProcessor.class.getName(), SpringValueDefinitionProcessor.class);
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloJsonValueProcessor.class.getName(),
ApolloJsonValueProcessor.class);
}
}
package com.ctrip.framework.apollo.spring.annotation;
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;
/**
* Use this annotation to inject json property from Apollo, support the same format as Spring @Value.
*
* <p>Usage example:</p>
* <pre class="code">
* // Inject the json property value for type SomeObject.
* // Suppose SomeObject has 2 properties, someString and someInt, then the possible config
* // in Apollo is someJsonPropertyKey={"someString":"someValue", "someInt":10}.
* &#064;ApolloJsonValue("${someJsonPropertyKey:someDefaultValue}")
* private SomeObject someObject;
* </pre>
*
* Create by zhangzheng on 2018/3/6
*
* @see org.springframework.beans.factory.annotation.Value
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@Documented
public @interface ApolloJsonValue {
/**
* The actual value expression: e.g. "${someJsonPropertyKey:someDefaultValue}".
*/
String value();
}
package com.ctrip.framework.apollo.spring.annotation;
import com.ctrip.framework.apollo.build.ApolloInjector;
import com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener;
import com.ctrip.framework.apollo.spring.property.PlaceholderHelper;
import com.ctrip.framework.apollo.spring.property.SpringValue;
import com.ctrip.framework.apollo.spring.property.SpringValueRegistry;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.google.common.base.Preconditions;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Set;
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.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;
/**
* Create by zhangzheng on 2018/2/6
*/
public class ApolloJsonValueProcessor extends ApolloProcessor implements BeanFactoryAware {
private static final Logger logger = LoggerFactory.getLogger(ApolloJsonValueProcessor.class);
private static final Gson gson = new Gson();
private final ConfigUtil configUtil;
private final PlaceholderHelper placeholderHelper;
private final SpringValueRegistry springValueRegistry;
private ConfigurableBeanFactory beanFactory;
public ApolloJsonValueProcessor() {
configUtil = ApolloInjector.getInstance(ConfigUtil.class);
placeholderHelper = ApolloInjector.getInstance(PlaceholderHelper.class);
springValueRegistry = ApolloInjector.getInstance(SpringValueRegistry.class);
}
@Override
protected void processField(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(beanFactory, 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<String> keys = placeholderHelper.extractPlaceholderKeys(placeholder);
for (String key : keys) {
SpringValue springValue = new SpringValue(key, placeholder, bean, beanName, field, true);
springValueRegistry.register(key, springValue);
logger.debug("Monitoring {}", springValue);
}
}
}
@Override
protected void processMethod(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(beanFactory, 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<String> keys = placeholderHelper.extractPlaceholderKeys(placeHolder);
for (String key : keys) {
SpringValue springValue = new SpringValue(key, apolloJsonValue.value(), bean, beanName,
method, true);
springValueRegistry.register(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.beanFactory = (ConfigurableBeanFactory) beanFactory;
}
}
package com.ctrip.framework.apollo.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;
/**
* Create by zhangzheng on 2018/2/6
*/
public abstract class ApolloProcessor 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<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;
}
}
......@@ -2,6 +2,7 @@ package com.ctrip.framework.apollo.spring.config;
import com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor;
import com.ctrip.framework.apollo.spring.property.SpringValueDefinitionProcessor;
import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValueProcessor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
......@@ -25,6 +26,8 @@ public class ConfigPropertySourcesProcessor extends PropertySourcesProcessor
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(),
ApolloAnnotationProcessor.class);
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(), SpringValueProcessor.class);
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloJsonValueProcessor.class.getName(),
ApolloJsonValueProcessor.class);
processSpringValueDefinition(registry);
}
......
package com.ctrip.framework.apollo.spring.config;
import com.ctrip.framework.apollo.build.ApolloInjector;
import com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
......@@ -8,6 +10,8 @@ import com.google.common.collect.Multimap;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigService;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
......@@ -33,9 +37,11 @@ import java.util.Iterator;
*/
public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, PriorityOrdered {
private static final Multimap<Integer, String> NAMESPACE_NAMES = LinkedHashMultimap.create();
private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);
private final ConfigPropertySourceFactory configPropertySourceFactory = ApolloInjector
.getInstance(ConfigPropertySourceFactory.class);
private final ConfigUtil configUtil = ApolloInjector.getInstance(ConfigUtil.class);
private ConfigurableEnvironment environment;
public static boolean addNamespaces(Collection<String> namespaces, int order) {
......@@ -44,10 +50,14 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
initializePropertySources();
if (INITIALIZED.compareAndSet(false, true)) {
initializePropertySources();
initializeAutoUpdatePropertiesFeature(beanFactory);
}
}
protected void initializePropertySources() {
private void initializePropertySources() {
if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME)) {
//already initialized
return;
......@@ -77,6 +87,20 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
}
}
private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) {
if (!configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) {
return;
}
AutoUpdateConfigChangeListener autoUpdateConfigChangeListener = new AutoUpdateConfigChangeListener(
environment, beanFactory);
List<ConfigPropertySource> configPropertySources = configPropertySourceFactory.getAllConfigPropertySources();
for (ConfigPropertySource configPropertySource : configPropertySources) {
configPropertySource.addChangeListener(autoUpdateConfigChangeListener);
}
}
@Override
public void setEnvironment(Environment environment) {
//it is safe enough to cast as all known environment is derived from ConfigurableEnvironment
......@@ -86,6 +110,7 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
//only for test
private static void reset() {
NAMESPACE_NAMES.clear();
INITIALIZED.set(false);
}
@Override
......
package com.ctrip.framework.apollo.spring.property;
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.annotation.SpringValueProcessor;
import com.google.gson.Gson;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.env.Environment;
import org.springframework.util.CollectionUtils;
/**
* Create by zhangzheng on 2018/3/6
*/
public class AutoUpdateConfigChangeListener implements ConfigChangeListener{
private static final Logger logger = LoggerFactory.getLogger(SpringValueProcessor.class);
private final boolean typeConverterHasConvertIfNecessaryWithFieldParameter;
private final Environment environment;
private final ConfigurableBeanFactory beanFactory;
private final TypeConverter typeConverter;
private final PlaceholderHelper placeholderHelper;
private final SpringValueRegistry springValueRegistry;
private final Gson gson;
public AutoUpdateConfigChangeListener(Environment environment, ConfigurableListableBeanFactory beanFactory){
this.typeConverterHasConvertIfNecessaryWithFieldParameter = testTypeConverterHasConvertIfNecessaryWithFieldParameter();
this.beanFactory = beanFactory;
this.typeConverter = this.beanFactory.getTypeConverter();
this.environment = environment;
this.placeholderHelper = ApolloInjector.getInstance(PlaceholderHelper.class);
this.springValueRegistry = ApolloInjector.getInstance(SpringValueRegistry.class);
this.gson = new Gson();
}
@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 = springValueRegistry.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);
}
}
}
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) {
// value will never be null, as @Value and @ApolloJsonValue will not allow that
Object value = placeholderHelper
.resolvePropertyValue(beanFactory, springValue.getBeanName(), springValue.getPlaceholder());
if (springValue.isJson()) {
value = parseJsonValue((String)value, springValue.getGenericType());
} else {
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 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;
}
}
private boolean testTypeConverterHasConvertIfNecessaryWithFieldParameter() {
try {
TypeConverter.class.getMethod("convertIfNecessary", Object.class, Class.class, Field.class);
} catch (Throwable ex) {
return false;
}
return true;
}
}
......@@ -4,18 +4,15 @@ import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import java.util.Set;
import java.util.Stack;
import org.springframework.beans.factory.BeanFactory;
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;
/**
* Extract keys from placeholder, e.g.
* <ul>
* <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>
* </ul>
* Placeholder helper functions.
*/
public class PlaceholderHelper {
......@@ -26,6 +23,45 @@ public class PlaceholderHelper {
private static final String EXPRESSION_PREFIX = "#{";
private static final String EXPRESSION_SUFFIX = "}";
/**
* Resolve placeholder property values, e.g.
* <br />
* <br />
* "${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.
* <ul>
* <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>
* </ul>
*/
public Set<String> extractPlaceholderKeys(String propertyString) {
Set<String> placeholderKeys = Sets.newHashSet();
......
......@@ -3,6 +3,7 @@ package com.ctrip.framework.apollo.spring.property;
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;
/**
......@@ -20,17 +21,23 @@ public class SpringValue {
private String key;
private String placeholder;
private Class<?> targetType;
private Type genericType;
private boolean isJson;
public SpringValue(String key, String placeholder, Object bean, String beanName, Field field) {
public SpringValue(String key, String placeholder, Object bean, String beanName, Field field, boolean isJson) {
this.bean = 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) {
public SpringValue(String key, String placeholder, Object bean, String beanName, Method method, boolean isJson) {
this.bean = bean;
this.beanName = beanName;
this.methodParameter = new MethodParameter(method, 0);
......@@ -38,6 +45,10 @@ public class SpringValue {
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 {
......@@ -84,6 +95,14 @@ public class SpringValue {
return field;
}
public Type getGenericType() {
return genericType;
}
public boolean isJson() {
return isJson;
}
@Override
public String toString() {
if (isField()) {
......
package com.ctrip.framework.apollo.spring.property;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
import java.util.Collection;
public class SpringValueRegistry {
private final Multimap<String, SpringValue> registry = LinkedListMultimap.create();
public void register(String key, SpringValue springValue) {
registry.put(key, springValue);
}
public Collection<SpringValue> get(String key) {
return registry.get(key);
}
}
......@@ -59,5 +59,6 @@ public class MockInjector implements Injector {
public static void reset() {
classMap.clear();
classTable.clear();
delegate = null;
}
}
......@@ -5,6 +5,7 @@ import static org.mockito.Mockito.when;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.internals.ConfigRepository;
import com.ctrip.framework.apollo.internals.DefaultInjector;
import com.ctrip.framework.apollo.internals.SimpleConfig;
import com.ctrip.framework.apollo.spring.property.SpringValueDefinitionProcessor;
import com.ctrip.framework.apollo.util.ConfigUtil;
......@@ -119,6 +120,7 @@ public abstract class AbstractSpringIntegrationTest {
ReflectionUtils.invokeMethod(CONFIG_SERVICE_RESET, null);
MockInjector.reset();
MockInjector.setInstance(ConfigManager.class, new MockConfigManager());
MockInjector.setDelegate(new DefaultInjector());
}
protected static void doTearDown() {
......
......@@ -2,13 +2,23 @@ package com.ctrip.framework.apollo.spring;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.ctrip.framework.apollo.build.MockInjector;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.internals.SimpleConfig;
import com.ctrip.framework.apollo.spring.JavaConfigPlaceholderTest.JsonBean;
import com.ctrip.framework.apollo.spring.XmlConfigPlaceholderTest.TestXmlBean;
import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.google.common.primitives.Ints;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
......@@ -21,14 +31,6 @@ import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ImportResource;
import org.springframework.stereotype.Component;
import com.ctrip.framework.apollo.build.MockInjector;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.internals.SimpleConfig;
import com.ctrip.framework.apollo.spring.XmlConfigPlaceholderTest.TestXmlBean;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.google.common.primitives.Ints;
public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrationTest {
private static final String TIMEOUT_PROPERTY = "timeout";
......@@ -624,6 +626,8 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
boolean someNewBoolean = !someBoolean;
String someString = "someString";
String someNewString = "someNewString";
String someJsonProperty = "[{\"a\":\"astring\", \"b\":10},{\"a\":\"astring2\", \"b\":20}]";
String someNewJsonProperty = "[{\"a\":\"newString\", \"b\":20},{\"a\":\"astring2\", \"b\":20}]";
String someDateFormat = "yyyy-MM-dd HH:mm:ss.SSS";
Date someDate = assembleDate(2018, 2, 23, 20, 1, 2, 123);
......@@ -642,6 +646,7 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
properties.setProperty("stringProperty", String.valueOf(someString));
properties.setProperty("dateFormat", String.valueOf(someDateFormat));
properties.setProperty("dateProperty", simpleDateFormat.format(someDate));
properties.setProperty("jsonProperty", someJsonProperty);
SimpleConfig config = prepareConfig(ConfigConsts.NAMESPACE_APPLICATION, properties);
......@@ -659,6 +664,8 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
assertEquals(someBoolean, bean.getBooleanProperty());
assertEquals(someString, bean.getStringProperty());
assertEquals(someDate, bean.getDateProperty());
assertEquals("astring", bean.getJsonBeanList().get(0).getA());
assertEquals(10, bean.getJsonBeanList().get(0).getB());
Properties newProperties = new Properties();
newProperties.setProperty("intProperty", String.valueOf(someNewInt));
......@@ -672,6 +679,7 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
newProperties.setProperty("stringProperty", String.valueOf(someNewString));
newProperties.setProperty("dateFormat", String.valueOf(someDateFormat));
newProperties.setProperty("dateProperty", simpleDateFormat.format(someNewDate));
newProperties.setProperty("jsonProperty", someNewJsonProperty);
config.onRepositoryChange(ConfigConsts.NAMESPACE_APPLICATION, newProperties);
......@@ -687,6 +695,92 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
assertEquals(someNewBoolean, bean.getBooleanProperty());
assertEquals(someNewString, bean.getStringProperty());
assertEquals(someNewDate, bean.getDateProperty());
assertEquals("newString", bean.getJsonBeanList().get(0).getA());
assertEquals(20, bean.getJsonBeanList().get(0).getB());
}
@Test
public void testAutoUpdateJsonValueWithInvalidValue() throws Exception {
String someValidValue = "{\"a\":\"someString\", \"b\":10}";
String someInvalidValue = "someInvalidValue";
Properties properties = assembleProperties("jsonProperty", someValidValue);
SimpleConfig config = prepareConfig(ConfigConsts.NAMESPACE_APPLICATION, properties);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig10.class);
TestApolloJsonValue bean = context.getBean(TestApolloJsonValue.class);
JsonBean jsonBean = bean.getJsonBean();
assertEquals("someString", jsonBean.getA());
assertEquals(10, jsonBean.getB());
Properties newProperties = assembleProperties("jsonProperty", someInvalidValue);
config.onRepositoryChange(ConfigConsts.NAMESPACE_APPLICATION, newProperties);
TimeUnit.MILLISECONDS.sleep(50);
// should not change anything
assertTrue(jsonBean == bean.getJsonBean());
}
@Test
public void testAutoUpdateJsonValueWithNoValueAndNoDefaultValue() throws Exception {
String someValidValue = "{\"a\":\"someString\", \"b\":10}";
Properties properties = assembleProperties("jsonProperty", someValidValue);
SimpleConfig config = prepareConfig(ConfigConsts.NAMESPACE_APPLICATION, properties);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig10.class);
TestApolloJsonValue bean = context.getBean(TestApolloJsonValue.class);
JsonBean jsonBean = bean.getJsonBean();
assertEquals("someString", jsonBean.getA());
assertEquals(10, jsonBean.getB());
Properties newProperties = new Properties();
config.onRepositoryChange(ConfigConsts.NAMESPACE_APPLICATION, newProperties);
TimeUnit.MILLISECONDS.sleep(50);
// should not change anything
assertTrue(jsonBean == bean.getJsonBean());
}
@Test
public void testAutoUpdateJsonValueWithNoValueAndDefaultValue() throws Exception {
String someValidValue = "{\"a\":\"someString\", \"b\":10}";
Properties properties = assembleProperties("jsonProperty", someValidValue);
SimpleConfig config = prepareConfig(ConfigConsts.NAMESPACE_APPLICATION, properties);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig11.class);
TestApolloJsonValueWithDefaultValue bean = context.getBean(TestApolloJsonValueWithDefaultValue.class);
JsonBean jsonBean = bean.getJsonBean();
assertEquals("someString", jsonBean.getA());
assertEquals(10, jsonBean.getB());
Properties newProperties = new Properties();
config.onRepositoryChange(ConfigConsts.NAMESPACE_APPLICATION, newProperties);
TimeUnit.MILLISECONDS.sleep(50);
JsonBean newJsonBean = bean.getJsonBean();
assertEquals("defaultString", newJsonBean.getA());
assertEquals(1, newJsonBean.getB());
}
@Configuration
......@@ -805,6 +899,26 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
}
}
@Configuration
@EnableApolloConfig
static class AppConfig10 {
@Bean
TestApolloJsonValue testApolloJsonValue() {
return new TestApolloJsonValue();
}
}
@Configuration
@EnableApolloConfig
static class AppConfig11 {
@Bean
TestApolloJsonValueWithDefaultValue testApolloJsonValue() {
return new TestApolloJsonValueWithDefaultValue();
}
}
static class TestJavaConfigBean {
@Value("${timeout:100}")
......@@ -964,6 +1078,9 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
@Value("#{new java.text.SimpleDateFormat('${dateFormat}').parse('${dateProperty}')}")
private Date dateProperty;
@ApolloJsonValue("${jsonProperty}")
private List<JsonBean> jsonBeanList;
public int getIntProperty() {
return intProperty;
}
......@@ -1003,5 +1120,30 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
public Date getDateProperty() {
return dateProperty;
}
public List<JsonBean> getJsonBeanList() {
return jsonBeanList;
}
}
static class TestApolloJsonValue {
@ApolloJsonValue("${jsonProperty}")
private JsonBean jsonBean;
public JsonBean getJsonBean() {
return jsonBean;
}
}
static class TestApolloJsonValueWithDefaultValue {
@ApolloJsonValue("${jsonProperty:{\"a\":\"defaultString\", \"b\":1}}")
private JsonBean jsonBean;
public JsonBean getJsonBean() {
return jsonBean;
}
}
}
......@@ -6,7 +6,13 @@ import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import java.util.List;
import org.junit.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
......@@ -14,10 +20,6 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Component;
......@@ -30,6 +32,8 @@ public class JavaConfigPlaceholderTest extends AbstractSpringIntegrationTest {
private static final String BATCH_PROPERTY = "batch";
private static final int DEFAULT_BATCH = 200;
private static final String FX_APOLLO_NAMESPACE = "FX.apollo";
private static final String JSON_PROPERTY = "jsonProperty";
private static final String OTHER_JSON_PROPERTY = "otherJsonProperty";
@Test
public void testPropertySourceWithNoNamespace() throws Exception {
......@@ -281,6 +285,53 @@ public class JavaConfigPlaceholderTest extends AbstractSpringIntegrationTest {
assertEquals(someValue, bean.getNestedProperty());
}
@Test
public void testApolloJsonValue() {
String someJson = "[{\"a\":\"astring\", \"b\":10},{\"a\":\"astring2\", \"b\":20}]";
String otherJson = "[{\"a\":\"otherString\", \"b\":10},{\"a\":\"astring2\", \"b\":20}]";
Config config = mock(Config.class);
when(config.getProperty(eq(JSON_PROPERTY), anyString())).thenReturn(someJson);
when(config.getProperty(eq(OTHER_JSON_PROPERTY), anyString())).thenReturn(otherJson);
when(config.getProperty(eq("a"), anyString())).thenReturn(JSON_PROPERTY);
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, config);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
AppConfig8.class);
TestJsonPropertyBean testJsonPropertyBean = context.getBean(TestJsonPropertyBean.class);
assertEquals(2, testJsonPropertyBean.getJsonBeanList().size());
assertEquals("astring", testJsonPropertyBean.getJsonBeanList().get(0).getA());
assertEquals(10, testJsonPropertyBean.getJsonBeanList().get(0).getB());
assertEquals("astring2", testJsonPropertyBean.getJsonBeanList().get(1).getA());
assertEquals(20, testJsonPropertyBean.getJsonBeanList().get(1).getB());
assertEquals(testJsonPropertyBean.getJsonBeanList(), testJsonPropertyBean.getEmbeddedJsonBeanList());
assertEquals("otherString", testJsonPropertyBean.getOtherJsonBeanList().get(0).getA());
assertEquals(10, testJsonPropertyBean.getOtherJsonBeanList().get(0).getB());
assertEquals("astring2", testJsonPropertyBean.getOtherJsonBeanList().get(1).getA());
assertEquals(20, testJsonPropertyBean.getOtherJsonBeanList().get(1).getB());
}
@Test(expected = BeanCreationException.class)
public void testApolloJsonValueWithInvalidJson() throws Exception {
String someInvalidJson = "someInvalidJson";
Config config = mock(Config.class);
when(config.getProperty(eq(JSON_PROPERTY), anyString())).thenReturn(someInvalidJson);
when(config.getProperty(eq(OTHER_JSON_PROPERTY), anyString())).thenReturn(someInvalidJson);
when(config.getProperty(eq("a"), anyString())).thenReturn(JSON_PROPERTY);
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, config);
new AnnotationConfigApplicationContext(AppConfig8.class).getBean(TestJsonPropertyBean.class);
}
@Test(expected = BeanCreationException.class)
public void testApolloJsonValueWithNoPropertyValue() throws Exception {
Config config = mock(Config.class);
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, config);
new AnnotationConfigApplicationContext(AppConfig8.class);
}
private void check(int expectedTimeout, int expectedBatch, Class<?>... annotatedClasses) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(annotatedClasses);
......@@ -363,6 +414,17 @@ public class JavaConfigPlaceholderTest extends AbstractSpringIntegrationTest {
}
}
@Configuration
@EnableApolloConfig
static class AppConfig8 {
@Bean
TestJsonPropertyBean testJavaConfigBean() {
return new TestJsonPropertyBean();
}
}
@Component
static class TestJavaConfigBean {
@Value("${timeout:100}")
private int timeout;
......@@ -434,4 +496,80 @@ public class JavaConfigPlaceholderTest extends AbstractSpringIntegrationTest {
}
}
static class TestJsonPropertyBean {
@ApolloJsonValue("${jsonProperty}")
private List<JsonBean> jsonBeanList;
private List<JsonBean> otherJsonBeanList;
@ApolloJsonValue("${${a}}")
private List<JsonBean> embeddedJsonBeanList;
public List<JsonBean> getJsonBeanList() {
return jsonBeanList;
}
@ApolloJsonValue("${otherJsonProperty}")
public void setOtherJsonBeanList(List<JsonBean> otherJsonBeanList) {
this.otherJsonBeanList = otherJsonBeanList;
}
public List<JsonBean> getOtherJsonBeanList() {
return otherJsonBeanList;
}
public List<JsonBean> getEmbeddedJsonBeanList() {
return embeddedJsonBeanList;
}
}
static class JsonBean {
private String a;
private int b;
String getA() {
return a;
}
public void setA(String a) {
this.a = a;
}
int getB() {
return b;
}
public void setB(int b) {
this.b = b;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
JsonBean jsonBean = (JsonBean) o;
if (b != jsonBean.b) {
return false;
}
return a != null ? a.equals(jsonBean.a) : jsonBean.a == null;
}
@Override
public int hashCode() {
int result = a != null ? a.hashCode() : 0;
result = 31 * result + b;
return result;
}
}
}
package com.ctrip.framework.apollo.demo.spring.common.bean;
import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
......@@ -14,6 +16,15 @@ public class AnnotatedBean {
private int timeout;
private int batch;
private List<JsonBean> jsonBeans;
/**
* ApolloJsonValue annotated on fields example, the default value is specified as empty list - []
* <br />
* jsonBeanProperty=[{"someString":"hello","someInt":100},{"someString":"world!","someInt":200}]
*/
@ApolloJsonValue("${jsonBeanProperty:[]}")
private List<JsonBean> anotherJsonBeans;
@Value("${batch:100}")
public void setBatch(int batch) {
......@@ -27,8 +38,33 @@ public class AnnotatedBean {
this.timeout = timeout;
}
/**
* ApolloJsonValue annotated on methods example, the default value is specified as empty list - []
* <br />
* jsonBeanProperty=[{"someString":"hello","someInt":100},{"someString":"world!","someInt":200}]
*/
@ApolloJsonValue("${jsonBeanProperty:[]}")
public void setJsonBeans(List<JsonBean> jsonBeans) {
logger.info("updating json beans, old value: {}, new value: {}", this.jsonBeans, jsonBeans);
this.jsonBeans = jsonBeans;
}
@Override
public String toString() {
return String.format("[AnnotatedBean] timeout: %d, batch: %d", timeout, batch);
return String.format("[AnnotatedBean] timeout: %d, batch: %d, jsonBeans: %s", timeout, batch, jsonBeans);
}
private static class JsonBean{
private String someString;
private int someInt;
@Override
public String toString() {
return "JsonBean{" +
"someString='" + someString + '\'' +
", someInt=" + someInt +
'}';
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment