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 {
*/
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
*
......
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.ConfigChangeListener;
import com.ctrip.framework.apollo.build.ApolloInjector;
......@@ -33,6 +20,18 @@ import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
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)
......@@ -44,6 +43,7 @@ public abstract class AbstractConfig implements Config {
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_interestedKeyPrefixes = Maps.newConcurrentMap();
private final ConfigUtil m_configUtil;
private volatile Cache<String, Integer> m_integerCache;
private volatile Cache<String, Long> m_longCache;
......@@ -77,17 +77,26 @@ public abstract class AbstractConfig implements Config {
@Override
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)) {
m_listeners.add(listener);
if (interestedKeys != null && !interestedKeys.isEmpty()) {
m_interestedKeys.put(listener, Sets.newHashSet(interestedKeys));
}
if (interestedKeyPrefixes != null && !interestedKeyPrefixes.isEmpty()) {
m_interestedKeyPrefixes.put(listener, Sets.newHashSet(interestedKeyPrefixes));
}
}
}
@Override
public boolean removeChangeListener(ConfigChangeListener listener) {
m_interestedKeys.remove(listener);
m_interestedKeyPrefixes.remove(listener);
return m_listeners.remove(listener);
}
......@@ -453,16 +462,30 @@ public abstract class AbstractConfig implements Config {
private boolean isConfigChangeListenerInterested(ConfigChangeListener configChangeListener, ConfigChangeEvent configChangeEvent) {
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
}
if (interestedKeys != null) {
for (String interestedKey : interestedKeys) {
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;
}
}
}
}
return false;
}
......
......@@ -5,10 +5,12 @@ 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 com.google.common.collect.Sets;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Set;
import com.google.common.collect.Sets;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;
......@@ -54,7 +56,7 @@ public class ApolloAnnotationProcessor extends ApolloProcessor {
ReflectionUtils.makeAccessible(method);
String[] namespaces = annotation.value();
String[] annotatedInterestedKeys = annotation.interestedKeys();
Set<String> interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
ConfigChangeListener configChangeListener = new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
......@@ -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) {
Config config = ConfigService.getConfig(namespace);
if (interestedKeys == null) {
if (interestedKeys == null && interestedKeyPrefixes == null) {
config.addChangeListener(configChangeListener);
} else {
config.addChangeListener(configChangeListener, interestedKeys);
config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes);
}
}
}
......
package com.ctrip.framework.apollo.spring.annotation;
import com.ctrip.framework.apollo.core.ConfigConsts;
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;
import com.ctrip.framework.apollo.core.ConfigConsts;
/**
* Use this annotation to register Apollo ConfigChangeListener.
*
......@@ -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.
* <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 {};
/**
* 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;
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.ConfigChangeListener;
import com.ctrip.framework.apollo.core.ConfigConsts;
......@@ -17,8 +9,6 @@ import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
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;
......@@ -28,6 +18,18 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean;
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)
*/
......@@ -219,10 +221,10 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
final ArgumentCaptor<Set> fxApolloConfigInterestedKeys = ArgumentCaptor.forClass(Set.class);
verify(applicationConfig, times(2))
.addChangeListener(any(ConfigChangeListener.class), applicationConfigInterestedKeys.capture());
.addChangeListener(any(ConfigChangeListener.class), applicationConfigInterestedKeys.capture(), anySetOf(String.class));
verify(fxApolloConfig, times(1))
.addChangeListener(any(ConfigChangeListener.class), fxApolloConfigInterestedKeys.capture());
.addChangeListener(any(ConfigChangeListener.class), fxApolloConfigInterestedKeys.capture(), anySetOf(String.class));
assertEquals(2, applicationConfigInterestedKeys.getAllValues().size());
......
......@@ -3,6 +3,7 @@ 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.Matchers.anySetOf;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
......@@ -146,10 +147,10 @@ public class XMLConfigAnnotationTest extends AbstractSpringIntegrationTest {
final ArgumentCaptor<Set> fxApolloConfigInterestedKeys = ArgumentCaptor.forClass(Set.class);
verify(applicationConfig, times(2))
.addChangeListener(any(ConfigChangeListener.class), applicationConfigInterestedKeys.capture());
.addChangeListener(any(ConfigChangeListener.class), applicationConfigInterestedKeys.capture(), anySetOf(String.class));
verify(fxApolloConfig, times(1))
.addChangeListener(any(ConfigChangeListener.class), fxApolloConfigInterestedKeys.capture());
.addChangeListener(any(ConfigChangeListener.class), fxApolloConfigInterestedKeys.capture(), anySetOf(String.class));
assertEquals(2, applicationConfigInterestedKeys.getAllValues().size());
......
......@@ -151,7 +151,9 @@ public class EmbeddedApollo extends ExternalResource {
if (addedOrModifiedPropertiesOfNamespace.containsKey(namespace)) {
addedOrModifiedPropertiesOfNamespace.get(namespace).put(someKey, someValue);
} 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;
import static org.junit.Assert.assertEquals;
import com.ctrip.framework.apollo.enums.PropertyChangeType;
import com.ctrip.framework.apollo.mockserver.ApolloMockServerSpringIntegrationTest.TestConfiguration;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
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.Test;
import org.junit.runner.RunWith;
......@@ -22,6 +17,12 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
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
*/
......@@ -37,6 +38,9 @@ public class ApolloMockServerSpringIntegrationTest {
@Autowired
private TestBean testBean;
@Autowired
private TestInterestedKeyPrefixesBean testInterestedKeyPrefixesBean;
@Test
@DirtiesContext
public void testPropertyInject() {
......@@ -63,6 +67,25 @@ public class ApolloMockServerSpringIntegrationTest {
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
@Configuration
static class TestConfiguration {
......@@ -71,6 +94,11 @@ public class ApolloMockServerSpringIntegrationTest {
public TestBean testBean() {
return new TestBean();
}
@Bean
public TestInterestedKeyPrefixesBean testInterestedKeyPrefixesBean() {
return new TestInterestedKeyPrefixesBean();
}
}
private static class TestBean {
......@@ -87,4 +115,13 @@ public class ApolloMockServerSpringIntegrationTest {
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