diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/ConfigFile.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/ConfigFile.java index e880f4df489d103776f7d3316377c974d5c97da9..5bff8a120fad2862d436b22d141b8efe669a20cd 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/ConfigFile.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/ConfigFile.java @@ -29,4 +29,11 @@ public interface ConfigFile { * @return the config file format enum */ ConfigFileFormat getConfigFileFormat(); + + /** + * Add change listener to this config file instance. + * + * @param listener the config file change listener + */ + void addChangeListener(ConfigFileChangeListener listener); } diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/ConfigFileChangeListener.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/ConfigFileChangeListener.java new file mode 100644 index 0000000000000000000000000000000000000000..478c1aac6383b3b2c04752781f640e5a8b11dd41 --- /dev/null +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/ConfigFileChangeListener.java @@ -0,0 +1,14 @@ +package com.ctrip.framework.apollo; + +import com.ctrip.framework.apollo.model.ConfigFileChangeEvent; + +/** + * @author Jason Song(song_s@ctrip.com) + */ +public interface ConfigFileChangeListener { + /** + * Invoked when there is any config change for the namespace. + * @param changeEvent the event for this change + */ + void onChange(ConfigFileChangeEvent changeEvent); +} diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfigFile.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfigFile.java index c6bb2ffd1e51adfb9ab18583144aa9cf83b6762d..7867a7908a399cd644a0ed339ab7447559039704 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfigFile.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/AbstractConfigFile.java @@ -1,23 +1,40 @@ package com.ctrip.framework.apollo.internals; +import java.util.List; +import java.util.Objects; import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.ctrip.framework.apollo.ConfigFile; +import com.ctrip.framework.apollo.ConfigFileChangeListener; +import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory; +import com.ctrip.framework.apollo.enums.PropertyChangeType; +import com.ctrip.framework.apollo.model.ConfigFileChangeEvent; import com.ctrip.framework.apollo.tracer.Tracer; +import com.ctrip.framework.apollo.tracer.spi.Transaction; import com.ctrip.framework.apollo.util.ExceptionUtil; +import com.google.common.collect.Lists; /** * @author Jason Song(song_s@ctrip.com) */ public abstract class AbstractConfigFile implements ConfigFile, RepositoryChangeListener { private static final Logger logger = LoggerFactory.getLogger(AbstractConfigFile.class); + private static ExecutorService m_executorService; protected ConfigRepository m_configRepository; protected String m_namespace; protected AtomicReference m_configProperties; + private List m_listeners = Lists.newCopyOnWriteArrayList(); + + static { + m_executorService = Executors.newCachedThreadPool(ApolloThreadFactory + .create("ConfigFile", true)); + } public AbstractConfigFile(String namespace, ConfigRepository configRepository) { m_configRepository = configRepository; @@ -45,6 +62,8 @@ public abstract class AbstractConfigFile implements ConfigFile, RepositoryChange return m_namespace; } + protected abstract void update(Properties newProperties); + @Override public synchronized void onRepositoryChange(String namespace, Properties newProperties) { if (newProperties.equals(m_configProperties.get())) { @@ -53,9 +72,51 @@ public abstract class AbstractConfigFile implements ConfigFile, RepositoryChange Properties newConfigProperties = new Properties(); newConfigProperties.putAll(newProperties); - m_configProperties.set(newConfigProperties); + String oldValue = getContent(); + + update(newProperties); + + String newValue = getContent(); + + PropertyChangeType changeType = PropertyChangeType.MODIFIED; + + if (oldValue == null) { + changeType = PropertyChangeType.ADDED; + } else if (newValue == null) { + changeType = PropertyChangeType.DELETED; + } + + this.fireConfigChange(new ConfigFileChangeEvent(m_namespace, oldValue, newValue, changeType)); Tracer.logEvent("Apollo.Client.ConfigChanges", m_namespace); } + @Override + public void addChangeListener(ConfigFileChangeListener listener) { + if (!m_listeners.contains(listener)) { + m_listeners.add(listener); + } + } + + private void fireConfigChange(final ConfigFileChangeEvent changeEvent) { + for (final ConfigFileChangeListener listener : m_listeners) { + m_executorService.submit(new Runnable() { + @Override + public void run() { + String listenerName = listener.getClass().getName(); + Transaction transaction = Tracer.newTransaction("Apollo.ConfigFileChangeListener", listenerName); + try { + listener.onChange(changeEvent); + transaction.setStatus(Transaction.SUCCESS); + } catch (Throwable ex) { + transaction.setStatus(ex); + Tracer.logError(ex); + logger.error("Failed to invoke config file change listener {}", listenerName, ex); + } finally { + transaction.complete(); + } + } + }); + } + } } diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PlainTextConfigFile.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PlainTextConfigFile.java index 056ec77b3d0b90f3458880f3b5bc1f7aeecc5e6b..cc9aedaafa302269f19a6dddc4eea5e1e970b444 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PlainTextConfigFile.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PlainTextConfigFile.java @@ -1,11 +1,13 @@ package com.ctrip.framework.apollo.internals; import com.ctrip.framework.apollo.core.ConfigConsts; +import java.util.Properties; /** * @author Jason Song(song_s@ctrip.com) */ public abstract class PlainTextConfigFile extends AbstractConfigFile { + public PlainTextConfigFile(String namespace, ConfigRepository configRepository) { super(namespace, configRepository); } @@ -25,4 +27,9 @@ public abstract class PlainTextConfigFile extends AbstractConfigFile { } return m_configProperties.get().containsKey(ConfigConsts.CONFIG_FILE_CONTENT_KEY); } + + @Override + protected void update(Properties newProperties) { + m_configProperties.set(newProperties); + } } diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PropertiesConfigFile.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PropertiesConfigFile.java index ee9e81027892902a23d2cfdaf6d5fc6ade3af662..26262a046ba49fb43a1751db9b5d4f7974467382 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PropertiesConfigFile.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PropertiesConfigFile.java @@ -25,6 +25,12 @@ public class PropertiesConfigFile extends AbstractConfigFile { m_contentCache = new AtomicReference<>(); } + @Override + protected void update(Properties newProperties) { + m_configProperties.set(newProperties); + m_contentCache.set(null); + } + @Override public String getContent() { if (m_contentCache.get() == null) { @@ -60,9 +66,4 @@ public class PropertiesConfigFile extends AbstractConfigFile { return ConfigFileFormat.Properties; } - @Override - public synchronized void onRepositoryChange(String namespace, Properties newProperties) { - super.onRepositoryChange(namespace, newProperties); - m_contentCache.set(null); - } } diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/model/ConfigFileChangeEvent.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/model/ConfigFileChangeEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..8ac4ac5cfd7186d18411c5cdc480adf315f40da2 --- /dev/null +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/model/ConfigFileChangeEvent.java @@ -0,0 +1,56 @@ +package com.ctrip.framework.apollo.model; + +import com.ctrip.framework.apollo.enums.PropertyChangeType; + +/** + * @author Jason Song(song_s@ctrip.com) + */ +public class ConfigFileChangeEvent { + private final String namespace; + private final String oldValue; + private final String newValue; + private final PropertyChangeType changeType; + + /** + * Constructor. + * + * @param namespace the namespace of the config file change event + * @param oldValue the value before change + * @param newValue the value after change + * @param changeType the change type + */ + public ConfigFileChangeEvent(String namespace, String oldValue, String newValue, + PropertyChangeType changeType) { + this.namespace = namespace; + this.oldValue = oldValue; + this.newValue = newValue; + this.changeType = changeType; + } + + public String getNamespace() { + return namespace; + } + + public String getOldValue() { + return oldValue; + } + + public String getNewValue() { + return newValue; + } + + public PropertyChangeType getChangeType() { + return changeType; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ConfigFileChangeEvent{"); + sb.append("namespace='").append(namespace).append('\''); + sb.append(", oldValue='").append(oldValue).append('\''); + sb.append(", newValue='").append(newValue).append('\''); + sb.append(", changeType=").append(changeType); + sb.append('}'); + return sb.toString(); + } +} diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/ConfigServiceTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/ConfigServiceTest.java index 7944d7f3f1562f9ed71967df6a625ccc72efb1b3..2bc7357d9a3df6502dd7748cb652c757b97adace 100644 --- a/apollo-client/src/test/java/com/ctrip/framework/apollo/ConfigServiceTest.java +++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/ConfigServiceTest.java @@ -132,6 +132,11 @@ public class ConfigServiceTest { public ConfigFileFormat getConfigFileFormat() { return m_configFileFormat; } + + @Override + public void addChangeListener(ConfigFileChangeListener listener) { + + } } public static class MockConfigFactory implements ConfigFactory { diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/DefaultConfigManagerTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/DefaultConfigManagerTest.java index 574a4d893936c1c7a023cce903bfd6aa1c80a892..506ec994a8ca1d835fe6f23d8b48ffa447b35766 100644 --- a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/DefaultConfigManagerTest.java +++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/DefaultConfigManagerTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; +import java.util.Properties; import java.util.Set; import org.junit.Before; @@ -110,6 +111,11 @@ public class DefaultConfigManagerTest { ConfigRepository someConfigRepository = mock(ConfigRepository.class); return new AbstractConfigFile(namespace, someConfigRepository) { + @Override + protected void update(Properties newProperties) { + + } + @Override public String getContent() { return someConfigContent; diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/PropertiesConfigFileTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/PropertiesConfigFileTest.java index 995fae7ac876584378671099888b777cfdbcc6a1..1a221ed9ef67337bb57261d6b94c3cf809d2ca2e 100644 --- a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/PropertiesConfigFileTest.java +++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/PropertiesConfigFileTest.java @@ -6,8 +6,14 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; +import com.ctrip.framework.apollo.ConfigFile; +import com.ctrip.framework.apollo.ConfigFileChangeListener; +import com.ctrip.framework.apollo.enums.PropertyChangeType; +import com.ctrip.framework.apollo.model.ConfigFileChangeEvent; +import com.google.common.util.concurrent.SettableFuture; import java.util.Properties; +import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -84,10 +90,27 @@ public class PropertiesConfigFileTest { Properties anotherProperties = new Properties(); anotherProperties.setProperty(someKey, anotherValue); + final SettableFuture configFileChangeFuture = SettableFuture.create(); + ConfigFileChangeListener someListener = new ConfigFileChangeListener() { + @Override + public void onChange(ConfigFileChangeEvent changeEvent) { + configFileChangeFuture.set(changeEvent); + } + }; + + configFile.addChangeListener(someListener); + configFile.onRepositoryChange(someNamespace, anotherProperties); + ConfigFileChangeEvent changeEvent = configFileChangeFuture.get(500, TimeUnit.MILLISECONDS); + assertFalse(configFile.getContent().contains(String.format("%s=%s", someKey, someValue))); assertTrue(configFile.getContent().contains(String.format("%s=%s", someKey, anotherValue))); + + assertEquals(someNamespace, changeEvent.getNamespace()); + assertTrue(changeEvent.getOldValue().contains(String.format("%s=%s", someKey, someValue))); + assertTrue(changeEvent.getNewValue().contains(String.format("%s=%s", someKey, anotherValue))); + assertEquals(PropertyChangeType.MODIFIED, changeEvent.getChangeType()); } @Test diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/XmlConfigFileTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/XmlConfigFileTest.java index 3d01d9fbaa49581243944876249ce0d995589804..13552029ec8010be8bb09958e906db3b78ba37d1 100644 --- a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/XmlConfigFileTest.java +++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/XmlConfigFileTest.java @@ -6,8 +6,13 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; +import com.ctrip.framework.apollo.ConfigFileChangeListener; +import com.ctrip.framework.apollo.enums.PropertyChangeType; +import com.ctrip.framework.apollo.model.ConfigFileChangeEvent; +import com.google.common.util.concurrent.SettableFuture; import java.util.Properties; +import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -85,9 +90,97 @@ public class XmlConfigFileTest { Properties anotherProperties = new Properties(); anotherProperties.setProperty(key, anotherValue); + final SettableFuture configFileChangeFuture = SettableFuture.create(); + ConfigFileChangeListener someListener = new ConfigFileChangeListener() { + @Override + public void onChange(ConfigFileChangeEvent changeEvent) { + configFileChangeFuture.set(changeEvent); + } + }; + + configFile.addChangeListener(someListener); + configFile.onRepositoryChange(someNamespace, anotherProperties); + ConfigFileChangeEvent changeEvent = configFileChangeFuture.get(500, TimeUnit.MILLISECONDS); + assertEquals(anotherValue, configFile.getContent()); + assertEquals(someNamespace, changeEvent.getNamespace()); + assertEquals(someValue, changeEvent.getOldValue()); + assertEquals(anotherValue, changeEvent.getNewValue()); + assertEquals(PropertyChangeType.MODIFIED, changeEvent.getChangeType()); + } + + @Test + public void testOnRepositoryChangeWithContentAdded() throws Exception { + Properties someProperties = new Properties(); + String key = ConfigConsts.CONFIG_FILE_CONTENT_KEY; + String someValue = "someValue"; + + when(configRepository.getConfig()).thenReturn(someProperties); + + XmlConfigFile configFile = new XmlConfigFile(someNamespace, configRepository); + + assertEquals(null, configFile.getContent()); + + Properties anotherProperties = new Properties(); + anotherProperties.setProperty(key, someValue); + + final SettableFuture configFileChangeFuture = SettableFuture.create(); + ConfigFileChangeListener someListener = new ConfigFileChangeListener() { + @Override + public void onChange(ConfigFileChangeEvent changeEvent) { + configFileChangeFuture.set(changeEvent); + } + }; + + configFile.addChangeListener(someListener); + + configFile.onRepositoryChange(someNamespace, anotherProperties); + + ConfigFileChangeEvent changeEvent = configFileChangeFuture.get(500, TimeUnit.MILLISECONDS); + + assertEquals(someValue, configFile.getContent()); + assertEquals(someNamespace, changeEvent.getNamespace()); + assertEquals(null, changeEvent.getOldValue()); + assertEquals(someValue, changeEvent.getNewValue()); + assertEquals(PropertyChangeType.ADDED, changeEvent.getChangeType()); + } + + @Test + public void testOnRepositoryChangeWithContentDeleted() throws Exception { + Properties someProperties = new Properties(); + String key = ConfigConsts.CONFIG_FILE_CONTENT_KEY; + String someValue = "someValue"; + someProperties.setProperty(key, someValue); + + when(configRepository.getConfig()).thenReturn(someProperties); + + XmlConfigFile configFile = new XmlConfigFile(someNamespace, configRepository); + + assertEquals(someValue, configFile.getContent()); + + Properties anotherProperties = new Properties(); + + final SettableFuture configFileChangeFuture = SettableFuture.create(); + ConfigFileChangeListener someListener = new ConfigFileChangeListener() { + @Override + public void onChange(ConfigFileChangeEvent changeEvent) { + configFileChangeFuture.set(changeEvent); + } + }; + + configFile.addChangeListener(someListener); + + configFile.onRepositoryChange(someNamespace, anotherProperties); + + ConfigFileChangeEvent changeEvent = configFileChangeFuture.get(500, TimeUnit.MILLISECONDS); + + assertEquals(null, configFile.getContent()); + assertEquals(someNamespace, changeEvent.getNamespace()); + assertEquals(someValue, changeEvent.getOldValue()); + assertEquals(null, changeEvent.getNewValue()); + assertEquals(PropertyChangeType.DELETED, changeEvent.getChangeType()); } @Test diff --git a/apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/api/ApolloConfigDemo.java b/apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/api/ApolloConfigDemo.java index 1f7949295eae8d6bc9d68829ffb991b095af86bd..62f39f0bb433381130ac90f24388dcb54308109c 100644 --- a/apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/api/ApolloConfigDemo.java +++ b/apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/api/ApolloConfigDemo.java @@ -5,10 +5,12 @@ import com.google.common.base.Charsets; import com.ctrip.framework.apollo.Config; import com.ctrip.framework.apollo.ConfigChangeListener; import com.ctrip.framework.apollo.ConfigFile; +import com.ctrip.framework.apollo.ConfigFileChangeListener; import com.ctrip.framework.apollo.ConfigService; import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; import com.ctrip.framework.apollo.model.ConfigChange; import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import com.ctrip.framework.apollo.model.ConfigFileChangeEvent; import com.ctrip.framework.foundation.Foundation; import org.slf4j.Logger; @@ -48,6 +50,12 @@ public class ApolloConfigDemo { publicConfig.addChangeListener(changeListener); applicationConfigFile = ConfigService.getConfigFile("application", ConfigFileFormat.Properties); xmlConfigFile = ConfigService.getConfigFile("datasources", ConfigFileFormat.XML); + xmlConfigFile.addChangeListener(new ConfigFileChangeListener() { + @Override + public void onChange(ConfigFileChangeEvent changeEvent) { + logger.info(changeEvent.toString()); + } + }); } private String getConfig(String key) {