From 49c1b3088e27e6bf875a779cb58400258182d192 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Wed, 23 Mar 2016 18:55:20 +0800 Subject: [PATCH] Add refresh config support and refactor --- apollo-biz/src/main/resources/import.sql | 1 + .../apollo/client/ApolloConfigManager.java | 53 +++-- .../client/enums/PropertyChangeType.java | 21 ++ .../apollo/client/loader/ConfigLoader.java | 11 +- .../client/loader/ConfigLoaderFactory.java | 23 +- .../client/loader/ConfigLoaderManager.java | 190 +++++++++++++++++ .../loader/impl/AbstractConfigLoader.java | 35 ++++ .../loader/impl/InMemoryConfigLoader.java | 15 ++ .../client/loader/impl/LocalConfigLoader.java | 17 -- .../loader/impl/LocalFileConfigLoader.java | 15 ++ .../loader/impl/RemoteConfigLoader.java | 131 ++++-------- .../apollo/client/model/ApolloRegistry.java | 11 + .../apollo/client/model/PropertyChange.java | 52 +++++ .../model/PropertySourceReloadResult.java | 44 ++++ .../ctrip/apollo/client/util/ConfigUtil.java | 2 +- .../com/ctrip/apollo/client/AllTests.java | 4 +- .../client/ApolloConfigManagerTest.java | 59 +++++- .../loader/ConfigLoaderManagerTest.java | 198 ++++++++++++++++++ .../loader/impl/RemoteConfigLoaderTest.java | 102 +++++---- .../controller/ConfigController.java | 7 +- .../ctrip/apollo/core/dto/ApolloConfig.java | 24 ++- .../demo/controller/DemoController.java | 13 +- apollo-demo/src/main/webapp/s/scripts/app.js | 40 ++-- .../src/main/webapp/s/templates/list.html | 16 +- 24 files changed, 866 insertions(+), 218 deletions(-) create mode 100644 apollo-client/src/main/java/com/ctrip/apollo/client/enums/PropertyChangeType.java create mode 100644 apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoaderManager.java create mode 100644 apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/AbstractConfigLoader.java create mode 100644 apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/InMemoryConfigLoader.java delete mode 100644 apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/LocalConfigLoader.java create mode 100644 apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/LocalFileConfigLoader.java create mode 100644 apollo-client/src/main/java/com/ctrip/apollo/client/model/PropertyChange.java create mode 100644 apollo-client/src/main/java/com/ctrip/apollo/client/model/PropertySourceReloadResult.java create mode 100644 apollo-client/src/test/java/com/ctrip/apollo/client/loader/ConfigLoaderManagerTest.java diff --git a/apollo-biz/src/main/resources/import.sql b/apollo-biz/src/main/resources/import.sql index 14d70fc36..07675f7a1 100644 --- a/apollo-biz/src/main/resources/import.sql +++ b/apollo-biz/src/main/resources/import.sql @@ -6,3 +6,4 @@ INSERT INTO Version (AppId, IsDeleted, Name, ReleaseId) VALUES (102, 0, '1.0', 2 INSERT INTO RELEASESNAPSHOT (ClusterName, IsDeleted, ReleaseId, Configurations) VALUES ('default', 0, 1, '{"apollo.foo":"bar", "apollo.bar":"foo"}'); INSERT INTO RELEASESNAPSHOT (ClusterName, IsDeleted, ReleaseId, Configurations) VALUES ('default', 0, 2, '{"demo.foo":"demo1", "demo.bar":"demo2"}'); +INSERT INTO RELEASESNAPSHOT (ClusterName, IsDeleted, ReleaseId, Configurations) VALUES ('default', 0, 3, '{"apollo.foo":"another bar", "apollo.bar_new":"foo"}'); diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/ApolloConfigManager.java b/apollo-client/src/main/java/com/ctrip/apollo/client/ApolloConfigManager.java index 21bd51e02..e88d79f0c 100644 --- a/apollo-client/src/main/java/com/ctrip/apollo/client/ApolloConfigManager.java +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/ApolloConfigManager.java @@ -1,8 +1,12 @@ package com.ctrip.apollo.client; -import com.ctrip.apollo.client.loader.ConfigLoader; import com.ctrip.apollo.client.loader.ConfigLoaderFactory; +import com.ctrip.apollo.client.loader.ConfigLoaderManager; +import com.ctrip.apollo.client.model.PropertyChange; +import com.ctrip.apollo.client.model.PropertySourceReloadResult; import com.ctrip.apollo.client.util.ConfigUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -19,6 +23,7 @@ import org.springframework.core.PriorityOrdered; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.MutablePropertySources; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; /** @@ -27,19 +32,18 @@ import java.util.concurrent.atomic.AtomicReference; * @author Jason Song(song_s@ctrip.com) */ public class ApolloConfigManager implements BeanDefinitionRegistryPostProcessor, PriorityOrdered, ApplicationContextAware { - public static final String APOLLO_PROPERTY_SOURCE_NAME = "ApolloConfigProperties"; + private static final Logger logger = LoggerFactory.getLogger(ApolloConfigManager.class); private static AtomicReference singletonProtector = new AtomicReference(); - private ConfigLoader configLoader; + private ConfigLoaderManager configLoaderManager; private ConfigurableApplicationContext applicationContext; - - private CompositePropertySource currentPropertySource; + private RefreshScope scope; public ApolloConfigManager() { if(!singletonProtector.compareAndSet(null, this)) { throw new IllegalStateException("There should be only one ApolloConfigManager instance!"); } - this.configLoader = ConfigLoaderFactory.getInstance().getRemoteConfigLoader(); + this.configLoaderManager = ConfigLoaderFactory.getInstance().getConfigLoaderManager(); } @Override @@ -97,23 +101,42 @@ public class ApolloConfigManager implements BeanDefinitionRegistryPostProcessor, } /** - * Prepare property sources - * First try to load from remote - * If loading from remote failed, then fall back to local cached properties + * Initialize property sources */ void initializePropertySource() { - currentPropertySource = loadPropertySource(); + //TODO stop application from starting when config cannot be loaded? + CompositePropertySource result = this.configLoaderManager.loadPropertySource(); + + updateEnvironmentPropertySource(result); + } + private void updateEnvironmentPropertySource(CompositePropertySource currentPropertySource) { MutablePropertySources currentPropertySources = applicationContext.getEnvironment().getPropertySources(); if (currentPropertySources.contains(currentPropertySource.getName())) { - currentPropertySources.remove(currentPropertySource.getName()); + currentPropertySources.replace(currentPropertySource.getName(), currentPropertySource); + return; } currentPropertySources.addFirst(currentPropertySource); } - CompositePropertySource loadPropertySource() { - CompositePropertySource compositePropertySource = new CompositePropertySource(APOLLO_PROPERTY_SOURCE_NAME); - compositePropertySource.addPropertySource(configLoader.loadPropertySource()); - return compositePropertySource; + public List updatePropertySource() { + PropertySourceReloadResult result = this.configLoaderManager.reloadPropertySource(); + if (result.hasChanges()) { + updateEnvironmentPropertySource(result.getPropertySource()); + refreshBeans(); + } + return result.getChanges(); + } + + private void refreshBeans() { + if (this.scope == null) { + this.scope = applicationContext.getBean("refreshScope", RefreshScope.class); + } + + if (this.scope == null) { + logger.error("Could not get refresh scope object, skip refresh beans"); + } + + this.scope.refreshAll(); } } diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/enums/PropertyChangeType.java b/apollo-client/src/main/java/com/ctrip/apollo/client/enums/PropertyChangeType.java new file mode 100644 index 000000000..bb3ade156 --- /dev/null +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/enums/PropertyChangeType.java @@ -0,0 +1,21 @@ +package com.ctrip.apollo.client.enums; + + +/** + * @author Jason Song(song_s@ctrip.com) + */ +public enum PropertyChangeType { + NEW("New"), + MODIFIED("Modified"), + DELETED("Deleted"); + + private String type; + + PropertyChangeType(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoader.java b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoader.java index cfcf19e28..513eeb632 100644 --- a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoader.java +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoader.java @@ -1,14 +1,13 @@ package com.ctrip.apollo.client.loader; -import org.springframework.core.env.CompositePropertySource; +import com.ctrip.apollo.client.model.ApolloRegistry; +import com.ctrip.apollo.core.dto.ApolloConfig; /** * @author Jason Song(songs_ctrip.com) */ public interface ConfigLoader { - /** - * Load property source for client use - * @return property source - */ - CompositePropertySource loadPropertySource(); + ApolloConfig loadApolloConfig(ApolloRegistry apolloRegistry, ApolloConfig previous); + + void setFallBackLoader(ConfigLoader configLoader); } diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoaderFactory.java b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoaderFactory.java index 54de60121..b09db1529 100644 --- a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoaderFactory.java +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoaderFactory.java @@ -1,6 +1,10 @@ package com.ctrip.apollo.client.loader; +import com.ctrip.apollo.client.loader.impl.InMemoryConfigLoader; +import com.ctrip.apollo.client.loader.impl.LocalFileConfigLoader; import com.ctrip.apollo.client.loader.impl.RemoteConfigLoader; +import com.ctrip.apollo.client.util.ConfigUtil; +import org.springframework.web.client.RestTemplate; /** * @author Jason Song(song_s@ctrip.com) @@ -15,7 +19,24 @@ public class ConfigLoaderFactory { return configLoaderFactory; } + public ConfigLoader getLocalFileConfigLoader() { + ConfigLoader configLoader = new LocalFileConfigLoader(); + return configLoader; + } + + public ConfigLoader getInMemoryConfigLoader() { + ConfigLoader inMemoryConfigLoader = new InMemoryConfigLoader(); + inMemoryConfigLoader.setFallBackLoader(getLocalFileConfigLoader()); + return inMemoryConfigLoader; + } + public ConfigLoader getRemoteConfigLoader() { - return new RemoteConfigLoader(); + ConfigLoader remoteConfigLoader = new RemoteConfigLoader(new RestTemplate(), ConfigUtil.getInstance()); +// remoteConfigLoader.setFallBackLoader(getInMemoryConfigLoader()); + return remoteConfigLoader; + } + + public ConfigLoaderManager getConfigLoaderManager() { + return new ConfigLoaderManager(getRemoteConfigLoader(), ConfigUtil.getInstance()); } } diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoaderManager.java b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoaderManager.java new file mode 100644 index 000000000..2a5821c91 --- /dev/null +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/ConfigLoaderManager.java @@ -0,0 +1,190 @@ +package com.ctrip.apollo.client.loader; + +import com.ctrip.apollo.client.enums.PropertyChangeType; +import com.ctrip.apollo.client.model.ApolloRegistry; +import com.ctrip.apollo.client.model.PropertyChange; +import com.ctrip.apollo.client.model.PropertySourceReloadResult; +import com.ctrip.apollo.client.util.ConfigUtil; +import com.ctrip.apollo.core.dto.ApolloConfig; +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 org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.MapPropertySource; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author Jason Song(song_s@ctrip.com) + */ +public class ConfigLoaderManager { + public static final String APOLLO_PROPERTY_SOURCE_NAME = "ApolloConfigProperties"; + private static final Logger logger = LoggerFactory.getLogger(ConfigLoaderManager.class); + private ConfigLoader configLoader; + private ConfigUtil configUtil; + private final ExecutorService executorService; + private final AtomicLong counter; + private Map currentApolloRegistryConfigCache; + private Map previousApolloRegistryConfigCache; + private List apolloRegistries; + + public ConfigLoaderManager(ConfigLoader configLoader, ConfigUtil configUtil) { + this.configLoader = configLoader; + this.configUtil = configUtil; + this.counter = new AtomicLong(); + this.executorService = Executors.newFixedThreadPool(5, new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, "ConfigLoaderManager-" + counter.incrementAndGet()); + return thread; + } + }); + this.currentApolloRegistryConfigCache = Maps.newConcurrentMap(); + } + + public CompositePropertySource loadPropertySource() { + try { + apolloRegistries = configUtil.loadApolloRegistries(); + } catch (IOException e) { + throw new RuntimeException("Load apollo config registry failed", e); + } + + return loadPropertySourceWithApolloRegistries(apolloRegistries); + } + + public PropertySourceReloadResult reloadPropertySource() { + CompositePropertySource composite = loadPropertySourceWithApolloRegistries(apolloRegistries); + List previous = Lists.newArrayList(this.previousApolloRegistryConfigCache.values()); + List current = Lists.newArrayList(this.currentApolloRegistryConfigCache.values()); + return new PropertySourceReloadResult(composite, calcPropertyChanges(previous, current)); + } + + /** + * Load property source with apollo registries provided + * Should not be invoked in parallel since there are some operations like create/destroy cache, + * writing to files etc. + * @param apolloRegistries + * @return + */ + private synchronized CompositePropertySource loadPropertySourceWithApolloRegistries(List apolloRegistries) { + resetApolloRegistryConfigCache(); + CompositePropertySource composite = new CompositePropertySource(APOLLO_PROPERTY_SOURCE_NAME); + if (apolloRegistries == null || apolloRegistries.isEmpty()) { + logger.warn("No Apollo Registry found!"); + return composite; + } + try { + List apolloConfigList = loadApolloConfigs(apolloRegistries); + + Collections.sort(apolloConfigList); + for (ApolloConfig apolloConfig : apolloConfigList) { + composite.addPropertySource(new MapPropertySource(assemblePropertySourceName(apolloConfig), apolloConfig.getConfigurations())); + } + return composite; + } catch (Throwable throwable) { + throw new RuntimeException("Load apollo configs failed", throwable); + } + } + + List calcPropertyChanges(List previous, List current) { + Map previousMap = collectConfigurations(previous); + Map currentMap = collectConfigurations(current); + + Set previousKeys = previousMap.keySet(); + Set currentKeys = currentMap.keySet(); + + Set commonKeys = Sets.intersection(previousKeys, currentKeys); + Set newKeys = Sets.difference(currentKeys, commonKeys); + Set removedKeys = Sets.difference(previousKeys, commonKeys); + + List changes = Lists.newArrayList(); + + for (String newKey : newKeys) { + changes.add(new PropertyChange(newKey, null, currentMap.get(newKey), PropertyChangeType.NEW)); + } + + for (String removedKey : removedKeys) { + changes.add(new PropertyChange(removedKey, previousMap.get(removedKey), null, PropertyChangeType.DELETED)); + } + + for (String commonKey : commonKeys) { + if (previousMap.get(commonKey).equals(currentMap.get(commonKey))) { + continue; + } + changes.add(new PropertyChange(commonKey, previousMap.get(commonKey), currentMap.get(commonKey), PropertyChangeType.MODIFIED)); + } + + return changes; + } + + Map collectConfigurations(List apolloConfigs) { + Collections.sort(apolloConfigs); + Map configMap = Maps.newHashMap(); + for (int i = apolloConfigs.size() - 1; i > -1; i--) { + configMap.putAll(apolloConfigs.get(i).getConfigurations()); + } + return configMap; + } + + List loadApolloConfigs(List apolloRegistries) throws Throwable { + List> futures = Lists.newArrayList(); + for (final ApolloRegistry apolloRegistry : apolloRegistries) { + futures.add(executorService.submit(new Callable() { + @Override + public ApolloConfig call() throws Exception { + return loadSingleApolloConfig(apolloRegistry); + } + })); + } + List apolloConfigList = Lists.newArrayList(); + for (Future future : futures) { + try { + ApolloConfig result = future.get(); + if (result == null) { + continue; + } + apolloConfigList.add(result); + } catch (ExecutionException e) { + throw e.getCause(); + } + } + return apolloConfigList; + } + + ApolloConfig loadSingleApolloConfig(ApolloRegistry apolloRegistry) { + ApolloConfig result = configLoader.loadApolloConfig(apolloRegistry, getPreviousApolloConfig(apolloRegistry)); + if (result == null) { + logger.error("Loaded config null..."); + return null; + } + logger.info("Loaded config: {}", result); + updateCurrentApolloConfigCache(apolloRegistry, result); + + return result; + } + + void resetApolloRegistryConfigCache() { + this.previousApolloRegistryConfigCache = currentApolloRegistryConfigCache; + this.currentApolloRegistryConfigCache = Maps.newConcurrentMap(); + } + + ApolloConfig getPreviousApolloConfig(ApolloRegistry apolloRegistry) { + return previousApolloRegistryConfigCache.get(apolloRegistry); + } + + void updateCurrentApolloConfigCache(ApolloRegistry apolloRegistry, ApolloConfig apolloConfig) { + currentApolloRegistryConfigCache.put(apolloRegistry, apolloConfig); + } + + private String assemblePropertySourceName(ApolloConfig apolloConfig) { + return String.format("%d-%s-%s-%d", apolloConfig.getAppId(), apolloConfig.getCluster(), apolloConfig.getVersion(), apolloConfig.getReleaseId()); + } +} diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/AbstractConfigLoader.java b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/AbstractConfigLoader.java new file mode 100644 index 000000000..6371ca5d9 --- /dev/null +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/AbstractConfigLoader.java @@ -0,0 +1,35 @@ +package com.ctrip.apollo.client.loader.impl; + +import com.ctrip.apollo.client.loader.ConfigLoader; +import com.ctrip.apollo.client.model.ApolloRegistry; +import com.ctrip.apollo.core.dto.ApolloConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Jason Song(song_s@ctrip.com) + */ +public abstract class AbstractConfigLoader implements ConfigLoader { + private static final Logger logger = LoggerFactory.getLogger(AbstractConfigLoader.class); + private ConfigLoader fallback; + + @Override + public ApolloConfig loadApolloConfig(ApolloRegistry apolloRegistry, ApolloConfig previous) { + try { + return doLoadApolloConfig(apolloRegistry, previous); + } catch (Throwable e) { + if (this.fallback == null) { + throw new RuntimeException(String.format("Load Apollo Config failed - %s", apolloRegistry.toString()), e); + } + logger.error("Load Config via {} failed, try to use its fallback {} to load", getClass().getSimpleName(), fallback.getClass().getSimpleName(), e); + return this.fallback.loadApolloConfig(apolloRegistry, previous); + } + } + + protected abstract ApolloConfig doLoadApolloConfig(ApolloRegistry apolloRegistry, ApolloConfig previous); + + @Override + public void setFallBackLoader(ConfigLoader configLoader) { + this.fallback = configLoader; + } +} diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/InMemoryConfigLoader.java b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/InMemoryConfigLoader.java new file mode 100644 index 000000000..d72e50a3d --- /dev/null +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/InMemoryConfigLoader.java @@ -0,0 +1,15 @@ +package com.ctrip.apollo.client.loader.impl; + +import com.ctrip.apollo.client.model.ApolloRegistry; +import com.ctrip.apollo.core.dto.ApolloConfig; + +/** + * @author Jason Song(song_s@ctrip.com) + */ +public class InMemoryConfigLoader extends AbstractConfigLoader{ + + @Override + protected ApolloConfig doLoadApolloConfig(ApolloRegistry apolloRegistry, ApolloConfig previous) { + return null; + } +} diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/LocalConfigLoader.java b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/LocalConfigLoader.java deleted file mode 100644 index aef1477fd..000000000 --- a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/LocalConfigLoader.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.ctrip.apollo.client.loader.impl; - -import com.ctrip.apollo.client.loader.ConfigLoader; -import org.springframework.core.env.CompositePropertySource; - -/** - * Load config from local backup file - * @author Jason Song(song_s@ctrip.com) - */ -public class LocalConfigLoader implements ConfigLoader { - private static final String PROPERTY_SOURCE_NAME = "ApolloLocalConfigProperties"; - - @Override - public CompositePropertySource loadPropertySource() { - return null; - } -} diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/LocalFileConfigLoader.java b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/LocalFileConfigLoader.java new file mode 100644 index 000000000..a997ed638 --- /dev/null +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/LocalFileConfigLoader.java @@ -0,0 +1,15 @@ +package com.ctrip.apollo.client.loader.impl; + +import com.ctrip.apollo.client.model.ApolloRegistry; +import com.ctrip.apollo.core.dto.ApolloConfig; + +/** + * Load config from local backup file + * @author Jason Song(song_s@ctrip.com) + */ +public class LocalFileConfigLoader extends AbstractConfigLoader{ + @Override + public ApolloConfig doLoadApolloConfig(ApolloRegistry apolloRegistry, ApolloConfig previous) { + return null; + } +} diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/RemoteConfigLoader.java b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/RemoteConfigLoader.java index 6be8cf6e8..d95fee41d 100644 --- a/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/RemoteConfigLoader.java +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/loader/impl/RemoteConfigLoader.java @@ -1,14 +1,11 @@ package com.ctrip.apollo.client.loader.impl; -import com.ctrip.apollo.client.loader.ConfigLoader; import com.ctrip.apollo.client.model.ApolloRegistry; import com.ctrip.apollo.client.util.ConfigUtil; import com.ctrip.apollo.core.dto.ApolloConfig; -import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.core.env.CompositePropertySource; -import org.springframework.core.env.MapPropertySource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -16,126 +13,74 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicLong; +import java.util.Map; /** * Load config from remote config server * * @author Jason Song(song_s@ctrip.com) */ -public class RemoteConfigLoader implements ConfigLoader { - private static final String PROPERTY_SOURCE_NAME = "ApolloRemoteConfigProperties"; +public class RemoteConfigLoader extends AbstractConfigLoader { private static final Logger logger = LoggerFactory.getLogger(RemoteConfigLoader.class); private final RestTemplate restTemplate; private final ConfigUtil configUtil; - private final ExecutorService executorService; - private final AtomicLong counter; - - public RemoteConfigLoader() { - this(new RestTemplate(), ConfigUtil.getInstance()); - } public RemoteConfigLoader(RestTemplate restTemplate, ConfigUtil configUtil) { this.restTemplate = restTemplate; this.configUtil = configUtil; - this.counter = new AtomicLong(); - this.executorService = Executors.newFixedThreadPool(5, new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread thread = new Thread(r, "RemoteConfigLoader-" + counter.incrementAndGet()); - return thread; - } - }); - } - - @Override - public CompositePropertySource loadPropertySource() { - CompositePropertySource composite = new CompositePropertySource(PROPERTY_SOURCE_NAME); - List apolloRegistries; - try { - apolloRegistries = configUtil.loadApolloRegistries(); - } catch (IOException e) { - throw new RuntimeException("Load apollo config registry failed", e); - } - - if (apolloRegistries == null || apolloRegistries.isEmpty()) { - logger.warn("No Apollo Registry found!"); - return composite; - } - - try { - doLoadRemoteApolloConfig(apolloRegistries, composite); - } catch (Throwable throwable) { - throw new RuntimeException("Load remote property source failed", throwable); - } - return composite; - } - - void doLoadRemoteApolloConfig(List apolloRegistries, CompositePropertySource compositePropertySource) throws Throwable { - List> futures = Lists.newArrayList(); - for (final ApolloRegistry apolloRegistry : apolloRegistries) { - futures.add(executorService.submit(new Callable() { - @Override - public MapPropertySource call() throws Exception { - return loadSingleApolloConfig(apolloRegistry.getAppId(), apolloRegistry.getVersion()); - } - })); - } - for (Future future : futures) { - try { - MapPropertySource result = future.get(); - if (result == null) { - continue; - } - compositePropertySource.addPropertySource(result); - } catch (ExecutionException e) { - throw e.getCause(); - } - } } - MapPropertySource loadSingleApolloConfig(long appId, String version) { - ApolloConfig result = - this.getRemoteConfig(restTemplate, configUtil.getConfigServerUrl(), appId, configUtil.getCluster(), version); - if (result == null) { - logger.error("Loaded config null..."); - return null; - } - logger.info("Loaded config: {}", result); - - return new MapPropertySource(assemblePropertySourceName(result), result.getConfigurations()); - } + ApolloConfig getRemoteConfig(RestTemplate restTemplate, String uri, String cluster, ApolloRegistry apolloRegistry, ApolloConfig previousConfig) { + long appId = apolloRegistry.getAppId(); + String version = apolloRegistry.getVersion(); - private String assemblePropertySourceName(ApolloConfig apolloConfig) { - return String.format("%d-%s-%s-%d", apolloConfig.getAppId(), apolloConfig.getCluster(), apolloConfig.getVersion(), apolloConfig.getReleaseId()); - } - - ApolloConfig getRemoteConfig(RestTemplate restTemplate, String uri, long appId, String cluster, String version) { logger.info("Loading config from {}, appId={}, cluster={}, version={}", uri, appId, cluster, version); - String path = "config/{appId}/{cluster}"; - Object[] args = new String[] {String.valueOf(appId), cluster}; + String path = "/config/{appId}/{cluster}"; + Map paramMap = Maps.newHashMap(); + paramMap.put("appId", appId); + paramMap.put("cluster", cluster); + if (StringUtils.hasText(version)) { - args = new String[] {String.valueOf(appId), cluster, version}; path = path + "/{version}"; + paramMap.put("version", version); + } + if (previousConfig != null) { + path = path + "?releaseId={releaseId}"; + paramMap.put("releaseId", previousConfig.getReleaseId()); } - ResponseEntity response = null; + + ResponseEntity response; try { // TODO retry response = restTemplate.exchange(uri - + path, HttpMethod.GET, new HttpEntity((Void) null), ApolloConfig.class, args); - } catch (Exception e) { + + path, HttpMethod.GET, new HttpEntity((Void) null), ApolloConfig.class, paramMap); + } catch (Throwable e) { throw e; } - if (response == null || response.getStatusCode() != HttpStatus.OK) { + if (response == null) { + throw new RuntimeException("Load apollo config failed, response is null"); + } + + if (response.getStatusCode() == HttpStatus.NOT_MODIFIED) { return null; } + + if (response.getStatusCode() != HttpStatus.OK) { + throw new RuntimeException(String.format("Load apollo config failed, response status %s", response.getStatusCode())); + } + ApolloConfig result = response.getBody(); return result; } + @Override + protected ApolloConfig doLoadApolloConfig(ApolloRegistry apolloRegistry, ApolloConfig previous) { + ApolloConfig result = this.getRemoteConfig(restTemplate, + configUtil.getConfigServerUrl(), configUtil.getCluster(), + apolloRegistry, previous); + //When remote server return 304, we need to return the previous result + return result == null ? previous : result; + } } diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/model/ApolloRegistry.java b/apollo-client/src/main/java/com/ctrip/apollo/client/model/ApolloRegistry.java index e18075696..d3a4932ff 100644 --- a/apollo-client/src/main/java/com/ctrip/apollo/client/model/ApolloRegistry.java +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/model/ApolloRegistry.java @@ -1,5 +1,7 @@ package com.ctrip.apollo.client.model; +import com.google.common.base.MoreObjects; + /** * @author Jason Song(song_s@ctrip.com) */ @@ -22,4 +24,13 @@ public class ApolloRegistry { public void setVersion(String version) { this.version = version; } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .omitNullValues() + .add("appId", appId) + .add("version", version) + .toString(); + } } diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/model/PropertyChange.java b/apollo-client/src/main/java/com/ctrip/apollo/client/model/PropertyChange.java new file mode 100644 index 000000000..699f08d7a --- /dev/null +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/model/PropertyChange.java @@ -0,0 +1,52 @@ +package com.ctrip.apollo.client.model; + +import com.ctrip.apollo.client.enums.PropertyChangeType; + +/** + * @author Jason Song(song_s@ctrip.com) + */ +public class PropertyChange { + private String propertyName; + private Object oldValue; + private Object newValue; + private PropertyChangeType changeType; + + public PropertyChange(String propertyName, Object oldValue, Object newValue, PropertyChangeType changeType) { + this.propertyName = propertyName; + this.oldValue = oldValue; + this.newValue = newValue; + this.changeType = changeType; + } + + public String getPropertyName() { + return propertyName; + } + + public void setPropertyName(String propertyName) { + this.propertyName = propertyName; + } + + public Object getOldValue() { + return oldValue; + } + + public void setOldValue(Object oldValue) { + this.oldValue = oldValue; + } + + public Object getNewValue() { + return newValue; + } + + public void setNewValue(Object newValue) { + this.newValue = newValue; + } + + public PropertyChangeType getChangeType() { + return changeType; + } + + public void setChangeType(PropertyChangeType changeType) { + this.changeType = changeType; + } +} diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/model/PropertySourceReloadResult.java b/apollo-client/src/main/java/com/ctrip/apollo/client/model/PropertySourceReloadResult.java new file mode 100644 index 000000000..ad594dda4 --- /dev/null +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/model/PropertySourceReloadResult.java @@ -0,0 +1,44 @@ +package com.ctrip.apollo.client.model; + +import com.google.common.collect.Lists; +import org.springframework.core.env.CompositePropertySource; + +import java.util.List; + +/** + * @author Jason Song(song_s@ctrip.com) + */ +public class PropertySourceReloadResult { + private CompositePropertySource propertySource; + private List changes; + + public PropertySourceReloadResult(CompositePropertySource propertySource) { + this.propertySource = propertySource; + changes = Lists.newArrayList(); + } + + public PropertySourceReloadResult(CompositePropertySource propertySource, List changes) { + this.propertySource = propertySource; + this.changes = changes; + } + + public CompositePropertySource getPropertySource() { + return propertySource; + } + + public void setPropertySource(CompositePropertySource propertySource) { + this.propertySource = propertySource; + } + + public List getChanges() { + return changes; + } + + public void setChanges(List changes) { + this.changes = changes; + } + + public boolean hasChanges() { + return !changes.isEmpty(); + } +} diff --git a/apollo-client/src/main/java/com/ctrip/apollo/client/util/ConfigUtil.java b/apollo-client/src/main/java/com/ctrip/apollo/client/util/ConfigUtil.java index 00d5a0535..81a7c8574 100644 --- a/apollo-client/src/main/java/com/ctrip/apollo/client/util/ConfigUtil.java +++ b/apollo-client/src/main/java/com/ctrip/apollo/client/util/ConfigUtil.java @@ -33,7 +33,7 @@ public class ConfigUtil { public String getConfigServerUrl() { // TODO return the meta server url based on different environments - return "http://localhost:8888"; + return "http://localhost"; } public String getCluster() { diff --git a/apollo-client/src/test/java/com/ctrip/apollo/client/AllTests.java b/apollo-client/src/test/java/com/ctrip/apollo/client/AllTests.java index a7a5097d2..ff1024338 100644 --- a/apollo-client/src/test/java/com/ctrip/apollo/client/AllTests.java +++ b/apollo-client/src/test/java/com/ctrip/apollo/client/AllTests.java @@ -1,5 +1,6 @@ package com.ctrip.apollo.client; +import com.ctrip.apollo.client.loader.ConfigLoaderManagerTest; import com.ctrip.apollo.client.loader.impl.RemoteConfigLoaderTest; import com.ctrip.apollo.client.util.ConfigUtilTest; import org.junit.runner.RunWith; @@ -8,7 +9,8 @@ import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) @SuiteClasses({ -ApolloConfigManagerTest.class, RemoteConfigLoaderTest.class, ConfigUtilTest.class +ApolloConfigManagerTest.class, ConfigLoaderManagerTest.class, RemoteConfigLoaderTest.class, +ConfigUtilTest.class }) public class AllTests { diff --git a/apollo-client/src/test/java/com/ctrip/apollo/client/ApolloConfigManagerTest.java b/apollo-client/src/test/java/com/ctrip/apollo/client/ApolloConfigManagerTest.java index edbcd7a40..e2ede58d3 100644 --- a/apollo-client/src/test/java/com/ctrip/apollo/client/ApolloConfigManagerTest.java +++ b/apollo-client/src/test/java/com/ctrip/apollo/client/ApolloConfigManagerTest.java @@ -1,9 +1,10 @@ package com.ctrip.apollo.client; -import com.ctrip.apollo.client.loader.ConfigLoader; +import com.ctrip.apollo.client.loader.ConfigLoaderManager; +import com.ctrip.apollo.client.model.PropertyChange; +import com.ctrip.apollo.client.model.PropertySourceReloadResult; import org.junit.After; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -11,6 +12,7 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.CompositePropertySource; @@ -18,10 +20,11 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MutablePropertySources; import org.springframework.test.util.ReflectionTestUtils; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.*; /** @@ -31,7 +34,7 @@ import static org.mockito.Mockito.*; public class ApolloConfigManagerTest { private ApolloConfigManager apolloConfigManager; @Mock - private ConfigLoader configLoader; + private ConfigLoaderManager configLoaderManager; @Mock private ConfigurableApplicationContext applicationContext; @Mock @@ -40,6 +43,8 @@ public class ApolloConfigManagerTest { private MutablePropertySources mutablePropertySources; @Mock private BeanDefinitionRegistry beanDefinitionRegistry; + @Mock + private RefreshScope scope; @Before public void setUp() { @@ -49,7 +54,8 @@ public class ApolloConfigManagerTest { when(env.getPropertySources()).thenReturn(mutablePropertySources); apolloConfigManager.setApplicationContext(applicationContext); - ReflectionTestUtils.setField(apolloConfigManager, "configLoader", configLoader); + ReflectionTestUtils.setField(apolloConfigManager, "configLoaderManager", configLoaderManager); + ReflectionTestUtils.setField(apolloConfigManager, "scope", scope); } @After @@ -66,21 +72,20 @@ public class ApolloConfigManagerTest { } @Test - public void testPreparePropertySourceSuccessfully() { + public void testInitializePropertySourceSuccessfully() { CompositePropertySource somePropertySource = mock(CompositePropertySource.class); final ArgumentCaptor captor = ArgumentCaptor.forClass(CompositePropertySource.class); - when(configLoader.loadPropertySource()).thenReturn(somePropertySource); + when(configLoaderManager.loadPropertySource()).thenReturn(somePropertySource); apolloConfigManager.initializePropertySource(); - verify(configLoader, times(1)).loadPropertySource(); + verify(configLoaderManager, times(1)).loadPropertySource(); verify(mutablePropertySources, times(1)).addFirst(captor.capture()); final CompositePropertySource insertedPropertySource = captor.getValue(); - assertEquals(ApolloConfigManager.APOLLO_PROPERTY_SOURCE_NAME, insertedPropertySource.getName()); - assertTrue(insertedPropertySource.getPropertySources().contains(somePropertySource)); + assertEquals(insertedPropertySource, somePropertySource); } @Test @@ -92,4 +97,38 @@ public class ApolloConfigManagerTest { verify(beanDefinitionRegistry, times(2)).registerBeanDefinition(anyString(), any(BeanDefinition.class)); } + @Test + public void testUpdatePropertySourceWithChanges() throws Exception { + PropertySourceReloadResult somePropertySourceReloadResult = mock(PropertySourceReloadResult.class); + CompositePropertySource somePropertySource = mock(CompositePropertySource.class); + List someChanges = mock(List.class); + + when(somePropertySourceReloadResult.hasChanges()).thenReturn(true); + when(somePropertySourceReloadResult.getPropertySource()).thenReturn(somePropertySource); + when(somePropertySourceReloadResult.getChanges()).thenReturn(someChanges); + when(configLoaderManager.reloadPropertySource()).thenReturn(somePropertySourceReloadResult); + + List result = apolloConfigManager.updatePropertySource(); + + assertEquals(someChanges, result); + verify(scope, times(1)).refreshAll(); + } + + @Test + public void testUpdatePropertySourceWithNoChange() throws Exception { + PropertySourceReloadResult somePropertySourceReloadResult = mock(PropertySourceReloadResult.class); + CompositePropertySource somePropertySource = mock(CompositePropertySource.class); + List emptyChanges = Collections.emptyList(); + + when(somePropertySourceReloadResult.hasChanges()).thenReturn(false); + when(somePropertySourceReloadResult.getPropertySource()).thenReturn(somePropertySource); + when(somePropertySourceReloadResult.getChanges()).thenReturn(emptyChanges); + when(configLoaderManager.reloadPropertySource()).thenReturn(somePropertySourceReloadResult); + + List result = apolloConfigManager.updatePropertySource(); + + assertEquals(emptyChanges, result); + verify(scope, never()).refreshAll(); + + } } diff --git a/apollo-client/src/test/java/com/ctrip/apollo/client/loader/ConfigLoaderManagerTest.java b/apollo-client/src/test/java/com/ctrip/apollo/client/loader/ConfigLoaderManagerTest.java new file mode 100644 index 000000000..3fbeb9622 --- /dev/null +++ b/apollo-client/src/test/java/com/ctrip/apollo/client/loader/ConfigLoaderManagerTest.java @@ -0,0 +1,198 @@ +package com.ctrip.apollo.client.loader; + +import com.ctrip.apollo.client.enums.PropertyChangeType; +import com.ctrip.apollo.client.model.ApolloRegistry; +import com.ctrip.apollo.client.model.PropertyChange; +import com.ctrip.apollo.client.model.PropertySourceReloadResult; +import com.ctrip.apollo.client.util.ConfigUtil; +import com.ctrip.apollo.core.dto.ApolloConfig; +import com.google.common.base.Function; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.ObjectArrays; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.env.CompositePropertySource; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +/** + * @author Jason Song(song_s@ctrip.com) + */ +@RunWith(MockitoJUnitRunner.class) +public class ConfigLoaderManagerTest { + private ConfigLoaderManager configLoaderManager; + @Mock + private ConfigLoader configLoader; + @Mock + private ConfigUtil configUtil; + + @Before + public void setUp() { + configLoaderManager = spy(new ConfigLoaderManager(configLoader, configUtil)); + } + + @Test + public void testLoadPropertySource() throws Exception { + long someAppId = 100; + long anotherAppId = 101; + ApolloRegistry someApolloRegistry = assembleSomeApolloRegistry(someAppId, "someVersion"); + ApolloRegistry anotherApolloRegistry = assembleSomeApolloRegistry(anotherAppId, "anotherVersion"); + ApolloConfig someApolloConfig = mock(ApolloConfig.class); + ApolloConfig anotherApolloConfig = mock(ApolloConfig.class); + Map someMap = mock(Map.class); + Map anotherMap = mock(Map.class); + + when(someApolloConfig.getAppId()).thenReturn(someAppId); + when(someApolloConfig.getAppId()).thenReturn(anotherAppId); + when(configUtil.loadApolloRegistries()).thenReturn(Lists.newArrayList(someApolloRegistry, anotherApolloRegistry)); + doReturn(someApolloConfig).when(configLoaderManager).loadSingleApolloConfig(someApolloRegistry); + doReturn(anotherApolloConfig).when(configLoaderManager).loadSingleApolloConfig(anotherApolloRegistry); + when(someApolloConfig.getConfigurations()).thenReturn(someMap); + when(anotherApolloConfig.getConfigurations()).thenReturn(anotherMap); + + CompositePropertySource result = configLoaderManager.loadPropertySource(); + + assertEquals(2, result.getPropertySources().size()); + + List> resultMaps = FluentIterable.from(result.getPropertySources()).transform(new Function, Map>() { + @Override + public Map apply(PropertySource input) { + return (Map)input.getSource(); + } + }).toList(); + + assertTrue(resultMaps.containsAll(Lists.newArrayList(someMap, anotherMap))); + } + + @Test(expected = RuntimeException.class) + public void testLoadPropertySourceWithError() throws Exception { + Exception someException = mock(Exception.class); + long someAppId = 100; + ApolloRegistry someApolloRegistry = assembleSomeApolloRegistry(someAppId, "someVersion"); + when(configUtil.loadApolloRegistries()).thenReturn(Lists.newArrayList(someApolloRegistry)); + + doThrow(someException).when(configLoaderManager).loadSingleApolloConfig(someApolloRegistry); + + configLoaderManager.loadPropertySource(); + } + + @Test + public void testLoadApolloConfigsWithNoApolloRegistry() throws Exception { + when(configUtil.loadApolloRegistries()).thenReturn(null); + + CompositePropertySource result = configLoaderManager.loadPropertySource(); + + assertTrue(result.getPropertySources().isEmpty()); + } + + @Test + public void testLoadSingleApolloConfig() throws Exception { + ApolloConfig someApolloConfig = mock(ApolloConfig.class); + Map someMap = Maps.newHashMap(); + long someAppId = 100; + ApolloRegistry someApolloRegistry = assembleSomeApolloRegistry(someAppId, "someVersion"); + ApolloConfig previousConfig = null; + + doReturn(null).when(configLoaderManager).getPreviousApolloConfig(someApolloRegistry); + when(someApolloConfig.getConfigurations()).thenReturn(someMap); + when(configLoader.loadApolloConfig(someApolloRegistry, previousConfig)).thenReturn(someApolloConfig); + + ApolloConfig result = configLoaderManager.loadSingleApolloConfig(someApolloRegistry); + + assertEquals(someMap, result.getConfigurations()); + } + + @Test + public void testReloadPropertySource() throws Exception { + long someAppId = 100; + ApolloRegistry someApolloRegistry = assembleSomeApolloRegistry(someAppId, "someVersion"); + ApolloConfig someApolloConfig = mock(ApolloConfig.class); + Map someMap = mock(Map.class); + List someChanges = mock(List.class); + ReflectionTestUtils.setField(configLoaderManager, "apolloRegistries", Lists.newArrayList(someApolloRegistry)); + + doReturn(someApolloConfig).when(configLoaderManager).loadSingleApolloConfig(someApolloRegistry); + when(someApolloConfig.getAppId()).thenReturn(someAppId); + when(someApolloConfig.getConfigurations()).thenReturn(someMap); + doReturn(someChanges).when(configLoaderManager).calcPropertyChanges(anyList(), anyList()); + + PropertySourceReloadResult result = configLoaderManager.reloadPropertySource(); + + assertEquals(1, result.getPropertySource().getPropertySources().size()); + assertEquals(someChanges, result.getChanges()); + List> resultMaps = FluentIterable.from(result.getPropertySource().getPropertySources()).transform(new Function, Map>() { + @Override + public Map apply(PropertySource input) { + return (Map)input.getSource(); + } + }).toList(); + + assertTrue(resultMaps.containsAll(Lists.newArrayList(someMap))); + } + + @Test + public void testCalcPropertyChanges() throws Exception { + long someAppId = 1; + Map someConfig = Maps.newHashMap(); + someConfig.put("key1", "val1"); + someConfig.put("key2", "val2"); + + Map anotherConfig = Maps.newHashMap(); + anotherConfig.put("key1", "val11"); + anotherConfig.put("key3", "val3"); + + List previous = Lists.newArrayList(assembleApolloConfig(someAppId, someConfig)); + List current = Lists.newArrayList(assembleApolloConfig(someAppId, anotherConfig)); + + List changes = configLoaderManager.calcPropertyChanges(previous, current); + + assertEquals(3, changes.size()); + + List changeResult = FluentIterable.from(changes).transform(new Function() { + @Override + public String apply(PropertyChange input) { + return String.format("%s-%s", input.getPropertyName(), input.getChangeType()); + } + }).toList(); + + assertTrue(changeResult.containsAll( + Lists.newArrayList( + "key1-" + PropertyChangeType.MODIFIED, + "key2-" + PropertyChangeType.DELETED, + "key3-" + PropertyChangeType.NEW + ))); + } + + ApolloConfig assembleApolloConfig(long appId, Map configurations) { + String someCluster = "someCluster"; + String someVersion = "someVersion"; + long someReleaseId = 1; + + ApolloConfig config = new ApolloConfig(appId, someCluster, someVersion, someReleaseId); + + config.setConfigurations(configurations); + + return config; + } + + private ApolloRegistry assembleSomeApolloRegistry(long someAppId, String someVersion) { + ApolloRegistry someApolloRegistry = new ApolloRegistry(); + someApolloRegistry.setAppId(someAppId); + someApolloRegistry.setVersion(someVersion); + + return someApolloRegistry; + } +} diff --git a/apollo-client/src/test/java/com/ctrip/apollo/client/loader/impl/RemoteConfigLoaderTest.java b/apollo-client/src/test/java/com/ctrip/apollo/client/loader/impl/RemoteConfigLoaderTest.java index 2f16b0a5f..5a5ed517f 100644 --- a/apollo-client/src/test/java/com/ctrip/apollo/client/loader/impl/RemoteConfigLoaderTest.java +++ b/apollo-client/src/test/java/com/ctrip/apollo/client/loader/impl/RemoteConfigLoaderTest.java @@ -12,11 +12,16 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.MapPropertySource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.*; @@ -29,6 +34,8 @@ public class RemoteConfigLoaderTest { @Mock private RestTemplate restTemplate; private ConfigUtil configUtil; + @Mock + private ResponseEntity someResponse; @Before public void setUp() { @@ -37,57 +44,78 @@ public class RemoteConfigLoaderTest { } @Test - public void testLoadPropertySource() throws Exception { - long someAppId = 100; - long anotherAppId = 101; - ApolloRegistry someApolloRegistry = assembleSomeApolloRegistry(someAppId, "someVersion"); - ApolloRegistry anotherApolloRegistry = assembleSomeApolloRegistry(anotherAppId, "anotherVersion"); - MapPropertySource somePropertySource = mock(MapPropertySource.class); - MapPropertySource anotherPropertySource = mock(MapPropertySource.class); - - doReturn(Lists.newArrayList(someApolloRegistry, anotherApolloRegistry)).when(configUtil).loadApolloRegistries(); - doReturn(somePropertySource).when(remoteConfigLoader).loadSingleApolloConfig(someApolloRegistry.getAppId(), someApolloRegistry.getVersion()); - doReturn(anotherPropertySource).when(remoteConfigLoader).loadSingleApolloConfig(anotherApolloRegistry.getAppId(), anotherApolloRegistry.getVersion()); - - CompositePropertySource result = remoteConfigLoader.loadPropertySource(); - - assertEquals(2, result.getPropertySources().size()); - assertTrue(result.getPropertySources().containsAll(Lists.newArrayList(somePropertySource, anotherPropertySource))); + public void testLoadApolloConfig() throws Exception { + String someServerUrl = "http://someUrl"; + String someCluster = "some cluster"; + ApolloConfig apolloConfig = mock(ApolloConfig.class); + long someAppId = 1; + ApolloRegistry apolloRegistry = assembleSomeApolloRegistry(someAppId, "someVersion"); + ApolloConfig previousConfig = null; + + when(configUtil.getConfigServerUrl()).thenReturn(someServerUrl); + when(configUtil.getCluster()).thenReturn(someCluster); + doReturn(apolloConfig).when(remoteConfigLoader) + .getRemoteConfig(restTemplate, someServerUrl, someCluster, apolloRegistry, previousConfig); + + ApolloConfig result = remoteConfigLoader.loadApolloConfig(apolloRegistry, previousConfig); + + assertEquals(apolloConfig, result); } @Test - public void testLoadPropertySourceWithNoApolloRegistry() throws Exception { - doReturn(null).when(configUtil).loadApolloRegistries(); + public void testGetRemoteConfig() throws Exception { + long someAppId = 1; + String someServerUrl = "http://someServer"; + String someClusterName = "someCluster"; + String someVersionName = "someVersion"; + ApolloConfig someApolloConfig = mock(ApolloConfig.class); + ApolloRegistry apolloRegistry = assembleSomeApolloRegistry(someAppId, someVersionName); + ApolloConfig previousConfig = null; - CompositePropertySource result = remoteConfigLoader.loadPropertySource(); + when(someResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(someResponse.getBody()).thenReturn(someApolloConfig); + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), + eq(ApolloConfig.class), anyMap())).thenReturn(someResponse); - assertTrue(result.getPropertySources().isEmpty()); + ApolloConfig result = remoteConfigLoader.getRemoteConfig(restTemplate, someServerUrl, someClusterName, apolloRegistry, previousConfig); + + assertEquals(someApolloConfig, result); } @Test(expected = RuntimeException.class) - public void testLoadPropertySourceWithError() throws Exception { - Exception someException = mock(Exception.class); - long someAppId = 100; - ApolloRegistry someApolloRegistry = assembleSomeApolloRegistry(someAppId, "someVersion"); - doReturn(Lists.newArrayList(someApolloRegistry)).when(configUtil).loadApolloRegistries(); - - doThrow(someException).when(remoteConfigLoader).loadSingleApolloConfig(someApolloRegistry.getAppId(), someApolloRegistry.getVersion()); - - remoteConfigLoader.loadPropertySource(); + public void testGetRemoteConfigWithServerError() throws Exception { + long someAppId = 1; + String someServerUrl = "http://someServer"; + String someClusterName = "someCluster"; + String someVersionName = "someVersion"; + ApolloRegistry apolloRegistry = assembleSomeApolloRegistry(someAppId, someVersionName); + ApolloConfig previousConfig = null; + HttpStatus someErrorCode = HttpStatus.INTERNAL_SERVER_ERROR; + + when(someResponse.getStatusCode()).thenReturn(someErrorCode); + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), + eq(ApolloConfig.class), anyMap())).thenReturn(someResponse); + + remoteConfigLoader.getRemoteConfig(restTemplate, someServerUrl, someClusterName, apolloRegistry, previousConfig); } @Test - public void testLoadSingleApolloConfig() throws Exception { - ApolloConfig someApolloConfig = mock(ApolloConfig.class); - Map someMap = Maps.newHashMap(); + public void testGetRemoteConfigWith304Response() throws Exception { + long someAppId = 1; + String someServerUrl = "http://someServer"; + String someClusterName = "someCluster"; + String someVersionName = "someVersion"; + ApolloRegistry apolloRegistry = assembleSomeApolloRegistry(someAppId, someVersionName); + ApolloConfig previousConfig = null; + + when(someResponse.getStatusCode()).thenReturn(HttpStatus.NOT_MODIFIED); + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), + eq(ApolloConfig.class), anyMap())).thenReturn(someResponse); - when(someApolloConfig.getConfigurations()).thenReturn(someMap); - doReturn(someApolloConfig).when(remoteConfigLoader).getRemoteConfig(any(RestTemplate.class), anyString(), anyLong(), anyString(), anyString()); + ApolloConfig result = remoteConfigLoader.getRemoteConfig(restTemplate, someServerUrl, someClusterName, apolloRegistry, previousConfig); - long someAppId = 100; - MapPropertySource result = remoteConfigLoader.loadSingleApolloConfig(someAppId, "someVersion"); + assertNull(result); - assertEquals(someMap, result.getSource()); } private ApolloRegistry assembleSomeApolloRegistry(long someAppId, String someVersion) { diff --git a/apollo-configservice/src/main/java/com/ctrip/apollo/configservice/controller/ConfigController.java b/apollo-configservice/src/main/java/com/ctrip/apollo/configservice/controller/ConfigController.java index 9467647e4..c2ba0db53 100644 --- a/apollo-configservice/src/main/java/com/ctrip/apollo/configservice/controller/ConfigController.java +++ b/apollo-configservice/src/main/java/com/ctrip/apollo/configservice/controller/ConfigController.java @@ -3,10 +3,7 @@ package com.ctrip.apollo.configservice.controller; import com.ctrip.apollo.biz.entity.Version; import com.ctrip.apollo.biz.service.ConfigService; import com.ctrip.apollo.core.dto.ApolloConfig; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; @@ -21,7 +18,7 @@ public class ConfigController { @Resource(name = "configService") private ConfigService configService; - @RequestMapping(value = "/{appId}/{clusterName}/{versionName:.*}") + @RequestMapping(value = "/{appId}/{clusterName}/{versionName:.*}", method = RequestMethod.GET) public ApolloConfig queryConfig(@PathVariable long appId, @PathVariable String clusterName, @PathVariable String versionName, diff --git a/apollo-core/src/main/java/com/ctrip/apollo/core/dto/ApolloConfig.java b/apollo-core/src/main/java/com/ctrip/apollo/core/dto/ApolloConfig.java index fd987fd7a..724534471 100644 --- a/apollo-core/src/main/java/com/ctrip/apollo/core/dto/ApolloConfig.java +++ b/apollo-core/src/main/java/com/ctrip/apollo/core/dto/ApolloConfig.java @@ -4,12 +4,13 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.MoreObjects; +import java.util.Comparator; import java.util.Map; /** * @author Jason Song(song_s@ctrip.com) */ -public class ApolloConfig { +public class ApolloConfig implements Comparable { private long appId; @@ -21,6 +22,8 @@ public class ApolloConfig { private long releaseId; + private int order; + @JsonCreator public ApolloConfig(@JsonProperty("appId") long appId, @JsonProperty("cluster") String cluster, @@ -57,6 +60,14 @@ public class ApolloConfig { return releaseId; } + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) @@ -68,4 +79,15 @@ public class ApolloConfig { .add("configurations", configurations) .toString(); } + + @Override + public int compareTo(ApolloConfig o) { + if (o == null || this.getOrder() > o.getOrder()) { + return 1; + } + if (o.getOrder() > this.getOrder()) { + return -1; + } + return 0; + } } diff --git a/apollo-demo/src/main/java/com/ctrip/apollo/demo/controller/DemoController.java b/apollo-demo/src/main/java/com/ctrip/apollo/demo/controller/DemoController.java index a3cacb170..210f69a4a 100644 --- a/apollo-demo/src/main/java/com/ctrip/apollo/demo/controller/DemoController.java +++ b/apollo-demo/src/main/java/com/ctrip/apollo/demo/controller/DemoController.java @@ -1,6 +1,8 @@ package com.ctrip.apollo.demo.controller; +import com.ctrip.apollo.client.ApolloConfigManager; import com.ctrip.apollo.client.model.ApolloRegistry; +import com.ctrip.apollo.client.model.PropertyChange; import com.ctrip.apollo.client.util.ConfigUtil; import com.ctrip.apollo.demo.model.Config; import com.ctrip.apollo.demo.service.DemoService; @@ -27,11 +29,12 @@ public class DemoController { private Environment env; @Autowired private DemoService demoService; - //Apollo config client internal impl, not intended to be used by application, only for this test page + //Apollo config client internal impl, not intended to be used by application, only for this test page! private ConfigUtil configUtil = ConfigUtil.getInstance(); + //ApolloConfigManager, not intended to be used by application, only for this test page! @Autowired - private RefreshScope scope; + private ApolloConfigManager apolloConfigManager; @RequestMapping(value = "/config/{configName:.*}", method = RequestMethod.GET) public Config queryConfig(@PathVariable String configName) { @@ -49,8 +52,8 @@ public class DemoController { } @RequestMapping(value = "/refresh", method = RequestMethod.POST) - public String refreshBeans() { - this.scope.refreshAll(); - return "ok"; + public List refreshBeans() { + List changes = this.apolloConfigManager.updatePropertySource(); + return changes; } } diff --git a/apollo-demo/src/main/webapp/s/scripts/app.js b/apollo-demo/src/main/webapp/s/scripts/app.js index 84e4b1c85..421b3b4b7 100644 --- a/apollo-demo/src/main/webapp/s/scripts/app.js +++ b/apollo-demo/src/main/webapp/s/scripts/app.js @@ -7,11 +7,11 @@ ]); app.controller('DemoController', function ($scope, $http, $modal, toastr) { - //var NONE = "none"; + var NONE = "none"; this.registries = {}; this.configQuery = {}; - //this.refreshResult = NONE; + this.refreshResult = NONE; this.injectedConfigValue = ''; var self = this; @@ -46,24 +46,26 @@ }); }; - //this.refreshConfig = function() { - // $http.post("refresh") - // .success(function(data) { - // self.assembleRefreshResult(data); - // }) - // .error(function(data, status) { - // toastr.error((data && data.msg) || 'Refresh config failed'); - // }); - // - //}; + this.refreshConfig = function() { + $http.post("demo/refresh") + .success(function(data) { + self.assembleRefreshResult(data); + }) + .error(function(data, status) { + toastr.error((data && data.msg) || 'Refresh config failed'); + }); - //this.assembleRefreshResult = function(changedPropertyArray) { - // if(!changedPropertyArray || !changedPropertyArray.length) { - // this.refreshResult = NONE; - // return; - // } - // this.refreshResult = changedPropertyArray.join(','); - //}; + }; + + this.assembleRefreshResult = function(changedPropertyArray) { + if(!changedPropertyArray || !changedPropertyArray.length) { + this.refreshResult = NONE; + return; + } + this.refreshResult = _.map(changedPropertyArray, function(propertyChange) { + return propertyChange.propertyName + '(' + propertyChange.changeType + ')'; + }); + }; this.loadRegistries(); diff --git a/apollo-demo/src/main/webapp/s/templates/list.html b/apollo-demo/src/main/webapp/s/templates/list.html index 76636cc6e..c577f1e03 100644 --- a/apollo-demo/src/main/webapp/s/templates/list.html +++ b/apollo-demo/src/main/webapp/s/templates/list.html @@ -70,12 +70,14 @@ - - - - - - - +
+

Refresh Config:

+ +
+ Changed Properties: {{demoCtrl.refreshResult}} +
+
-- GitLab