Commit 21a8d3ba by xiaohuo Committed by nobodyiam

Update field or method which has @Value annotation automatically

parent 7edfb1aa
......@@ -30,5 +30,7 @@ public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(),
ApolloAnnotationProcessor.class);
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(),SpringValueProcessor.class);
}
}
package com.ctrip.framework.apollo.spring.annotation;
import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.auto.SpringFieldValue;
import com.ctrip.framework.apollo.spring.auto.SpringMethodValue;
import com.ctrip.framework.apollo.spring.auto.SpringValue;
import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor;
import com.ctrip.framework.foundation.Foundation;
import com.ctrip.framework.foundation.spi.provider.ApplicationProvider;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.EnvironmentAware;
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 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 java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Spring value processor of field or method which has @Value.
*
* @author github.com/zhegexiaohuozi seimimaster@gmail.com
* @since 2017/12/20.
*/
public class SpringValueProcessor implements BeanPostProcessor, PriorityOrdered, EnvironmentAware {
private Pattern pattern = Pattern.compile("\\$\\{([^:]*)\\}:?(.*)");
private static Multimap<String, SpringValue> monitor = LinkedListMultimap.create();
private static ApplicationProvider applicationProvider = Foundation.app();
private Environment environment;
private Logger logger = LoggerFactory.getLogger(SpringValueProcessor.class);
public static Multimap<String, SpringValue> monitor() {
return monitor;
}
public static boolean enable(){
return applicationProvider.isAutoUpdateEnable();
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
boolean enabled = enable();
if (enabled){
Class clazz = bean.getClass();
processFields(bean, findAllField(clazz));
processMethods(bean, findAllMethod(clazz));
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
private void processFields(Object bean, List<Field> declaredFields) {
for (Field field : declaredFields) {
// regist @Value on field
Value value = field.getAnnotation(Value.class);
if (value == null) {
continue;
}
Matcher matcher = pattern.matcher(value.value());
if (matcher.matches()) {
String key = matcher.group(1);
monitor.put(key, SpringFieldValue.create(key,bean, field));
logger.info("Listening apollo key = {}", key);
}
}
}
private void processMethods(final Object bean, List<Method> declaredMethods) {
for (final Method method : declaredMethods) {
//regist @Value on method
Value value = method.getAnnotation(Value.class);
if (value == null) {
continue;
}
Matcher matcher = pattern.matcher(value.value());
if (matcher.matches()) {
String key = matcher.group(1);
monitor.put(key, SpringMethodValue.create(key,bean, method));
logger.info("Listening apollo key = {}", key);
}
}
}
@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;
}
@Override
public void setEnvironment(Environment env) {
this.environment = env;
PropertySourcesProcessor.registerListener(new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
Set<String> keys = changeEvent.changedKeys();
if (CollectionUtils.isEmpty(keys)) {
return;
}
if (!SpringValueProcessor.enable()) {
return;
}
for (String k : keys) {
ConfigChange configChange = changeEvent.getChange(k);
if (!Objects.equals(environment.getProperty(k), configChange.getNewValue())) {
continue;
}
Collection<SpringValue> targetValues = SpringValueProcessor.monitor().get(k);
if (targetValues == null || targetValues.isEmpty()) {
continue;
}
for (SpringValue val : targetValues) {
val.updateVal(environment.getProperty(k));
}
}
}
});
}
}
package com.ctrip.framework.apollo.spring.auto;
import java.lang.reflect.Field;
/**
* Spring @Value field info
* @author github.com/zhegexiaohuozi seimimaster@gmail.com
* @since 2018/2/6.
*/
public class SpringFieldValue extends SpringValue {
private Field field;
private SpringFieldValue(String key, Object ins, Field field) {
super();
this.bean = ins;
this.className = ins.getClass().getName();
this.fieldName = field.getName();
this.field = field;
this.parser = findParser(field.getType());
this.valKey = key;
}
public static SpringFieldValue create(String key, Object ins, Field field) {
return new SpringFieldValue(key, ins, field);
}
@Override
public void updateVal(String newVal) {
try {
boolean accessible = field.isAccessible();
field.setAccessible(true);
field.set(bean, parseVal(newVal));
field.setAccessible(accessible);
logger.info("auto update apollo changed value, key={}, newVal={} in {}.{}", valKey, newVal, className, fieldName);
} catch (Exception e) {
logger.error("update field {}.{} fail with new val={},key = {}, msg = {}", className, fieldName, newVal, valKey, e.getMessage());
}
}
}
package com.ctrip.framework.apollo.spring.auto;
import java.lang.reflect.Method;
/**
* Spring @Value method info
* @author github.com/zhegexiaohuozi seimimaster@gmail.com
* @since 2018/2/6.
*/
public class SpringMethodValue extends SpringValue {
private Method method;
private SpringMethodValue(String key, Object ins, Method method) {
this.bean = ins;
this.method = method;
this.className = ins.getClass().getName();
this.fieldName = method.getName() + "(*)";
Class<?>[] paramTps = method.getParameterTypes();
if (paramTps.length != 1) {
logger.error("invalid setter,can not update in {}.{}", className, fieldName);
return;
}
this.parser = findParser(paramTps[0]);
this.valKey = key;
}
public static SpringMethodValue create(String key, Object ins, Method method) {
return new SpringMethodValue(key, ins, method);
}
@Override
public void updateVal(String newVal) {
try {
Class<?>[] paramTps = method.getParameterTypes();
if (paramTps.length != 1) {
logger.error("invalid setter ,can not update key={} val={} in {}.{}", valKey, newVal, className, fieldName);
return;
}
method.invoke(bean, parseVal(newVal));
logger.info("auto update apollo changed value, key={}, newVal={} in {}.{}", valKey, newVal, className, fieldName);
} catch (Exception e) {
logger.error("update field {}.{} fail with new val={},key = {}, msg = {}", className, fieldName, newVal, valKey, e.getMessage());
}
}
}
package com.ctrip.framework.apollo.spring.auto;
import com.ctrip.framework.apollo.util.function.Functions;
import com.google.common.base.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
/**
* Spring @Value field and method common info
*
* @author github.com/zhegexiaohuozi seimimaster@gmail.com
* @since 2017/12/20.
*/
public abstract class SpringValue {
protected Object bean;
String className;
String fieldName;
String valKey;
protected Function<String, ?> parser;
protected Logger logger = LoggerFactory.getLogger(getClass());
public abstract void updateVal(String newVal);
Object parseVal(String newVal) {
if (parser == null) {
return newVal;
}
return parser.apply(newVal);
}
Function<String, ?> findParser(Class<?> targetType) {
Function<String, ?> res = null;
if (targetType.equals(String.class)) {
return null;
} else if (targetType.equals(int.class) || targetType.equals(Integer.class)) {
res = Functions.TO_INT_FUNCTION;
} else if (targetType.equals(long.class) || targetType.equals(Long.class)) {
res = Functions.TO_LONG_FUNCTION;
} else if (targetType.equals(boolean.class) || targetType.equals(Boolean.class)) {
res = Functions.TO_BOOLEAN_FUNCTION;
} else if (targetType.equals(Date.class)) {
res = Functions.TO_DATE_FUNCTION;
} else if (targetType.equals(short.class) || targetType.equals(Short.class)) {
res = Functions.TO_SHORT_FUNCTION;
} else if (targetType.equals(double.class) || targetType.equals(Double.class)) {
res = Functions.TO_DOUBLE_FUNCTION;
} else if (targetType.equals(float.class) || targetType.equals(Float.class)) {
res = Functions.TO_FLOAT_FUNCTION;
} else if (targetType.equals(byte.class) || targetType.equals(Byte.class)) {
res = Functions.TO_BYTE_FUNCTION;
}
return res;
}
}
package com.ctrip.framework.apollo.spring.config;
import com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
......@@ -22,5 +23,6 @@ public class ConfigPropertySourcesProcessor extends PropertySourcesProcessor
PropertySourcesPlaceholderConfigurer.class);
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(),
ApolloAnnotationProcessor.class);
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(),SpringValueProcessor.class);
}
}
package com.ctrip.framework.apollo.spring.config;
import com.google.common.collect.HashMultimap;
import com.ctrip.framework.apollo.ConfigChangeListener;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
......@@ -19,6 +22,7 @@ import org.springframework.core.env.Environment;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
/**
* Apollo Property Sources processor for Spring Annotation Based Application. <br /> <br />
......@@ -31,6 +35,7 @@ import java.util.Iterator;
* @author Jason Song(song_s@ctrip.com)
*/
public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, PriorityOrdered {
private static final List<Config> ALL_CONFIG = Lists.newLinkedList();
private static final Multimap<Integer, String> NAMESPACE_NAMES = LinkedHashMultimap.create();
private ConfigurableEnvironment environment;
......@@ -39,6 +44,12 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
return NAMESPACE_NAMES.putAll(order, namespaces);
}
public static void registerListener(ConfigChangeListener configChangeListener){
for(Config config:ALL_CONFIG){
config.addChangeListener(configChangeListener);
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
initializePropertySources();
......@@ -59,7 +70,7 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
int order = iterator.next();
for (String namespace : NAMESPACE_NAMES.get(order)) {
Config config = ConfigService.getConfig(namespace);
ALL_CONFIG.add(config);
composite.addPropertySource(new ConfigPropertySource(namespace, config));
}
}
......
......@@ -88,7 +88,8 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
TestApolloConfigChangeListenerBean1 bean = getBean(TestApolloConfigChangeListenerBean1.class, AppConfig3.class);
assertEquals(3, applicationListeners.size());
//PropertySourcesProcessor add listeners to listen config changed of all namespace
assertEquals(4, applicationListeners.size());
assertEquals(1, fxApolloListeners.size());
for (ConfigChangeListener listener : applicationListeners) {
......
......@@ -86,7 +86,8 @@ public class XMLConfigAnnotationTest extends AbstractSpringIntegrationTest {
TestApolloConfigChangeListenerBean1 bean = getBean("spring/XmlConfigAnnotationTest3.xml",
TestApolloConfigChangeListenerBean1.class);
assertEquals(3, applicationListeners.size());
//PropertySourcesProcessor add listeners to listen config changed of all namespace
assertEquals(4, applicationListeners.size());
assertEquals(1, fxApolloListeners.size());
for (ConfigChangeListener listener : applicationListeners) {
......
package com.ctrip.framework.apollo.spring.auto;
import com.ctrip.framework.apollo.enums.PropertyChangeType;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.util.function.Functions;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* SpringValue Tester.
*
* @author github.com/zhegexiaohuozi seimimaster@gmail.com
* @version 1.0
*/
public class SpringValueTest {
private SpringValue defaultVal;
private ConfigChange testTarget;
//SpringValueProcessor.pattern
private Pattern pattern = Pattern.compile("\\$\\{([^:]*)\\}:?(.*)");
@Before
public void before() throws Exception {
Field field = ConfigChange.class.getDeclaredField("newValue");
field.setAccessible(true);
testTarget = new ConfigChange("test","test","testO","testN", PropertyChangeType.MODIFIED);
defaultVal = SpringFieldValue.create("test",testTarget,field);
}
/**
* Method: updateVal(String newVal)
*/
@Test
public void testUpdateVal() throws Exception {
defaultVal.updateVal("testUp");
Assert.assertEquals("testUp",testTarget.getNewValue());
}
/**
* Method: findParser(Class<?> targetType)
*/
@Test
public void testFindParser() throws Exception {
Method findParser = SpringValue.class.getDeclaredMethod("findParser",Class.class);
findParser.setAccessible(true);
Assert.assertNull(findParser.invoke(defaultVal,String.class));
Assert.assertEquals(Functions.TO_INT_FUNCTION,findParser.invoke(defaultVal,int.class));
Assert.assertEquals(Functions.TO_INT_FUNCTION,findParser.invoke(defaultVal,Integer.class));
Assert.assertEquals(Functions.TO_LONG_FUNCTION,findParser.invoke(defaultVal,long.class));
Assert.assertEquals(Functions.TO_LONG_FUNCTION,findParser.invoke(defaultVal,Long.class));
Assert.assertEquals(Functions.TO_DOUBLE_FUNCTION,findParser.invoke(defaultVal,double.class));
Assert.assertEquals(Functions.TO_DOUBLE_FUNCTION,findParser.invoke(defaultVal,Double.class));
Assert.assertEquals(Functions.TO_FLOAT_FUNCTION,findParser.invoke(defaultVal,float.class));
Assert.assertEquals(Functions.TO_FLOAT_FUNCTION,findParser.invoke(defaultVal,Float.class));
Assert.assertEquals(Functions.TO_BYTE_FUNCTION,findParser.invoke(defaultVal,byte.class));
Assert.assertEquals(Functions.TO_BYTE_FUNCTION,findParser.invoke(defaultVal,Byte.class));
Assert.assertEquals(Functions.TO_BOOLEAN_FUNCTION,findParser.invoke(defaultVal,boolean.class));
Assert.assertEquals(Functions.TO_BOOLEAN_FUNCTION,findParser.invoke(defaultVal,Boolean.class));
Assert.assertEquals(Functions.TO_SHORT_FUNCTION,findParser.invoke(defaultVal,short.class));
Assert.assertEquals(Functions.TO_SHORT_FUNCTION,findParser.invoke(defaultVal,Short.class));
Assert.assertEquals(Functions.TO_DATE_FUNCTION,findParser.invoke(defaultVal,Date.class));
}
@Test
public void testPattern(){
String valP = "${some.timeout:5000}";
Matcher matcher = pattern.matcher(valP);
if (matcher.matches()) {
String key = matcher.group(1);
Assert.assertEquals("some.timeout",key);
}
}
}
......@@ -19,6 +19,7 @@ public class DefaultApplicationProvider implements ApplicationProvider {
private Properties m_appProperties = new Properties();
private String m_appId;
private boolean m_enableAutoUpdate;
@Override
public void initialize() {
......@@ -49,11 +50,20 @@ public class DefaultApplicationProvider implements ApplicationProvider {
}
initAppId();
initEnableAutoUpdate();
} catch (Throwable ex) {
logger.error("Initialize DefaultApplicationProvider failed.", ex);
}
}
/**
* @return whether update the field or method which has '@Value' automatically
*/
@Override
public boolean isAutoUpdateEnable() {
return m_enableAutoUpdate;
}
@Override
public String getAppId() {
return m_appId;
......@@ -101,8 +111,29 @@ public class DefaultApplicationProvider implements ApplicationProvider {
logger.warn("app.id is not available from System Property and {}. It is set to null", APP_PROPERTIES_CLASSPATH);
}
private void initEnableAutoUpdate(){
// 1. Get app.autoupdate.enabled from System Property
String enabeAutoUpdate = System.getProperty("app.autoupdate.enabled");
if (!Utils.isBlank(enabeAutoUpdate)) {
m_enableAutoUpdate = Boolean.parseBoolean(enabeAutoUpdate.trim());
logger.info("App update value automatically is {} by app.autoupdate property from System Property", m_enableAutoUpdate);
return;
}
// 2. Try to get app.autoupdate.enabled from app.properties.
enabeAutoUpdate = m_appProperties.getProperty("app.autoupdate.enabled");
if (!Utils.isBlank(enabeAutoUpdate)) {
m_enableAutoUpdate = Boolean.parseBoolean(enabeAutoUpdate.trim());
logger.info("App update value automatically is {} by app.autoupdate property from {}", m_enableAutoUpdate, APP_PROPERTIES_CLASSPATH);
return;
}
// default true, update field automatically
m_enableAutoUpdate = true;
}
@Override
public String toString() {
return "appId [" + getAppId() + "] properties: " + m_appProperties + " (DefaultApplicationProvider)";
return "appId [" + getAppId() + "],enableAutoUpdate["+isAutoUpdateEnable()+"] properties: " + m_appProperties + " (DefaultApplicationProvider)";
}
}
......@@ -59,6 +59,11 @@ public class NullProvider implements ApplicationProvider, NetworkProvider, Serve
}
@Override
public boolean isAutoUpdateEnable() {
return false;
}
@Override
public String getHostAddress() {
return null;
}
......
......@@ -20,4 +20,8 @@ public interface ApplicationProvider extends Provider {
* Initialize the application provider with the specified input stream
*/
public void initialize(InputStream in);
/**
* @return whether update the field or method which has '@Value' automatically
*/
public boolean isAutoUpdateEnable();
}
......@@ -61,4 +61,34 @@ public class DefaultApplicationProviderTest {
assertEquals(null, defaultApplicationProvider.getAppId());
assertFalse(defaultApplicationProvider.isAppIdSet());
}
@Test
public void testLoadAutoUpdateSwitchFromSystemProperty(){
String notEnable = "false";
System.setProperty("app.autoupdate.enabled", notEnable);
defaultApplicationProvider.initialize();
System.clearProperty("app.autoupdate.enabled");
assertFalse(defaultApplicationProvider.isAutoUpdateEnable());
}
@Test
public void testLoadAutoUpdateSwitchFormConfigFile() throws Exception {
File baseDir = new File("src/test/resources/META-INF");
File appProperties = new File(baseDir, "some-invalid-app.properties");
defaultApplicationProvider.initialize(new FileInputStream(appProperties));
assertFalse(defaultApplicationProvider.isAutoUpdateEnable());
}
@Test
public void testLoadAutoUpdateSwitchByDefault() throws Exception {
File baseDir = new File("src/test/resources/META-INF");
File appProperties = new File(baseDir, "app.properties");
defaultApplicationProvider.initialize(new FileInputStream(appProperties));
assertTrue(defaultApplicationProvider.isAutoUpdateEnable());
}
}
app.id=110402
app.id=110402
\ No newline at end of file
appid=110402
\ No newline at end of file
appid=110402
app.autoupdate.enabled=false
\ No newline at end of file
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