ソースを参照

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

feature: support listeners on events based on key patterns. #1461
kezhenxu94 6 年 前
コミット
4fa65a1d4a

+ 15 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/Config.java

@@ -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
    *

+ 40 - 17
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfig.java

@@ -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;
@@ -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,14 +462,28 @@ 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
     }
 
-    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;
+          }
+        }
       }
     }
 

+ 9 - 4
apollo-client/src/main/java/com/ctrip/framework/apollo/spring/annotation/ApolloAnnotationProcessor.java

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

+ 12 - 3
apollo-client/src/main/java/com/ctrip/framework/apollo/spring/annotation/ApolloConfigChangeListener.java

@@ -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.
  *
@@ -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 {};
 }

+ 14 - 12
apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigAnnotationTest.java

@@ -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;
@@ -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 - 2
apollo-client/src/test/java/com/ctrip/framework/apollo/spring/XMLConfigAnnotationTest.java

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

+ 3 - 1
apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/EmbeddedApollo.java

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

+ 42 - 5
apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ApolloMockServerSpringIntegrationTest.java

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