Commit e101a9e0 authored by Jason Song's avatar Jason Song Committed by nobodyiam

add ConfigChangeListener with optional interested keys

parent c8e372c7
...@@ -162,12 +162,20 @@ public interface Config { ...@@ -162,12 +162,20 @@ public interface Config {
public long getDurationProperty(String key, long defaultValue); public long getDurationProperty(String key, long defaultValue);
/** /**
* Add change listener to this config instance. * Add change listener to this config instance, will be notified when any key is changed in this namespace.
* *
* @param listener the config change listener * @param listener the config change listener
*/ */
public void addChangeListener(ConfigChangeListener listener); public void addChangeListener(ConfigChangeListener listener);
/**
* Add change listener to this config instance, will only be notified when any of the interested keys is changed in this namespace.
*
* @param listener the config change listener
* @param interestedKeys the keys interested by the listener
*/
public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys);
/** /**
* Return a set of the property names * Return a set of the property names
* *
......
...@@ -40,10 +40,11 @@ import com.google.common.collect.Sets; ...@@ -40,10 +40,11 @@ import com.google.common.collect.Sets;
public abstract class AbstractConfig implements Config { public abstract class AbstractConfig implements Config {
private static final Logger logger = LoggerFactory.getLogger(AbstractConfig.class); private static final Logger logger = LoggerFactory.getLogger(AbstractConfig.class);
private static ExecutorService m_executorService; private static final ExecutorService m_executorService;
private List<ConfigChangeListener> m_listeners = Lists.newCopyOnWriteArrayList(); private final List<ConfigChangeListener> m_listeners = Lists.newCopyOnWriteArrayList();
private ConfigUtil m_configUtil; private final Map<ConfigChangeListener, Set<String>> m_interestedKeys = Maps.newConcurrentMap();
private final ConfigUtil m_configUtil;
private volatile Cache<String, Integer> m_integerCache; private volatile Cache<String, Integer> m_integerCache;
private volatile Cache<String, Long> m_longCache; private volatile Cache<String, Long> m_longCache;
private volatile Cache<String, Short> m_shortCache; private volatile Cache<String, Short> m_shortCache;
...@@ -53,9 +54,9 @@ public abstract class AbstractConfig implements Config { ...@@ -53,9 +54,9 @@ public abstract class AbstractConfig implements Config {
private volatile Cache<String, Boolean> m_booleanCache; private volatile Cache<String, Boolean> m_booleanCache;
private volatile Cache<String, Date> m_dateCache; private volatile Cache<String, Date> m_dateCache;
private volatile Cache<String, Long> m_durationCache; private volatile Cache<String, Long> m_durationCache;
private Map<String, Cache<String, String[]>> m_arrayCache; private final Map<String, Cache<String, String[]>> m_arrayCache;
private List<Cache> allCaches; private final List<Cache> allCaches;
private AtomicLong m_configVersion; //indicate config version private final AtomicLong m_configVersion; //indicate config version
static { static {
m_executorService = Executors.newCachedThreadPool(ApolloThreadFactory m_executorService = Executors.newCachedThreadPool(ApolloThreadFactory
...@@ -71,8 +72,16 @@ public abstract class AbstractConfig implements Config { ...@@ -71,8 +72,16 @@ public abstract class AbstractConfig implements Config {
@Override @Override
public void addChangeListener(ConfigChangeListener listener) { public void addChangeListener(ConfigChangeListener listener) {
addChangeListener(listener, null);
}
@Override
public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys) {
if (!m_listeners.contains(listener)) { if (!m_listeners.contains(listener)) {
m_listeners.add(listener); m_listeners.add(listener);
if (interestedKeys != null && !interestedKeys.isEmpty()) {
m_interestedKeys.put(listener, Sets.newHashSet(interestedKeys));
}
} }
} }
...@@ -395,6 +404,10 @@ public abstract class AbstractConfig implements Config { ...@@ -395,6 +404,10 @@ public abstract class AbstractConfig implements Config {
protected void fireConfigChange(final ConfigChangeEvent changeEvent) { protected void fireConfigChange(final ConfigChangeEvent changeEvent) {
for (final ConfigChangeListener listener : m_listeners) { for (final ConfigChangeListener listener : m_listeners) {
// check whether the listener is interested in this change event
if (!isConfigChangeListenerInterested(listener, changeEvent)) {
continue;
}
m_executorService.submit(new Runnable() { m_executorService.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
...@@ -415,6 +428,22 @@ public abstract class AbstractConfig implements Config { ...@@ -415,6 +428,22 @@ public abstract class AbstractConfig implements Config {
} }
} }
private boolean isConfigChangeListenerInterested(ConfigChangeListener configChangeListener, ConfigChangeEvent configChangeEvent) {
Set<String> interestedKeys = m_interestedKeys.get(configChangeListener);
if (interestedKeys == null || interestedKeys.isEmpty()) {
return true; // no interested keys means interested in all keys
}
for (String interestedKey : interestedKeys) {
if (configChangeEvent.isChanged(interestedKey)) {
return true;
}
}
return false;
}
List<ConfigChange> calcPropertyChanges(String namespace, Properties previous, List<ConfigChange> calcPropertyChanges(String namespace, Properties previous,
Properties current) { Properties current) {
if (previous == null) { if (previous == null) {
......
...@@ -5,8 +5,10 @@ import com.ctrip.framework.apollo.ConfigChangeListener; ...@@ -5,8 +5,10 @@ import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.ConfigService; import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.model.ConfigChangeEvent; import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Set;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
...@@ -51,6 +53,8 @@ public class ApolloAnnotationProcessor extends ApolloProcessor { ...@@ -51,6 +53,8 @@ public class ApolloAnnotationProcessor extends ApolloProcessor {
ReflectionUtils.makeAccessible(method); ReflectionUtils.makeAccessible(method);
String[] namespaces = annotation.value(); String[] namespaces = annotation.value();
String[] annotatedInterestedKeys = annotation.interestedKeys();
Set<String> interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
ConfigChangeListener configChangeListener = new ConfigChangeListener() { ConfigChangeListener configChangeListener = new ConfigChangeListener() {
@Override @Override
public void onChange(ConfigChangeEvent changeEvent) { public void onChange(ConfigChangeEvent changeEvent) {
...@@ -61,7 +65,11 @@ public class ApolloAnnotationProcessor extends ApolloProcessor { ...@@ -61,7 +65,11 @@ public class ApolloAnnotationProcessor extends ApolloProcessor {
for (String namespace : namespaces) { for (String namespace : namespaces) {
Config config = ConfigService.getConfig(namespace); Config config = ConfigService.getConfig(namespace);
if (interestedKeys == null) {
config.addChangeListener(configChangeListener); config.addChangeListener(configChangeListener);
} else {
config.addChangeListener(configChangeListener, interestedKeys);
}
} }
} }
} }
...@@ -13,11 +13,17 @@ import com.ctrip.framework.apollo.core.ConfigConsts; ...@@ -13,11 +13,17 @@ import com.ctrip.framework.apollo.core.ConfigConsts;
* *
* <p>Usage example:</p> * <p>Usage example:</p>
* <pre class="code"> * <pre class="code">
* //Listener on namespaces of "someNamespace" and "anotherNamespace" * //Listener on namespaces of "someNamespace" and "anotherNamespace", will be notified when any key is changed
* &#064;ApolloConfigChangeListener({"someNamespace","anotherNamespace"}) * &#064;ApolloConfigChangeListener({"someNamespace","anotherNamespace"})
* private void onChange(ConfigChangeEvent changeEvent) { * private void onChange(ConfigChangeEvent changeEvent) {
* //handle change event * //handle change event
* } * }
* <br />
* //Listener on namespaces of "someNamespace" and "anotherNamespace", will only be notified when "someKey" or "anotherKey" is changed
* &#064;ApolloConfigChangeListener(value = {"someNamespace","anotherNamespace"}, interestedKeys = {"someKey", "anotherKey"})
* private void onChange(ConfigChangeEvent changeEvent) {
* //handle change event
* }
* </pre> * </pre>
* *
* @author Jason Song(song_s@ctrip.com) * @author Jason Song(song_s@ctrip.com)
...@@ -30,4 +36,11 @@ public @interface ApolloConfigChangeListener { ...@@ -30,4 +36,11 @@ public @interface ApolloConfigChangeListener {
* Apollo namespace for the config, if not specified then default to application * Apollo namespace for the config, if not specified then default to application
*/ */
String[] value() default {ConfigConsts.NAMESPACE_APPLICATION}; String[] value() default {ConfigConsts.NAMESPACE_APPLICATION};
/**
* The keys interested by the listener, will only be notified if any of the interested keys is changed.
* <br />
* If not specified then will be notified when any key is changed.
*/
String[] interestedKeys() default {};
} }
...@@ -2,14 +2,18 @@ package com.ctrip.framework.apollo.internals; ...@@ -2,14 +2,18 @@ package com.ctrip.framework.apollo.internals;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.File; import java.io.File;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.Collections; import java.util.Collections;
...@@ -647,6 +651,56 @@ public class DefaultConfigTest { ...@@ -647,6 +651,56 @@ public class DefaultConfigTest {
assertEquals(PropertyChangeType.ADDED, newKeyChange.getChangeType()); assertEquals(PropertyChangeType.ADDED, newKeyChange.getChangeType());
} }
@Test
public void testFireConfigChangeWithInterestedKeys() throws Exception {
String someKeyChanged = "someKeyChanged";
String anotherKeyChanged = "anotherKeyChanged";
String someKeyNotChanged = "someKeyNotChanged";
String someNamespace = "someNamespace";
Map<String, ConfigChange> changes = Maps.newHashMap();
changes.put(someKeyChanged, mock(ConfigChange.class));
changes.put(anotherKeyChanged, mock(ConfigChange.class));
ConfigChangeEvent someChangeEvent = new ConfigChangeEvent(someNamespace, changes);
final SettableFuture<ConfigChangeEvent> interestedInAllKeysFuture = SettableFuture.create();
ConfigChangeListener interestedInAllKeys = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
interestedInAllKeysFuture.set(changeEvent);
}
};
final SettableFuture<ConfigChangeEvent> interestedInSomeKeyFuture = SettableFuture.create();
ConfigChangeListener interestedInSomeKey = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
interestedInSomeKeyFuture.set(changeEvent);
}
};
final SettableFuture<ConfigChangeEvent> interestedInSomeKeyNotChangedFuture = SettableFuture.create();
ConfigChangeListener interestedInSomeKeyNotChanged = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
interestedInSomeKeyNotChangedFuture.set(changeEvent);
}
};
DefaultConfig config = new DefaultConfig(someNamespace, mock(ConfigRepository.class));
config.addChangeListener(interestedInAllKeys);
config.addChangeListener(interestedInSomeKey, Sets.newHashSet(someKeyChanged));
config.addChangeListener(interestedInSomeKeyNotChanged, Sets.newHashSet(someKeyNotChanged));
config.fireConfigChange(someChangeEvent);
ConfigChangeEvent changeEvent = interestedInAllKeysFuture.get(500, TimeUnit.MILLISECONDS);
assertEquals(someChangeEvent, changeEvent);
assertEquals(someChangeEvent, interestedInSomeKeyFuture.get(500, TimeUnit.MILLISECONDS));
assertFalse(interestedInSomeKeyNotChangedFuture.isDone());
}
@Test @Test
public void testGetPropertyNames() { public void testGetPropertyNames() {
String someKeyPrefix = "someKey"; String someKeyPrefix = "someKey";
......
package com.ctrip.framework.apollo.spring; package com.ctrip.framework.apollo.spring;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import java.util.List; import static org.mockito.Mockito.verify;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.ctrip.framework.apollo.Config; import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigChangeListener; import com.ctrip.framework.apollo.ConfigChangeListener;
...@@ -23,6 +16,17 @@ import com.ctrip.framework.apollo.spring.annotation.ApolloConfig; ...@@ -23,6 +16,17 @@ import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener; import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/** /**
* @author Jason Song(song_s@ctrip.com) * @author Jason Song(song_s@ctrip.com)
...@@ -200,6 +204,39 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest { ...@@ -200,6 +204,39 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
assertEquals(someEvent, bean.getSomeChangeEvent()); assertEquals(someEvent, bean.getSomeChangeEvent());
} }
@Test
public void testApolloConfigChangeListenerWithInterestedKeys() throws Exception {
Config applicationConfig = mock(Config.class);
Config fxApolloConfig = mock(Config.class);
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, applicationConfig);
mockConfig(FX_APOLLO_NAMESPACE, fxApolloConfig);
TestApolloConfigChangeListenerWithInterestedKeysBean bean = getBean(
TestApolloConfigChangeListenerWithInterestedKeysBean.class, AppConfig8.class);
final ArgumentCaptor<Set> applicationConfigInterestedKeys = ArgumentCaptor.forClass(Set.class);
final ArgumentCaptor<Set> fxApolloConfigInterestedKeys = ArgumentCaptor.forClass(Set.class);
verify(applicationConfig, times(2))
.addChangeListener(any(ConfigChangeListener.class), applicationConfigInterestedKeys.capture());
verify(fxApolloConfig, times(1))
.addChangeListener(any(ConfigChangeListener.class), fxApolloConfigInterestedKeys.capture());
assertEquals(2, applicationConfigInterestedKeys.getAllValues().size());
Set<String> result = Sets.newHashSet();
for (Set interestedKeys : applicationConfigInterestedKeys.getAllValues()) {
result.addAll(interestedKeys);
}
assertEquals(Sets.newHashSet("someKey", "anotherKey"), result);
assertEquals(1, fxApolloConfigInterestedKeys.getAllValues().size());
assertEquals(asList(Sets.newHashSet("anotherKey")), fxApolloConfigInterestedKeys.getAllValues());
}
private <T> T getBean(Class<T> beanClass, Class<?>... annotatedClasses) { private <T> T getBean(Class<T> beanClass, Class<?>... annotatedClasses) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(annotatedClasses); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(annotatedClasses);
...@@ -269,6 +306,15 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest { ...@@ -269,6 +306,15 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
} }
} }
@Configuration
@EnableApolloConfig
static class AppConfig8 {
@Bean
public TestApolloConfigChangeListenerWithInterestedKeysBean bean() {
return new TestApolloConfigChangeListenerWithInterestedKeysBean();
}
}
static class TestApolloConfigBean1 { static class TestApolloConfigBean1 {
@ApolloConfig @ApolloConfig
private Config config; private Config config;
...@@ -365,4 +411,16 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest { ...@@ -365,4 +411,16 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
return someChangeEvent; return someChangeEvent;
} }
} }
static class TestApolloConfigChangeListenerWithInterestedKeysBean {
@ApolloConfigChangeListener(interestedKeys = {"someKey"})
private void someOnChange(ConfigChangeEvent changeEvent) {}
@ApolloConfigChangeListener(value = {ConfigConsts.NAMESPACE_APPLICATION, FX_APOLLO_NAMESPACE},
interestedKeys = {"anotherKey"})
private void anotherOnChange(ConfigChangeEvent changeEvent) {
}
}
} }
package com.ctrip.framework.apollo.spring; package com.ctrip.framework.apollo.spring;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.google.common.collect.Sets;
import java.util.List; import java.util.List;
import java.util.Set;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCreationException;
...@@ -125,6 +131,39 @@ public class XMLConfigAnnotationTest extends AbstractSpringIntegrationTest { ...@@ -125,6 +131,39 @@ public class XMLConfigAnnotationTest extends AbstractSpringIntegrationTest {
getBean("spring/XmlConfigAnnotationTest5.xml", TestApolloConfigChangeListenerBean3.class); getBean("spring/XmlConfigAnnotationTest5.xml", TestApolloConfigChangeListenerBean3.class);
} }
@Test
public void testApolloConfigChangeListenerWithInterestedKeys() throws Exception {
Config applicationConfig = mock(Config.class);
Config fxApolloConfig = mock(Config.class);
mockConfig(ConfigConsts.NAMESPACE_APPLICATION, applicationConfig);
mockConfig(FX_APOLLO_NAMESPACE, fxApolloConfig);
TestApolloConfigChangeListenerWithInterestedKeysBean bean = getBean(
"spring/XmlConfigAnnotationTest6.xml", TestApolloConfigChangeListenerWithInterestedKeysBean.class);
final ArgumentCaptor<Set> applicationConfigInterestedKeys = ArgumentCaptor.forClass(Set.class);
final ArgumentCaptor<Set> fxApolloConfigInterestedKeys = ArgumentCaptor.forClass(Set.class);
verify(applicationConfig, times(2))
.addChangeListener(any(ConfigChangeListener.class), applicationConfigInterestedKeys.capture());
verify(fxApolloConfig, times(1))
.addChangeListener(any(ConfigChangeListener.class), fxApolloConfigInterestedKeys.capture());
assertEquals(2, applicationConfigInterestedKeys.getAllValues().size());
Set<String> result = Sets.newHashSet();
for (Set interestedKeys : applicationConfigInterestedKeys.getAllValues()) {
result.addAll(interestedKeys);
}
assertEquals(Sets.newHashSet("someKey", "anotherKey"), result);
assertEquals(1, fxApolloConfigInterestedKeys.getAllValues().size());
assertEquals(asList(Sets.newHashSet("anotherKey")), fxApolloConfigInterestedKeys.getAllValues());
}
private <T> T getBean(String xmlLocation, Class<T> beanClass) { private <T> T getBean(String xmlLocation, Class<T> beanClass) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(xmlLocation); ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(xmlLocation);
...@@ -204,4 +243,15 @@ public class XMLConfigAnnotationTest extends AbstractSpringIntegrationTest { ...@@ -204,4 +243,15 @@ public class XMLConfigAnnotationTest extends AbstractSpringIntegrationTest {
} }
} }
static class TestApolloConfigChangeListenerWithInterestedKeysBean {
@ApolloConfigChangeListener(interestedKeys = {"someKey"})
private void someOnChange(ConfigChangeEvent changeEvent) {}
@ApolloConfigChangeListener(value = {ConfigConsts.NAMESPACE_APPLICATION, FX_APOLLO_NAMESPACE},
interestedKeys = {"anotherKey"})
private void anotherOnChange(ConfigChangeEvent changeEvent) {
}
}
} }
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:apollo="http://www.ctrip.com/schema/apollo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">
<apollo:config/>
<bean class="com.ctrip.framework.apollo.spring.XMLConfigAnnotationTest.TestApolloConfigChangeListenerWithInterestedKeysBean"/>
</beans>
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