Skip to content

Commit

Permalink
feature: support listeners on events based on key patterns. #1461 (#1871
Browse files Browse the repository at this point in the history
)

feature: support listeners on events based on key patterns. #1461
  • Loading branch information
kezhenxu94 authored and nobodyiam committed Jan 20, 2019
1 parent 0776918 commit 4fa65a1
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 44 deletions.
15 changes: 15 additions & 0 deletions apollo-client/src/main/java/com/ctrip/framework/apollo/Config.java
Expand Up @@ -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
*
Expand Down
@@ -1,18 +1,5 @@
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;
Expand All @@ -33,6 +20,18 @@
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)
Expand All @@ -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;
Expand Down Expand Up @@ -77,17 +77,26 @@ public void addChangeListener(ConfigChangeListener listener) {

@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);
}

Expand Down Expand Up @@ -453,14 +462,28 @@ public void run() {

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
}

for (String interestedKey : interestedKeys) {
if (configChangeEvent.isChanged(interestedKey)) {
return true;
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;
}
}
}
}

Expand Down
Expand Up @@ -5,10 +5,12 @@
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;

Expand Down Expand Up @@ -54,21 +56,24 @@ protected void processMethod(final Object bean, String beanName, final Method me
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) {
ReflectionUtils.invokeMethod(method, bean, changeEvent);
}
};

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);
}
}
}
Expand Down
@@ -1,13 +1,12 @@
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.
*
Expand Down Expand Up @@ -40,7 +39,17 @@
/**
* 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 {};
}
@@ -1,13 +1,5 @@
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;
Expand All @@ -17,8 +9,6 @@
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;
Expand All @@ -28,6 +18,18 @@
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)
*/
Expand Down Expand Up @@ -219,10 +221,10 @@ public void testApolloConfigChangeListenerWithInterestedKeys() throws Exception
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());

Expand Down
Expand Up @@ -3,6 +3,7 @@
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;
Expand Down Expand Up @@ -146,10 +147,10 @@ public void testApolloConfigChangeListenerWithInterestedKeys() throws Exception
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());

Expand Down
Expand Up @@ -151,7 +151,9 @@ public void addOrModifyProperty(String namespace, String someKey, String someVal
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);
}
}

Expand Down
@@ -1,16 +1,11 @@
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;
Expand All @@ -22,6 +17,12 @@
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
*/
Expand All @@ -37,6 +38,9 @@ public class ApolloMockServerSpringIntegrationTest {
@Autowired
private TestBean testBean;

@Autowired
private TestInterestedKeyPrefixesBean testInterestedKeyPrefixesBean;

@Test
@DirtiesContext
public void testPropertyInject() {
Expand All @@ -63,6 +67,25 @@ public void testListenerTriggeredByDel()
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 {
Expand All @@ -71,6 +94,11 @@ static class TestConfiguration {
public TestBean testBean() {
return new TestBean();
}

@Bean
public TestInterestedKeyPrefixesBean testInterestedKeyPrefixesBean() {
return new TestInterestedKeyPrefixesBean();
}
}

private static class TestBean {
Expand All @@ -87,4 +115,13 @@ private void onChange(ConfigChangeEvent 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);
}
}
}

0 comments on commit 4fa65a1

Please sign in to comment.