Commit 4fa65a1d authored by kezhenxu94's avatar kezhenxu94 Committed by Jason Song

feature: support listeners on events based on key patterns. #1461 (#1871)

feature: support listeners on events based on key patterns. #1461
parent 0776918c
...@@ -181,6 +181,21 @@ public interface Config { ...@@ -181,6 +181,21 @@ public interface Config {
*/ */
public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys); public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys);
/**
* 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 that the listener is interested in
* @param interestedKeyPrefixes the key prefixes that the listener is interested in,
* e.g. "spring." means that {@code listener} is interested in keys that starts with "spring.", such as "spring.banner", "spring.jpa", etc.
* and "application" means that {@code listener} is interested in keys that starts with "application", such as "applicationName", "application.port", etc.
* For more details, see {@link com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener#interestedKeyPrefixes()}
* and {@link java.lang.String#startsWith(String)}
*
* @since 1.3.0
*/
public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys, Set<String> interestedKeyPrefixes);
/** /**
* Remove the change listener * Remove the change listener
* *
......
package com.ctrip.framework.apollo.internals; package com.ctrip.framework.apollo.internals;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ctrip.framework.apollo.Config; import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigChangeListener; import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.build.ApolloInjector; import com.ctrip.framework.apollo.build.ApolloInjector;
...@@ -33,6 +20,18 @@ import com.google.common.cache.CacheBuilder; ...@@ -33,6 +20,18 @@ import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
/** /**
* @author Jason Song(song_s@ctrip.com) * @author Jason Song(song_s@ctrip.com)
...@@ -44,6 +43,7 @@ public abstract class AbstractConfig implements Config { ...@@ -44,6 +43,7 @@ public abstract class AbstractConfig implements Config {
private final List<ConfigChangeListener> m_listeners = Lists.newCopyOnWriteArrayList(); private final List<ConfigChangeListener> m_listeners = Lists.newCopyOnWriteArrayList();
private final Map<ConfigChangeListener, Set<String>> m_interestedKeys = Maps.newConcurrentMap(); private final Map<ConfigChangeListener, Set<String>> m_interestedKeys = Maps.newConcurrentMap();
private final Map<ConfigChangeListener, Set<String>> m_interestedKeyPrefixes = Maps.newConcurrentMap();
private final ConfigUtil m_configUtil; 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;
...@@ -77,17 +77,26 @@ public abstract class AbstractConfig implements Config { ...@@ -77,17 +77,26 @@ public abstract class AbstractConfig implements Config {
@Override @Override
public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys) { public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys) {
addChangeListener(listener, interestedKeys, null);
}
@Override
public void addChangeListener(ConfigChangeListener listener, Set<String> interestedKeys, Set<String> interestedKeyPrefixes) {
if (!m_listeners.contains(listener)) { if (!m_listeners.contains(listener)) {
m_listeners.add(listener); m_listeners.add(listener);
if (interestedKeys != null && !interestedKeys.isEmpty()) { if (interestedKeys != null && !interestedKeys.isEmpty()) {
m_interestedKeys.put(listener, Sets.newHashSet(interestedKeys)); m_interestedKeys.put(listener, Sets.newHashSet(interestedKeys));
} }
if (interestedKeyPrefixes != null && !interestedKeyPrefixes.isEmpty()) {
m_interestedKeyPrefixes.put(listener, Sets.newHashSet(interestedKeyPrefixes));
}
} }
} }
@Override @Override
public boolean removeChangeListener(ConfigChangeListener listener) { public boolean removeChangeListener(ConfigChangeListener listener) {
m_interestedKeys.remove(listener); m_interestedKeys.remove(listener);
m_interestedKeyPrefixes.remove(listener);
return m_listeners.remove(listener); return m_listeners.remove(listener);
} }
...@@ -453,14 +462,28 @@ public abstract class AbstractConfig implements Config { ...@@ -453,14 +462,28 @@ public abstract class AbstractConfig implements Config {
private boolean isConfigChangeListenerInterested(ConfigChangeListener configChangeListener, ConfigChangeEvent configChangeEvent) { private boolean isConfigChangeListenerInterested(ConfigChangeListener configChangeListener, ConfigChangeEvent configChangeEvent) {
Set<String> interestedKeys = m_interestedKeys.get(configChangeListener); Set<String> interestedKeys = m_interestedKeys.get(configChangeListener);
Set<String> interestedKeyPrefixes = m_interestedKeyPrefixes.get(configChangeListener);
if (interestedKeys == null || interestedKeys.isEmpty()) { if ((interestedKeys == null || interestedKeys.isEmpty())
&& (interestedKeyPrefixes == null || interestedKeyPrefixes.isEmpty())) {
return true; // no interested keys means interested in all keys return true; // no interested keys means interested in all keys
} }
for (String interestedKey : interestedKeys) { if (interestedKeys != null) {
if (configChangeEvent.isChanged(interestedKey)) { for (String interestedKey : interestedKeys) {
return true; if (configChangeEvent.isChanged(interestedKey)) {
return true;
}
}
}
if (interestedKeyPrefixes != null) {
for (String prefix : interestedKeyPrefixes) {
for (final String changedKey : configChangeEvent.changedKeys()) {
if (changedKey.startsWith(prefix)) {
return true;
}
}
} }
} }
......
...@@ -5,10 +5,12 @@ import com.ctrip.framework.apollo.ConfigChangeListener; ...@@ -5,10 +5,12 @@ 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.Collections;
import java.util.Set; import java.util.Set;
import com.google.common.collect.Sets;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
...@@ -54,7 +56,7 @@ public class ApolloAnnotationProcessor extends ApolloProcessor { ...@@ -54,7 +56,7 @@ public class ApolloAnnotationProcessor extends ApolloProcessor {
ReflectionUtils.makeAccessible(method); ReflectionUtils.makeAccessible(method);
String[] namespaces = annotation.value(); String[] namespaces = annotation.value();
String[] annotatedInterestedKeys = annotation.interestedKeys(); String[] annotatedInterestedKeys = annotation.interestedKeys();
Set<String> interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null; String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
ConfigChangeListener configChangeListener = new ConfigChangeListener() { ConfigChangeListener configChangeListener = new ConfigChangeListener() {
@Override @Override
public void onChange(ConfigChangeEvent changeEvent) { public void onChange(ConfigChangeEvent changeEvent) {
...@@ -62,13 +64,16 @@ public class ApolloAnnotationProcessor extends ApolloProcessor { ...@@ -62,13 +64,16 @@ public class ApolloAnnotationProcessor extends ApolloProcessor {
} }
}; };
Set<String> interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
Set<String> interestedKeyPrefixes = annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes) : null;
for (String namespace : namespaces) { for (String namespace : namespaces) {
Config config = ConfigService.getConfig(namespace); Config config = ConfigService.getConfig(namespace);
if (interestedKeys == null) { if (interestedKeys == null && interestedKeyPrefixes == null) {
config.addChangeListener(configChangeListener); config.addChangeListener(configChangeListener);
} else { } else {
config.addChangeListener(configChangeListener, interestedKeys); config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes);
} }
} }
} }
......
package com.ctrip.framework.apollo.spring.annotation; package com.ctrip.framework.apollo.spring.annotation;
import com.ctrip.framework.apollo.core.ConfigConsts;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import com.ctrip.framework.apollo.core.ConfigConsts;
/** /**
* Use this annotation to register Apollo ConfigChangeListener. * Use this annotation to register Apollo ConfigChangeListener.
* *
...@@ -40,7 +39,17 @@ public @interface ApolloConfigChangeListener { ...@@ -40,7 +39,17 @@ public @interface ApolloConfigChangeListener {
/** /**
* The keys interested by the listener, will only be notified if any of the interested keys is changed. * The keys interested by the listener, will only be notified if any of the interested keys is changed.
* <br /> * <br />
* If not specified then will be notified when any key is changed. * If neither of {@code interestedKeys} and {@code interestedKeyPrefixes} is specified then the {@code listener} will be notified when any key is changed.
*/ */
String[] interestedKeys() default {}; String[] interestedKeys() default {};
/**
* The key prefixes that the listener is interested in, will be notified if and only if the changed keys start with anyone of the prefixes.
* The prefixes will simply be used to determine whether the {@code listener} should be notified or not using {@code changedKey.startsWith(prefix)}.
* e.g. "spring." means that {@code listener} is interested in keys that starts with "spring.", such as "spring.banner", "spring.jpa", etc.
* and "application" means that {@code listener} is interested in keys that starts with "application", such as "applicationName", "application.port", etc.
* <br />
* If neither of {@code interestedKeys} and {@code interestedKeyPrefixes} is specified then the {@code listener} will be notified when whatever key is changed.
*/
String[] interestedKeyPrefixes() default {};
} }
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.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.ctrip.framework.apollo.Config; import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigChangeListener; import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.ConfigConsts;
...@@ -17,8 +9,6 @@ import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener; ...@@ -17,8 +9,6 @@ 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 com.google.common.collect.Sets;
import java.util.List;
import java.util.Set;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
...@@ -28,6 +18,18 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext ...@@ -28,6 +18,18 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Set;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anySetOf;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/** /**
* @author Jason Song(song_s@ctrip.com) * @author Jason Song(song_s@ctrip.com)
*/ */
...@@ -219,10 +221,10 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest { ...@@ -219,10 +221,10 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
final ArgumentCaptor<Set> fxApolloConfigInterestedKeys = ArgumentCaptor.forClass(Set.class); final ArgumentCaptor<Set> fxApolloConfigInterestedKeys = ArgumentCaptor.forClass(Set.class);
verify(applicationConfig, times(2)) verify(applicationConfig, times(2))
.addChangeListener(any(ConfigChangeListener.class), applicationConfigInterestedKeys.capture()); .addChangeListener(any(ConfigChangeListener.class), applicationConfigInterestedKeys.capture(), anySetOf(String.class));
verify(fxApolloConfig, times(1)) verify(fxApolloConfig, times(1))
.addChangeListener(any(ConfigChangeListener.class), fxApolloConfigInterestedKeys.capture()); .addChangeListener(any(ConfigChangeListener.class), fxApolloConfigInterestedKeys.capture(), anySetOf(String.class));
assertEquals(2, applicationConfigInterestedKeys.getAllValues().size()); assertEquals(2, applicationConfigInterestedKeys.getAllValues().size());
......
...@@ -3,6 +3,7 @@ package com.ctrip.framework.apollo.spring; ...@@ -3,6 +3,7 @@ package com.ctrip.framework.apollo.spring;
import static java.util.Arrays.asList; 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.Matchers.anySetOf;
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.times;
...@@ -146,10 +147,10 @@ public class XMLConfigAnnotationTest extends AbstractSpringIntegrationTest { ...@@ -146,10 +147,10 @@ public class XMLConfigAnnotationTest extends AbstractSpringIntegrationTest {
final ArgumentCaptor<Set> fxApolloConfigInterestedKeys = ArgumentCaptor.forClass(Set.class); final ArgumentCaptor<Set> fxApolloConfigInterestedKeys = ArgumentCaptor.forClass(Set.class);
verify(applicationConfig, times(2)) verify(applicationConfig, times(2))
.addChangeListener(any(ConfigChangeListener.class), applicationConfigInterestedKeys.capture()); .addChangeListener(any(ConfigChangeListener.class), applicationConfigInterestedKeys.capture(), anySetOf(String.class));
verify(fxApolloConfig, times(1)) verify(fxApolloConfig, times(1))
.addChangeListener(any(ConfigChangeListener.class), fxApolloConfigInterestedKeys.capture()); .addChangeListener(any(ConfigChangeListener.class), fxApolloConfigInterestedKeys.capture(), anySetOf(String.class));
assertEquals(2, applicationConfigInterestedKeys.getAllValues().size()); assertEquals(2, applicationConfigInterestedKeys.getAllValues().size());
......
...@@ -151,7 +151,9 @@ public class EmbeddedApollo extends ExternalResource { ...@@ -151,7 +151,9 @@ public class EmbeddedApollo extends ExternalResource {
if (addedOrModifiedPropertiesOfNamespace.containsKey(namespace)) { if (addedOrModifiedPropertiesOfNamespace.containsKey(namespace)) {
addedOrModifiedPropertiesOfNamespace.get(namespace).put(someKey, someValue); addedOrModifiedPropertiesOfNamespace.get(namespace).put(someKey, someValue);
} else { } else {
addedOrModifiedPropertiesOfNamespace.put(namespace, ImmutableMap.of(someKey, someValue)); Map<String, String> m = new HashMap<>();
m.put(someKey, someValue);
addedOrModifiedPropertiesOfNamespace.put(namespace, m);
} }
} }
......
package com.ctrip.framework.apollo.mockserver; package com.ctrip.framework.apollo.mockserver;
import static org.junit.Assert.assertEquals;
import com.ctrip.framework.apollo.enums.PropertyChangeType; import com.ctrip.framework.apollo.enums.PropertyChangeType;
import com.ctrip.framework.apollo.mockserver.ApolloMockServerSpringIntegrationTest.TestConfiguration; import com.ctrip.framework.apollo.mockserver.ApolloMockServerSpringIntegrationTest.TestConfiguration;
import com.ctrip.framework.apollo.model.ConfigChangeEvent; import com.ctrip.framework.apollo.model.ConfigChangeEvent;
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.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -22,6 +17,12 @@ import org.springframework.context.annotation.Configuration; ...@@ -22,6 +17,12 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertEquals;
/** /**
* Create by zhangzheng on 8/16/18 Email:zhangzheng@youzan.com * Create by zhangzheng on 8/16/18 Email:zhangzheng@youzan.com
*/ */
...@@ -37,6 +38,9 @@ public class ApolloMockServerSpringIntegrationTest { ...@@ -37,6 +38,9 @@ public class ApolloMockServerSpringIntegrationTest {
@Autowired @Autowired
private TestBean testBean; private TestBean testBean;
@Autowired
private TestInterestedKeyPrefixesBean testInterestedKeyPrefixesBean;
@Test @Test
@DirtiesContext @DirtiesContext
public void testPropertyInject() { public void testPropertyInject() {
...@@ -63,6 +67,25 @@ public class ApolloMockServerSpringIntegrationTest { ...@@ -63,6 +67,25 @@ public class ApolloMockServerSpringIntegrationTest {
assertEquals(PropertyChangeType.DELETED, changeEvent.getChange("key1").getChangeType()); assertEquals(PropertyChangeType.DELETED, changeEvent.getChange("key1").getChangeType());
} }
@Test
@DirtiesContext
public void shouldNotifyOnInterestedPatterns() throws Exception {
embeddedApollo.addOrModifyProperty(otherNamespace, "server.port", "8080");
embeddedApollo.addOrModifyProperty(otherNamespace, "server.path", "/apollo");
embeddedApollo.addOrModifyProperty(otherNamespace, "spring.application.name", "whatever");
ConfigChangeEvent changeEvent = testInterestedKeyPrefixesBean.futureData.get(5000, TimeUnit.MILLISECONDS);
assertEquals(otherNamespace, changeEvent.getNamespace());
assertEquals("8080", changeEvent.getChange("server.port").getNewValue());
assertEquals("/apollo", changeEvent.getChange("server.path").getNewValue());
}
@Test(expected = TimeoutException.class)
@DirtiesContext
public void shouldNotNotifyOnUninterestedPatterns() throws Exception {
embeddedApollo.addOrModifyProperty(otherNamespace, "spring.application.name", "apollo");
testInterestedKeyPrefixesBean.futureData.get(5000, TimeUnit.MILLISECONDS);
}
@EnableApolloConfig @EnableApolloConfig
@Configuration @Configuration
static class TestConfiguration { static class TestConfiguration {
...@@ -71,6 +94,11 @@ public class ApolloMockServerSpringIntegrationTest { ...@@ -71,6 +94,11 @@ public class ApolloMockServerSpringIntegrationTest {
public TestBean testBean() { public TestBean testBean() {
return new TestBean(); return new TestBean();
} }
@Bean
public TestInterestedKeyPrefixesBean testInterestedKeyPrefixesBean() {
return new TestInterestedKeyPrefixesBean();
}
} }
private static class TestBean { private static class TestBean {
...@@ -87,4 +115,13 @@ public class ApolloMockServerSpringIntegrationTest { ...@@ -87,4 +115,13 @@ public class ApolloMockServerSpringIntegrationTest {
futureData.set(changeEvent); futureData.set(changeEvent);
} }
} }
private static class TestInterestedKeyPrefixesBean {
private SettableFuture<ConfigChangeEvent> futureData = SettableFuture.create();
@ApolloConfigChangeListener(value = otherNamespace, interestedKeyPrefixes = "server.")
private void onChange(ConfigChangeEvent changeEvent) {
futureData.set(changeEvent);
}
}
} }
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