Commit 5bb2aaf1 by Yiming Liu

Merge pull request #79 from nobodyiam/client-refresh-merge

Add client side auto refresh capability
parents b1c4b7d8 df474afd
......@@ -13,8 +13,7 @@ import com.ctrip.apollo.biz.entity.Release;
*/
public interface ReleaseRepository extends PagingAndSortingRepository<Release, Long> {
@Query("SELECT r FROM Release r WHERE r.appId = :appId AND r.clusterName = :clusterName AND r.namespaceName = :namespaceName order by r.id desc")
Release findLatest(@Param("appId") String appId, @Param("clusterName") String clusterName,
Release findFirstByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(@Param("appId") String appId, @Param("clusterName") String clusterName,
@Param("namespaceName") String namespaceName);
List<Release> findByAppIdAndClusterNameAndNamespaceName(String appId, String clusterName,
......
......@@ -30,7 +30,7 @@ public class ConfigService {
private Type configurationTypeReference = new TypeToken<Map<String, String>>(){}.getType();
public Release findRelease(String appId, String clusterName, String namespaceName) {
Release release = releaseRepository.findLatest(appId, clusterName, namespaceName);
Release release = releaseRepository.findFirstByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(appId, clusterName, namespaceName);
return release;
}
......
......@@ -12,4 +12,6 @@ public interface Config {
* @return the property value
*/
public String getProperty(String key, String defaultValue);
public void addChangeListener(ConfigChangeListener listener);
}
package com.ctrip.apollo;
import com.ctrip.apollo.model.ConfigChangeEvent;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public interface ConfigChangeListener {
public void onChange(ConfigChangeEvent changeEvent);
}
package com.ctrip.apollo.internals;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.ctrip.apollo.Config;
import com.ctrip.apollo.ConfigChangeListener;
import com.ctrip.apollo.enums.PropertyChangeType;
import com.ctrip.apollo.model.ConfigChange;
import com.ctrip.apollo.model.ConfigChangeEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Properties;
import java.util.Set;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public abstract class AbstractConfig implements Config {
private static final Logger logger = LoggerFactory.getLogger(AbstractConfig.class);
private List<ConfigChangeListener> m_listeners = Lists.newCopyOnWriteArrayList();
@Override
public void addChangeListener(ConfigChangeListener listener) {
if (!m_listeners.contains(listener)) {
m_listeners.add(listener);
}
}
protected void fireConfigChange(ConfigChangeEvent changeEvent) {
for (ConfigChangeListener listener : m_listeners) {
try {
listener.onChange(changeEvent);
} catch (Throwable t) {
logger.error("Failed to invoke config change listener {}", listener.getClass(), t);
}
}
}
List<ConfigChange> calcPropertyChanges(Properties previous,
Properties current) {
if (previous == null) {
previous = new Properties();
}
if (current == null) {
current = new Properties();
}
Set<String> previousKeys = previous.stringPropertyNames();
Set<String> currentKeys = current.stringPropertyNames();
Set<String> commonKeys = Sets.intersection(previousKeys, currentKeys);
Set<String> newKeys = Sets.difference(currentKeys, commonKeys);
Set<String> removedKeys = Sets.difference(previousKeys, commonKeys);
List<ConfigChange> changes = Lists.newArrayList();
for (String newKey : newKeys) {
changes.add(new ConfigChange(newKey, null, current.getProperty(newKey), PropertyChangeType.NEW));
}
for (String removedKey : removedKeys) {
changes.add(new ConfigChange(removedKey, previous.getProperty(removedKey), null,
PropertyChangeType.DELETED));
}
for (String commonKey : commonKeys) {
String previousValue = previous.getProperty(commonKey);
String currentValue = current.getProperty(commonKey);
if (Objects.equal(previousValue, currentValue)) {
continue;
}
changes.add(new ConfigChange(commonKey, previousValue,
currentValue, PropertyChangeType.MODIFIED));
}
return changes;
}
}
package com.ctrip.apollo.internals;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Properties;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public abstract class AbstractConfigRepository implements ConfigRepository {
private static final Logger logger = LoggerFactory.getLogger(AbstractConfigRepository.class);
private List<RepositoryChangeListener> m_listeners = Lists.newCopyOnWriteArrayList();
@Override
public void addChangeListener(RepositoryChangeListener listener) {
if (!m_listeners.contains(listener)) {
m_listeners.add(listener);
}
}
@Override
public void removeChangeListener(RepositoryChangeListener listener) {
m_listeners.remove(listener);
}
protected void fireRepositoryChange(String namespace, Properties newProperties) {
for (RepositoryChangeListener listener : m_listeners) {
try {
listener.onRepositoryChange(namespace, newProperties);
} catch (Throwable t) {
logger.error("Failed to invoke repository change listener {}", listener.getClass(), t);
}
}
}
}
......@@ -6,7 +6,19 @@ import java.util.Properties;
* @author Jason Song(song_s@ctrip.com)
*/
public interface ConfigRepository {
public Properties loadConfig();
/**
* Get the config from this repository
* @return
*/
public Properties getConfig();
/**
* Set the fallback repo for this repository
* @param fallbackConfigRepository
*/
public void setFallback(ConfigRepository fallbackConfigRepository);
public void addChangeListener(RepositoryChangeListener listener);
public void removeChangeListener(RepositoryChangeListener listener);
}
package com.ctrip.apollo.internals;
import com.ctrip.apollo.Config;
import com.google.common.collect.ImmutableMap;
import com.ctrip.apollo.core.utils.ClassLoaderUtil;
import com.ctrip.apollo.enums.PropertyChangeType;
import com.ctrip.apollo.model.ConfigChange;
import com.ctrip.apollo.model.ConfigChangeEvent;
import com.dianping.cat.Cat;
import org.slf4j.Logger;
......@@ -9,12 +13,15 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public class DefaultConfig implements Config {
public class DefaultConfig extends AbstractConfig implements RepositoryChangeListener {
private static final Logger logger = LoggerFactory.getLogger(DefaultConfig.class);
private final String m_namespace;
private Properties m_resourceProperties;
......@@ -30,7 +37,8 @@ public class DefaultConfig implements Config {
private void initialize() {
try {
m_configProperties = m_configRepository.loadConfig();
m_configProperties = m_configRepository.getConfig();
m_configRepository.addChangeListener(this);
} catch (Throwable ex) {
String message = String.format("Init Apollo Local Config failed - namespace: %s",
m_namespace);
......@@ -68,6 +76,69 @@ public class DefaultConfig implements Config {
return value == null ? defaultValue : value;
}
@Override
public synchronized void onRepositoryChange(String namespace, Properties newProperties) {
if (newProperties.equals(m_configProperties)) {
return;
}
Properties newConfigProperties = new Properties();
newConfigProperties.putAll(newProperties);
Map<String, ConfigChange> actualChanges = updateAndCalcConfigChanges(newConfigProperties);
this.fireConfigChange(new ConfigChangeEvent(m_namespace, actualChanges));
}
private Map<String, ConfigChange> updateAndCalcConfigChanges(Properties newConfigProperties) {
List<ConfigChange> configChanges =
calcPropertyChanges(m_configProperties, newConfigProperties);
// List<ConfigChange> actualChanges = Lists.newArrayListWithCapacity(configChanges.size());
ImmutableMap.Builder<String, ConfigChange> actualChanges =
new ImmutableMap.Builder<>();
/** === Double check since DefaultConfig has multiple config sources ==== **/
//1. use getProperty to update configChanges's old value
for (ConfigChange change : configChanges) {
change.setOldValue(this.getProperty(change.getPropertyName(), change.getOldValue()));
}
//2. update m_configProperties
m_configProperties = newConfigProperties;
//3. use getProperty to update configChange's new value and calc the final changes
for (ConfigChange change : configChanges) {
change.setNewValue(this.getProperty(change.getPropertyName(), change.getNewValue()));
switch (change.getChangeType()) {
case NEW:
if (Objects.equals(change.getOldValue(), change.getNewValue())) {
break;
}
if (!Objects.isNull(change.getOldValue())) {
change.setChangeType(PropertyChangeType.MODIFIED);
}
actualChanges.put(change.getPropertyName(), change);
break;
case MODIFIED:
if (!Objects.equals(change.getOldValue(), change.getNewValue())) {
actualChanges.put(change.getPropertyName(), change);
}
break;
case DELETED:
if (Objects.equals(change.getOldValue(), change.getNewValue())) {
break;
}
if (!Objects.isNull(change.getNewValue())) {
change.setChangeType(PropertyChangeType.MODIFIED);
}
actualChanges.put(change.getPropertyName(), change);
break;
}
}
return actualChanges.build();
}
private Properties loadFromResource(String namespace) {
String name = String.format("META-INF/config/%s.properties", namespace);
InputStream in = ClassLoaderUtil.getLoader().getResourceAsStream(name);
......@@ -92,6 +163,4 @@ public class DefaultConfig implements Config {
return properties;
}
}
......@@ -19,7 +19,7 @@ public class DefaultConfigManager implements ConfigManager {
@Inject
private ConfigFactoryManager m_factoryManager;
private Map<String, Config> m_configs = Maps.newHashMap();
private Map<String, Config> m_configs = Maps.newConcurrentMap();
@Override
public Config getConfig(String namespace) {
......
......@@ -22,14 +22,15 @@ import java.util.Properties;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public class LocalFileConfigRepository implements ConfigRepository {
public class LocalFileConfigRepository extends AbstractConfigRepository
implements RepositoryChangeListener {
private static final Logger logger = LoggerFactory.getLogger(LocalFileConfigRepository.class);
private PlexusContainer m_container;
private final PlexusContainer m_container;
private final String m_namespace;
private final File m_baseDir;
private final ConfigUtil m_configUtil;
private Properties m_fileProperties;
private ConfigRepository m_fallback;
private volatile Properties m_fileProperties;
private volatile ConfigRepository m_fallback;
public LocalFileConfigRepository(File baseDir, String namespace) {
m_baseDir = baseDir;
......@@ -43,7 +44,7 @@ public class LocalFileConfigRepository implements ConfigRepository {
}
@Override
public Properties loadConfig() {
public Properties getConfig() {
if (m_fileProperties == null) {
initLocalConfig();
}
......@@ -54,26 +55,51 @@ public class LocalFileConfigRepository implements ConfigRepository {
@Override
public void setFallback(ConfigRepository fallbackConfigRepository) {
//clear previous listener
if (m_fallback != null) {
m_fallback.removeChangeListener(this);
}
m_fallback = fallbackConfigRepository;
fallbackConfigRepository.addChangeListener(this);
}
void initLocalConfig() {
if (m_fallback != null) {
try {
m_fileProperties = m_fallback.loadConfig();
//TODO register change listener
persistLocalCacheFile(m_baseDir, m_namespace);
return;
} catch (Throwable ex) {
logger.error("Load config from fallback loader failed", ex);
Cat.logError(ex);
}
@Override
public synchronized void onRepositoryChange(String namespace, Properties newProperties) {
if (newProperties.equals(m_fileProperties)) {
return;
}
Properties newFileProperties = new Properties();
newFileProperties.putAll(newProperties);
this.m_fileProperties = newFileProperties;
persistLocalCacheFile(m_baseDir, m_namespace);
this.fireRepositoryChange(namespace, newProperties);
}
void initLocalConfig() {
try {
m_fileProperties = this.loadFromLocalCacheFile(m_baseDir, m_namespace);
} catch (IOException ex) {
throw new RuntimeException("Loading config from local cache file failed", ex);
} catch (Throwable ex) {
logger.error("Load config from local config cache file failed", ex);
}
//TODO check whether properties is expired or should we return after it's synced with fallback?
if (m_fileProperties != null) {
return;
}
if (m_fallback == null) {
throw new RuntimeException(
"Load config from local config cache failed and there is no fallback repository!");
}
try {
m_fileProperties = m_fallback.getConfig();
persistLocalCacheFile(m_baseDir, m_namespace);
} catch (Throwable ex) {
String message =
String.format("Load config from fallback repository %s failed", m_fallback.getClass());
logger.error(message, ex);
throw new RuntimeException(message, ex);
}
}
......
......@@ -5,6 +5,7 @@ import com.google.common.collect.Lists;
import com.ctrip.apollo.core.dto.ApolloConfig;
import com.ctrip.apollo.core.dto.ServiceDTO;
import com.ctrip.apollo.core.utils.ApolloThreadFactory;
import com.ctrip.apollo.util.ConfigUtil;
import com.ctrip.apollo.util.http.HttpRequest;
import com.ctrip.apollo.util.http.HttpResponse;
......@@ -18,19 +19,22 @@ import org.unidal.lookup.ContainerLoader;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public class RemoteConfigRepository implements ConfigRepository {
public class RemoteConfigRepository extends AbstractConfigRepository{
private static final Logger logger = LoggerFactory.getLogger(RemoteConfigRepository.class);
private PlexusContainer m_container;
private ConfigServiceLocator m_serviceLocator;
private HttpUtil m_httpUtil;
private ConfigUtil m_configUtil;
private AtomicReference<ApolloConfig> m_configCache;
private String m_namespace;
private final ConfigServiceLocator m_serviceLocator;
private final HttpUtil m_httpUtil;
private final ConfigUtil m_configUtil;
private volatile AtomicReference<ApolloConfig> m_configCache;
private final String m_namespace;
private final ScheduledExecutorService m_executorService;
public RemoteConfigRepository(String namespace) {
m_namespace = namespace;
......@@ -43,12 +47,15 @@ public class RemoteConfigRepository implements ConfigRepository {
} catch (ComponentLookupException e) {
throw new IllegalStateException("Unable to load component!", e);
}
this.m_executorService = Executors.newScheduledThreadPool(1,
ApolloThreadFactory.create("RemoteConfigRepository", true));
this.schedulePeriodicRefresh();
}
@Override
public Properties loadConfig() {
public Properties getConfig() {
if (m_configCache.get() == null) {
initRemoteConfig();
this.loadRemoteConfig();
}
return transformApolloConfigToProperties(m_configCache.get());
}
......@@ -58,8 +65,37 @@ public class RemoteConfigRepository implements ConfigRepository {
//remote config doesn't need fallback
}
private void initRemoteConfig() {
m_configCache.set(this.loadApolloConfig());
private void schedulePeriodicRefresh() {
logger.info("Schedule periodic refresh with interval: {} {}",
m_configUtil.getRefreshInterval(), m_configUtil.getRefreshTimeUnit());
this.m_executorService.scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
try {
loadRemoteConfig();
} catch (Throwable ex) {
logger.error("Refreshing config failed", ex);
}
}
}, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),
m_configUtil.getRefreshTimeUnit());
}
synchronized void loadRemoteConfig() {
ApolloConfig previous = m_configCache.get();
ApolloConfig current = loadApolloConfig();
//HTTP 304, nothing changed
if (previous == current) {
return;
}
logger.info("Remote Config changes!");
m_configCache.set(current);
this.fireRepositoryChange(m_namespace, this.getConfig());
}
private Properties transformApolloConfigToProperties(ApolloConfig apolloConfig) {
......@@ -72,16 +108,15 @@ public class RemoteConfigRepository implements ConfigRepository {
private ApolloConfig loadApolloConfig() {
String appId = m_configUtil.getAppId();
String cluster = m_configUtil.getCluster();
String uri = getConfigServiceUrl();
String url = assembleUrl(getConfigServiceUrl(), appId, cluster, m_namespace, m_configCache.get());
logger.info("Loading config from {}, appId={}, cluster={}, namespace={}", uri, appId, cluster,
m_namespace);
HttpRequest request =
new HttpRequest(assembleUrl(uri, appId, cluster, m_namespace, m_configCache.get()));
logger.info("Loading config from {}", url);
HttpRequest request = new HttpRequest(url);
try {
HttpResponse<ApolloConfig> response = m_httpUtil.doGet(request, ApolloConfig.class);
if (response.getStatusCode() == 304) {
logger.info("Config server responds with 304 HTTP status code.");
return m_configCache.get();
}
......
package com.ctrip.apollo.internals;
import java.util.Properties;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public interface RepositoryChangeListener {
public void onRepositoryChange(String namespace, Properties newProperties);
}
package com.ctrip.apollo.internals;
import com.ctrip.apollo.Config;
import com.google.common.base.Function;
import com.google.common.collect.Maps;
import com.ctrip.apollo.model.ConfigChange;
import com.ctrip.apollo.model.ConfigChangeEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public class SimpleConfig implements Config {
public class SimpleConfig extends AbstractConfig implements RepositoryChangeListener {
private static final Logger logger = LoggerFactory.getLogger(SimpleConfig.class);
private String m_namespace;
private ConfigRepository m_configRepository;
private Properties m_configProperties;
private final String m_namespace;
private final ConfigRepository m_configRepository;
private volatile Properties m_configProperties;
public SimpleConfig(String namespace, ConfigRepository configRepository) {
m_namespace = namespace;
......@@ -24,9 +30,10 @@ public class SimpleConfig implements Config {
private void initialize() {
try {
m_configProperties = m_configRepository.loadConfig();
m_configProperties = m_configRepository.getConfig();
m_configRepository.addChangeListener(this);
} catch (Throwable ex) {
String message = String.format("Init Apollo Remote Config failed - namespace: %s",
String message = String.format("Init Apollo Simple Config failed - namespace: %s",
m_namespace);
logger.error(message, ex);
throw new RuntimeException(message, ex);
......@@ -38,4 +45,25 @@ public class SimpleConfig implements Config {
return this.m_configProperties.getProperty(key, defaultValue);
}
@Override
public synchronized void onRepositoryChange(String namespace, Properties newProperties) {
if (newProperties.equals(m_configProperties)) {
return;
}
Properties newConfigProperties = new Properties();
newConfigProperties.putAll(newProperties);
List<ConfigChange> changes = calcPropertyChanges(m_configProperties, newConfigProperties);
Map<String, ConfigChange> changeMap = Maps.uniqueIndex(changes,
new Function<ConfigChange, String>() {
@Override
public String apply(ConfigChange input) {
return input.getPropertyName();
}
});
m_configProperties = newConfigProperties;
this.fireConfigChange(new ConfigChangeEvent(m_namespace, changeMap));
}
}
package com.ctrip.apollo.model;
import com.google.common.base.MoreObjects;
import com.ctrip.apollo.enums.PropertyChangeType;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public class PropertyChange {
private String propertyName;
private Object oldValue;
private Object newValue;
public class ConfigChange {
private final String propertyName;
private String oldValue;
private String newValue;
private PropertyChangeType changeType;
public PropertyChange(String propertyName, Object oldValue, Object newValue,
PropertyChangeType changeType) {
public ConfigChange(String propertyName, String oldValue, String newValue,
PropertyChangeType changeType) {
this.propertyName = propertyName;
this.oldValue = oldValue;
this.newValue = newValue;
......@@ -24,31 +26,38 @@ public class PropertyChange {
return propertyName;
}
public void setPropertyName(String propertyName) {
this.propertyName = propertyName;
}
public Object getOldValue() {
public String getOldValue() {
return oldValue;
}
public void setOldValue(Object oldValue) {
this.oldValue = oldValue;
public String getNewValue() {
return newValue;
}
public Object getNewValue() {
return newValue;
public PropertyChangeType getChangeType() {
return changeType;
}
public void setNewValue(Object newValue) {
this.newValue = newValue;
public void setOldValue(String oldValue) {
this.oldValue = oldValue;
}
public PropertyChangeType getChangeType() {
return changeType;
public void setNewValue(String newValue) {
this.newValue = newValue;
}
public void setChangeType(PropertyChangeType changeType) {
this.changeType = changeType;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.omitNullValues()
.add("propertyName", propertyName)
.add("oldValue", oldValue)
.add("newValue", newValue)
.add("changeType", changeType)
.toString();
}
}
package com.ctrip.apollo.model;
import java.util.Map;
import java.util.Set;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public class ConfigChangeEvent {
private final String m_namespace;
private final Map<String, ConfigChange> changes;
public ConfigChangeEvent(String m_namespace,
Map<String, ConfigChange> changes) {
this.m_namespace = m_namespace;
this.changes = changes;
}
public Set<String> changedKeys() {
return changes.keySet();
}
public ConfigChange getChange(String key) {
return changes.get(key);
}
/**
* Please note that the returned Map is immutable
* @return changes
*/
public Map<String, ConfigChange> getChanges() {
return changes;
}
public String getNamespace() {
return m_namespace;
}
}
package com.ctrip.apollo.model;
import com.google.common.collect.Lists;
import java.util.List;
import java.util.Properties;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public class ConfigRefreshResult {
private String m_namespace;
private Properties m_properties;
private List<PropertyChange> m_changes;
public ConfigRefreshResult(String namespace, Properties properties) {
this.m_namespace = namespace;
this.m_properties = properties;
m_changes = Lists.newArrayList();
}
public Properties getProperties() {
return m_properties;
}
public String getNamespace() {
return m_namespace;
}
public List<PropertyChange> getChanges() {
return m_changes;
}
public void setChanges(List<PropertyChange> changes) {
this.m_changes = changes;
}
public boolean hasChanges() {
return !m_changes.isEmpty();
}
}
......@@ -33,8 +33,7 @@ public class DefaultConfigFactory implements ConfigFactory {
}
LocalFileConfigRepository createLocalConfigRepository(String namespace) {
LocalFileConfigRepository
localFileConfigLoader =
LocalFileConfigRepository localFileConfigLoader =
new LocalFileConfigRepository(m_baseDir, namespace);
localFileConfigLoader.setFallback(createRemoteConfigRepository(namespace));
return localFileConfigLoader;
......
......@@ -39,18 +39,19 @@ public class HttpUtil {
conn.setRequestMethod("GET");
if (httpRequest.getConnectTimeout() < 0) {
conn.setConnectTimeout(m_configUtil.getConnectTimeout());
} else {
conn.setConnectTimeout(httpRequest.getConnectTimeout());
int connectTimeout = httpRequest.getConnectTimeout();
if (connectTimeout < 0) {
connectTimeout = m_configUtil.getConnectTimeout();
}
if (httpRequest.getReadTimeout() < 0) {
conn.setReadTimeout(m_configUtil.getReadTimeout());
} else {
conn.setReadTimeout(httpRequest.getReadTimeout());
int readTimeout = httpRequest.getReadTimeout();
if (readTimeout < 0) {
readTimeout = m_configUtil.getReadTimeout();
}
conn.setConnectTimeout(connectTimeout);
conn.setReadTimeout(readTimeout);
conn.connect();
int statusCode = conn.getResponseCode();
......@@ -66,7 +67,7 @@ public class HttpUtil {
}
throw new RuntimeException(
String.format("Get operation failed, status code - %d", statusCode));
String.format("Get operation failed for %s, status code - %d", httpRequest.getUrl(), statusCode));
} catch (Throwable ex) {
throw new RuntimeException("Could not complete get operation", ex);
......
......@@ -71,6 +71,11 @@ public class ConfigServiceTest extends ComponentTestCase {
return m_namespace + ":" + key;
}
@Override
public void addChangeListener(ConfigChangeListener listener) {
}
}
public static class MockConfigFactory implements ConfigFactory {
......
package com.ctrip.apollo.internals;
import com.ctrip.apollo.Config;
import com.ctrip.apollo.ConfigChangeListener;
import com.ctrip.apollo.spi.ConfigFactory;
import com.ctrip.apollo.spi.ConfigFactoryManager;
......@@ -60,6 +61,12 @@ public class DefaultConfigManagerTest extends ComponentTestCase {
public String getProperty(String key, String defaultValue) {
return namespace + ":" + key;
}
@Override
public void addChangeListener(ConfigChangeListener listener) {
}
};
}
};
......
package com.ctrip.apollo.internals;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import com.ctrip.apollo.ConfigChangeListener;
import com.ctrip.apollo.core.utils.ClassLoaderUtil;
import com.ctrip.apollo.enums.PropertyChangeType;
import com.ctrip.apollo.model.ConfigChange;
import com.ctrip.apollo.model.ConfigChangeEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.io.File;
import java.util.Properties;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
......@@ -69,7 +77,7 @@ public class DefaultConfigTest {
someProperties = new Properties();
someProperties.setProperty(someKey, someLocalFileValue);
someProperties.setProperty(anotherKey, someLocalFileValue);
when(configRepository.loadConfig()).thenReturn(someProperties);
when(configRepository.getConfig()).thenReturn(someProperties);
//set up resource file
File resourceFile = new File(someResourceDir, someNamespace + ".properties");
......@@ -79,9 +87,8 @@ public class DefaultConfigTest {
Files.append(System.getProperty("line.separator"), resourceFile, Charsets.UTF_8);
Files.append(lastKey + "=" + someResourceValue, resourceFile, Charsets.UTF_8);
DefaultConfig
defaultConfig =
new DefaultConfig(someNamespace, configRepository);
DefaultConfig defaultConfig =
new DefaultConfig(someNamespace, configRepository);
String someKeyValue = defaultConfig.getProperty(someKey, null);
String anotherKeyValue = defaultConfig.getProperty(anotherKey, null);
......@@ -95,4 +102,80 @@ public class DefaultConfigTest {
assertEquals(someResourceValue, lastKeyValue);
}
@Test
public void testOnRepositoryChange() throws Exception {
String someKey = "someKey";
String someSystemPropertyValue = "system-property-value";
String anotherKey = "anotherKey";
String someLocalFileValue = "local-file-value";
String keyToBeDeleted = "keyToBeDeleted";
String keyToBeDeletedValue = "keyToBeDeletedValue";
String yetAnotherKey = "yetAnotherKey";
String yetAnotherValue = "yetAnotherValue";
String yetAnotherResourceValue = "yetAnotherResourceValue";
//set up system property
System.setProperty(someKey, someSystemPropertyValue);
//set up config repo
someProperties = new Properties();
someProperties.putAll(ImmutableMap
.of(someKey, someLocalFileValue, anotherKey, someLocalFileValue, keyToBeDeleted,
keyToBeDeletedValue, yetAnotherKey, yetAnotherValue));
when(configRepository.getConfig()).thenReturn(someProperties);
//set up resource file
File resourceFile = new File(someResourceDir, someNamespace + ".properties");
Files.append(yetAnotherKey + "=" + yetAnotherResourceValue, resourceFile, Charsets.UTF_8);
DefaultConfig defaultConfig =
new DefaultConfig(someNamespace, configRepository);
ConfigChangeListener someListener = mock(ConfigChangeListener.class);
defaultConfig.addChangeListener(someListener);
Properties newProperties = new Properties();
String someKeyNewValue = "new-some-value";
String anotherKeyNewValue = "another-new-value";
String newKey = "newKey";
String newValue = "newValue";
newProperties.putAll(ImmutableMap
.of(someKey, someKeyNewValue, anotherKey, anotherKeyNewValue, newKey, newValue));
final ArgumentCaptor<ConfigChangeEvent> captor =
ArgumentCaptor.forClass(ConfigChangeEvent.class);
defaultConfig.onRepositoryChange(someNamespace, newProperties);
verify(someListener, times(1)).onChange(captor.capture());
ConfigChangeEvent changeEvent = captor.getValue();
assertEquals(someNamespace, changeEvent.getNamespace());
assertEquals(4, changeEvent.getChanges().size());
ConfigChange anotherKeyChange = changeEvent.getChange(anotherKey);
assertEquals(someLocalFileValue, anotherKeyChange.getOldValue());
assertEquals(anotherKeyNewValue, anotherKeyChange.getNewValue());
assertEquals(PropertyChangeType.MODIFIED, anotherKeyChange.getChangeType());
ConfigChange yetAnotherKeyChange = changeEvent.getChange(yetAnotherKey);
assertEquals(yetAnotherValue, yetAnotherKeyChange.getOldValue());
assertEquals(yetAnotherResourceValue, yetAnotherKeyChange.getNewValue());
assertEquals(PropertyChangeType.MODIFIED, yetAnotherKeyChange.getChangeType());
ConfigChange keyToBeDeletedChange = changeEvent.getChange(keyToBeDeleted);
assertEquals(keyToBeDeletedValue, keyToBeDeletedChange.getOldValue());
assertEquals(null, keyToBeDeletedChange.getNewValue());
assertEquals(PropertyChangeType.DELETED, keyToBeDeletedChange.getChangeType());
ConfigChange newKeyChange = changeEvent.getChange(newKey);
assertEquals(null, newKeyChange.getOldValue());
assertEquals(newValue, newKeyChange.getNewValue());
assertEquals(PropertyChangeType.NEW, newKeyChange.getChangeType());
}
}
......@@ -8,6 +8,7 @@ import com.ctrip.apollo.util.ConfigUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.unidal.lookup.ComponentTestCase;
import java.io.File;
......@@ -16,7 +17,11 @@ import java.util.Properties;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
......@@ -40,7 +45,7 @@ public class LocalFileConfigRepositoryTest extends ComponentTestCase {
someProperties = new Properties();
someProperties.setProperty("defaultKey", "defaultValue");
fallbackRepo = mock(ConfigRepository.class);
when(fallbackRepo.loadConfig()).thenReturn(someProperties);
when(fallbackRepo.getConfig()).thenReturn(someProperties);
defineComponent(ConfigUtil.class, MockConfigUtil.class);
}
......@@ -83,7 +88,7 @@ public class LocalFileConfigRepositoryTest extends ComponentTestCase {
Files.write(someKey + "=" + someValue, file, Charsets.UTF_8);
LocalFileConfigRepository localRepo = new LocalFileConfigRepository(someBaseDir, someNamespace);
Properties properties = localRepo.loadConfig();
Properties properties = localRepo.getConfig();
assertEquals(someValue, properties.getProperty(someKey));
......@@ -97,7 +102,7 @@ public class LocalFileConfigRepositoryTest extends ComponentTestCase {
localFileConfigRepository.setFallback(fallbackRepo);
Properties result = localFileConfigRepository.loadConfig();
Properties result = localFileConfigRepository.getConfig();
assertThat(
"LocalFileConfigRepository's properties should be the same as fallback repo's when there is no local cache",
......@@ -111,13 +116,13 @@ public class LocalFileConfigRepositoryTest extends ComponentTestCase {
localRepo.setFallback(fallbackRepo);
Properties someProperties = localRepo.loadConfig();
Properties someProperties = localRepo.getConfig();
LocalFileConfigRepository
anotherLocalRepoWithNoFallback =
new LocalFileConfigRepository(someBaseDir, someNamespace);
Properties anotherProperties = anotherLocalRepoWithNoFallback.loadConfig();
Properties anotherProperties = anotherLocalRepoWithNoFallback.getConfig();
assertThat(
"LocalFileConfigRepository should persist local cache files and return that afterwards",
......@@ -125,6 +130,30 @@ public class LocalFileConfigRepositoryTest extends ComponentTestCase {
}
@Test
public void testOnRepositoryChange() throws Exception {
RepositoryChangeListener someListener = mock(RepositoryChangeListener.class);
LocalFileConfigRepository localFileConfigRepository =
new LocalFileConfigRepository(someBaseDir, someNamespace);
localFileConfigRepository.setFallback(fallbackRepo);
localFileConfigRepository.addChangeListener(someListener);
localFileConfigRepository.getConfig();
Properties anotherProperties = new Properties();
anotherProperties.put("anotherKey", "anotherValue");
localFileConfigRepository.onRepositoryChange(someNamespace, anotherProperties);
final ArgumentCaptor<Properties> captor = ArgumentCaptor.forClass(Properties.class);
verify(someListener, times(1)).onRepositoryChange(eq(someNamespace), captor.capture());
assertEquals(anotherProperties, captor.getValue());
}
public static class MockConfigUtil extends ConfigUtil {
@Override
public String getAppId() {
......
package com.ctrip.apollo.internals;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
......@@ -13,6 +14,7 @@ import com.ctrip.apollo.util.http.HttpUtil;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.unidal.lookup.ComponentTestCase;
......@@ -22,7 +24,10 @@ import java.util.Map;
import java.util.Properties;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
......@@ -60,7 +65,7 @@ public class RemoteConfigRepositoryTest extends ComponentTestCase {
when(someResponse.getBody()).thenReturn(someApolloConfig);
RemoteConfigRepository remoteConfigRepository = new RemoteConfigRepository(someNamespace);
Properties config = remoteConfigRepository.loadConfig();
Properties config = remoteConfigRepository.getConfig();
assertEquals(configurations, config);
}
......@@ -71,7 +76,27 @@ public class RemoteConfigRepositoryTest extends ComponentTestCase {
when(someResponse.getStatusCode()).thenReturn(500);
RemoteConfigRepository remoteConfigRepository = new RemoteConfigRepository(someNamespace);
remoteConfigRepository.loadConfig();
remoteConfigRepository.getConfig();
}
@Test
public void testRepositoryChangeListener() throws Exception {
Map<String, String> configurations = ImmutableMap.of("someKey", "someValue");
ApolloConfig someApolloConfig = assembleApolloConfig(configurations);
when(someResponse.getStatusCode()).thenReturn(200);
when(someResponse.getBody()).thenReturn(someApolloConfig);
RepositoryChangeListener someListener = mock(RepositoryChangeListener.class);
RemoteConfigRepository remoteConfigRepository = new RemoteConfigRepository(someNamespace);
remoteConfigRepository.addChangeListener(someListener);
final ArgumentCaptor<Properties> captor = ArgumentCaptor.forClass(Properties.class);
remoteConfigRepository.loadRemoteConfig();
verify(someListener, times(1)).onRepositoryChange(eq(someNamespace), captor.capture());
assertEquals(configurations, captor.getValue());
}
private ApolloConfig assembleApolloConfig(Map<String, String> configurations) {
......
package com.ctrip.apollo.internals;
import com.google.common.collect.ImmutableMap;
import com.ctrip.apollo.ConfigChangeListener;
import com.ctrip.apollo.enums.PropertyChangeType;
import com.ctrip.apollo.model.ConfigChange;
import com.ctrip.apollo.model.ConfigChangeEvent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.Properties;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
......@@ -32,7 +43,7 @@ public class SimpleConfigTest {
String someValue = "someValue";
someProperties.setProperty(someKey, someValue);
when(configRepository.loadConfig()).thenReturn(someProperties);
when(configRepository.getConfig()).thenReturn(someProperties);
SimpleConfig config = new SimpleConfig(someNamespace, configRepository);
......@@ -41,8 +52,57 @@ public class SimpleConfigTest {
@Test(expected = RuntimeException.class)
public void testLoadConfigFromConfigRepositoryError() throws Exception {
when(configRepository.loadConfig()).thenThrow(Throwable.class);
when(configRepository.getConfig()).thenThrow(Throwable.class);
new SimpleConfig(someNamespace, configRepository);
}
@Test
public void testOnRepositoryChange() throws Exception {
Properties someProperties = new Properties();
String someKey = "someKey";
String someValue = "someValue";
String anotherKey = "anotherKey";
String anotherValue = "anotherValue";
someProperties.putAll(ImmutableMap.of(someKey, someValue, anotherKey, anotherValue));
Properties anotherProperties = new Properties();
String newKey = "newKey";
String newValue = "newValue";
String someValueNew = "someValueNew";
anotherProperties.putAll(ImmutableMap.of(someKey, someValueNew, newKey, newValue));
when(configRepository.getConfig()).thenReturn(someProperties);
ConfigChangeListener someListener = mock(ConfigChangeListener.class);
SimpleConfig config = new SimpleConfig(someNamespace, configRepository);
config.addChangeListener(someListener);
config.onRepositoryChange(someNamespace, anotherProperties);
ArgumentCaptor<ConfigChangeEvent> captor = ArgumentCaptor.forClass(ConfigChangeEvent.class);
verify(someListener, times(1)).onChange(captor.capture());
ConfigChangeEvent changeEvent = captor.getValue();
assertEquals(someNamespace, changeEvent.getNamespace());
assertEquals(3, changeEvent.getChanges().size());
ConfigChange someKeyChange = changeEvent.getChange(someKey);
assertEquals(someValue, someKeyChange.getOldValue());
assertEquals(someValueNew, someKeyChange.getNewValue());
assertEquals(PropertyChangeType.MODIFIED, someKeyChange.getChangeType());
ConfigChange anotherKeyChange = changeEvent.getChange(anotherKey);
assertEquals(anotherValue, anotherKeyChange.getOldValue());
assertEquals(null, anotherKeyChange.getNewValue());
assertEquals(PropertyChangeType.DELETED, anotherKeyChange.getChangeType());
ConfigChange newKeyChange = changeEvent.getChange(newKey);
assertEquals(null, newKeyChange.getOldValue());
assertEquals(newValue, newKeyChange.getNewValue());
assertEquals(PropertyChangeType.NEW, newKeyChange.getChangeType());
}
}
......@@ -40,7 +40,7 @@ public class DefaultConfigFactoryTest extends ComponentTestCase {
someProperties.setProperty(someKey, someValue);
LocalFileConfigRepository someLocalConfigRepo = mock(LocalFileConfigRepository.class);
when(someLocalConfigRepo.loadConfig()).thenReturn(someProperties);
when(someLocalConfigRepo.getConfig()).thenReturn(someProperties);
doReturn(someLocalConfigRepo).when(defaultConfigFactory).createLocalConfigRepository(someNamespace);
......
import com.ctrip.apollo.Config;
import com.ctrip.apollo.ConfigChangeListener;
import com.ctrip.apollo.ConfigService;
import com.ctrip.apollo.model.ConfigChange;
import com.ctrip.apollo.model.ConfigChangeEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
......@@ -9,16 +15,18 @@ import java.io.InputStreamReader;
/**
* @author Jason Song(song_s@ctrip.com)
*/
public class ApolloConfigDemo {
public class ApolloConfigDemo implements ConfigChangeListener {
private static final Logger logger = LoggerFactory.getLogger(ApolloConfigDemo.class);
private Config config;
public ApolloConfigDemo() {
config = ConfigService.getConfig();
config.addChangeListener(this);
}
private String getConfig(String key) {
String result = config.getProperty(key, "undefined");
System.out.println(String.format("Loading key: %s with value: %s", key, result));
logger.info(String.format("Loading key: %s with value: %s", key, result));
return result;
}
......@@ -35,4 +43,14 @@ public class ApolloConfigDemo {
apolloConfigDemo.getConfig(input);
}
}
@Override
public void onChange(ConfigChangeEvent changeEvent) {
logger.info("Changes for namespace {}", changeEvent.getNamespace());
for (ConfigChange change : changeEvent.getChanges().values()) {
logger.info("Change - key: {}, oldValue: {}, newValue: {}, changeType: {}",
change.getPropertyName(), change.getOldValue(), change.getNewValue(),
change.getChangeType());
}
}
}
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