From ccc974a26831e3184c19b1e13908ee036770cf96 Mon Sep 17 00:00:00 2001 From: tian90coder Date: Tue, 1 Aug 2023 17:53:28 +0800 Subject: [PATCH] Support concurrent loading of Config for different namespaces (#29) * Support concurrent loading of Config for different namespaces. * Supports loading the apollo module through spi --- .../internals/ConcurrentConfigManager.java | 62 +++++++ .../internals/DefaultConfigManager.java | 21 ++- .../apollo/internals/DefaultInjector.java | 44 +++-- .../internals/AbstractConfigManagerTest.java | 167 ++++++++++++++++++ .../ConcurrentConfigManagerTest.java | 28 +++ .../internals/DefaultConfigManagerTest.java | 145 +-------------- .../internals/ServiceBootstrap.java | 72 ++++++++ .../internals/ServiceBootstrapTest.java | 8 + 8 files changed, 391 insertions(+), 156 deletions(-) create mode 100644 apollo-client/src/main/java/com/ctrip/framework/apollo/internals/ConcurrentConfigManager.java create mode 100644 apollo-client/src/test/java/com/ctrip/framework/apollo/internals/AbstractConfigManagerTest.java create mode 100644 apollo-client/src/test/java/com/ctrip/framework/apollo/internals/ConcurrentConfigManagerTest.java diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/ConcurrentConfigManager.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/ConcurrentConfigManager.java new file mode 100644 index 00000000..37321668 --- /dev/null +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/ConcurrentConfigManager.java @@ -0,0 +1,62 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.internals; + +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigFile; +import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; +import com.google.common.collect.Maps; + +import java.util.Map; + +/** + * @author tian + */ +public class ConcurrentConfigManager extends DefaultConfigManager { + + private final Map m_configLocks = Maps.newConcurrentMap(); + private final Map m_configFileLocks = Maps.newConcurrentMap(); + + @Override + public Config getConfig(String namespace) { + String lockKey = buildLockKey(namespace); + Object lock = m_configLocks.get(lockKey); + if (lock == null) { + synchronized (lockKey) { + lock = m_configLocks.computeIfAbsent(lockKey, k -> new Object()); + } + } + return getConfigInLock(lock, namespace); + } + + @Override + public ConfigFile getConfigFile(String namespace, ConfigFileFormat configFileFormat) { + String namespaceFileName = buildNamespaceFileName(namespace, configFileFormat); + String lockKey = buildLockKey(namespaceFileName); + Object lock = m_configFileLocks.get(lockKey); + if (lock == null) { + synchronized (lockKey) { + lock = m_configFileLocks.computeIfAbsent(lockKey, k -> new Object()); + } + } + return getConfigFileInLock(lock, namespaceFileName, configFileFormat); + } + + private String buildLockKey(String factor) { + return (String.format("%s.%s", getClass().getName(), factor)).intern(); + } +} diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultConfigManager.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultConfigManager.java index f583a915..6429ea75 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultConfigManager.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultConfigManager.java @@ -41,10 +41,13 @@ public DefaultConfigManager() { @Override public Config getConfig(String namespace) { - Config config = m_configs.get(namespace); + return getConfigInLock(this, namespace); + } + protected Config getConfigInLock(Object lock, String namespace) { + Config config = m_configs.get(namespace); if (config == null) { - synchronized (this) { + synchronized (lock) { config = m_configs.get(namespace); if (config == null) { @@ -55,17 +58,21 @@ public Config getConfig(String namespace) { } } } - return config; } @Override public ConfigFile getConfigFile(String namespace, ConfigFileFormat configFileFormat) { - String namespaceFileName = String.format("%s.%s", namespace, configFileFormat.getValue()); + String namespaceFileName = buildNamespaceFileName(namespace, configFileFormat); + return getConfigFileInLock(this, namespaceFileName, configFileFormat); + } + + protected ConfigFile getConfigFileInLock(Object lock, String namespaceFileName, + ConfigFileFormat configFileFormat) { ConfigFile configFile = m_configFiles.get(namespaceFileName); if (configFile == null) { - synchronized (this) { + synchronized (lock) { configFile = m_configFiles.get(namespaceFileName); if (configFile == null) { @@ -79,4 +86,8 @@ public ConfigFile getConfigFile(String namespace, ConfigFileFormat configFileFor return configFile; } + + protected String buildNamespaceFileName(String namespace, ConfigFileFormat configFileFormat) { + return String.format("%s.%s", namespace, configFileFormat.getValue()); + } } diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultInjector.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultInjector.java index 707ad6c5..d51aff30 100644 --- a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultInjector.java +++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultInjector.java @@ -36,6 +36,7 @@ import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Singleton; +import java.lang.annotation.Annotation; import java.util.List; /** @@ -95,16 +96,39 @@ public T getInstance(Class clazz, String name) { private static class ApolloModule extends AbstractModule { @Override protected void configure() { - bind(ConfigManager.class).to(DefaultConfigManager.class).in(Singleton.class); - bind(ConfigFactoryManager.class).to(DefaultConfigFactoryManager.class).in(Singleton.class); - bind(ConfigRegistry.class).to(DefaultConfigRegistry.class).in(Singleton.class); - bind(ConfigFactory.class).to(DefaultConfigFactory.class).in(Singleton.class); - bind(ConfigUtil.class).in(Singleton.class); - bind(HttpClient.class).to(DefaultHttpClient.class).in(Singleton.class); - bind(ConfigServiceLocator.class).in(Singleton.class); - bind(RemoteConfigLongPollService.class).in(Singleton.class); - bind(YamlParser.class).in(Singleton.class); - bind(PropertiesFactory.class).to(DefaultPropertiesFactory.class).in(Singleton.class); + bind(ConfigManager.class, DefaultConfigManager.class, Singleton.class); + bind(ConfigFactoryManager.class, DefaultConfigFactoryManager.class, Singleton.class); + bind(ConfigRegistry.class, DefaultConfigRegistry.class, Singleton.class); + bind(ConfigFactory.class, DefaultConfigFactory.class, Singleton.class); + bind(ConfigUtil.class, Singleton.class); + bind(HttpClient.class, DefaultHttpClient.class, Singleton.class); + bind(ConfigServiceLocator.class, Singleton.class); + bind(RemoteConfigLongPollService.class, Singleton.class); + bind(YamlParser.class, Singleton.class); + bind(PropertiesFactory.class, DefaultPropertiesFactory.class, Singleton.class); + } + + /** + * bind the type's implementation from spi + * defaultImpl will be bound if the implementation cannot be found + */ + private void bind(Class bindClass, Class defaultImpl, Class in) { + List> classList = ServiceBootstrap.loadClass(bindClass); + Class impl = defaultImpl; + if (!classList.isEmpty()) { + Class spiClass = classList.get(0); + if (bindClass.isAssignableFrom(spiClass)) { + impl = (Class) spiClass; + } + } + bind(bindClass).to(impl).in(in); + } + + /** + * bind the determining type + */ + private void bind(Class bindClass, Class in) { + bind(bindClass).in(in); } } } diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/AbstractConfigManagerTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/AbstractConfigManagerTest.java new file mode 100644 index 00000000..061bf79f --- /dev/null +++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/AbstractConfigManagerTest.java @@ -0,0 +1,167 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.internals; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; + +import com.ctrip.framework.apollo.enums.ConfigSourceType; +import java.util.Properties; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigFile; +import com.ctrip.framework.apollo.build.MockInjector; +import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; +import com.ctrip.framework.apollo.spi.ConfigFactory; +import com.ctrip.framework.apollo.spi.ConfigFactoryManager; +import com.ctrip.framework.apollo.util.ConfigUtil; + +/** + * @author tian + */ +public abstract class AbstractConfigManagerTest { + private ConfigManager configManager; + private static String someConfigContent; + + protected abstract ConfigManager getConfigManager(); + + @Before + public void setUp() throws Exception { + MockInjector.setInstance(ConfigFactoryManager.class, new MockConfigFactoryManager()); + MockInjector.setInstance(ConfigUtil.class, new ConfigUtil()); + configManager = getConfigManager(); + someConfigContent = "someContent"; + } + + @After + public void tearDown() throws Exception { + MockInjector.reset(); + } + + @Test + public void testGetConfig() throws Exception { + String someNamespace = "someName"; + String anotherNamespace = "anotherName"; + String someKey = "someKey"; + Config config = configManager.getConfig(someNamespace); + Config anotherConfig = configManager.getConfig(anotherNamespace); + + assertEquals(someNamespace + ":" + someKey, config.getProperty(someKey, null)); + assertEquals(anotherNamespace + ":" + someKey, anotherConfig.getProperty(someKey, null)); + } + + @Test + public void testGetConfigMultipleTimesWithSameNamespace() throws Exception { + String someNamespace = "someName"; + Config config = configManager.getConfig(someNamespace); + Config anotherConfig = configManager.getConfig(someNamespace); + + assertThat( + "Get config multiple times with the same namespace should return the same config instance", + config, equalTo(anotherConfig)); + } + + @Test + public void testGetConfigFile() throws Exception { + String someNamespace = "someName"; + ConfigFileFormat someConfigFileFormat = ConfigFileFormat.Properties; + + ConfigFile configFile = + configManager.getConfigFile(someNamespace, someConfigFileFormat); + + assertEquals(someConfigFileFormat, configFile.getConfigFileFormat()); + assertEquals(someConfigContent, configFile.getContent()); + } + + @Test + public void testGetConfigFileMultipleTimesWithSameNamespace() throws Exception { + String someNamespace = "someName"; + ConfigFileFormat someConfigFileFormat = ConfigFileFormat.Properties; + + ConfigFile someConfigFile = + configManager.getConfigFile(someNamespace, someConfigFileFormat); + ConfigFile anotherConfigFile = + configManager.getConfigFile(someNamespace, someConfigFileFormat); + + assertThat( + "Get config file multiple times with the same namespace should return the same config file instance", + someConfigFile, equalTo(anotherConfigFile)); + + } + + public static class MockConfigFactoryManager implements ConfigFactoryManager { + + @Override + public ConfigFactory getFactory(String namespace) { + return new ConfigFactory() { + @Override + public Config create(final String namespace) { + return new AbstractConfig() { + @Override + public String getProperty(String key, String defaultValue) { + return namespace + ":" + key; + } + + @Override + public Set getPropertyNames() { + return null; + } + + @Override + public ConfigSourceType getSourceType() { + return null; + } + }; + } + + @Override + public ConfigFile createConfigFile(String namespace, final ConfigFileFormat configFileFormat) { + ConfigRepository someConfigRepository = mock(ConfigRepository.class); + return new AbstractConfigFile(namespace, someConfigRepository) { + + @Override + protected void update(Properties newProperties) { + + } + + @Override + public String getContent() { + return someConfigContent; + } + + @Override + public boolean hasContent() { + return true; + } + + @Override + public ConfigFileFormat getConfigFileFormat() { + return configFileFormat; + } + }; + } + }; + } + } +} diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/ConcurrentConfigManagerTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/ConcurrentConfigManagerTest.java new file mode 100644 index 00000000..d4693309 --- /dev/null +++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/ConcurrentConfigManagerTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.internals; + +/** + * @author tian + */ +public class ConcurrentConfigManagerTest extends AbstractConfigManagerTest { + + @Override + protected ConfigManager getConfigManager() { + return new ConcurrentConfigManager(); + } +} diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/DefaultConfigManagerTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/DefaultConfigManagerTest.java index fdc9bb41..69fb5f11 100644 --- a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/DefaultConfigManagerTest.java +++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/DefaultConfigManagerTest.java @@ -16,150 +16,13 @@ */ package com.ctrip.framework.apollo.internals; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.junit.Assert.assertEquals; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Mockito.mock; - -import com.ctrip.framework.apollo.enums.ConfigSourceType; -import java.util.Properties; -import java.util.Set; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import com.ctrip.framework.apollo.Config; -import com.ctrip.framework.apollo.ConfigFile; -import com.ctrip.framework.apollo.build.MockInjector; -import com.ctrip.framework.apollo.core.enums.ConfigFileFormat; -import com.ctrip.framework.apollo.spi.ConfigFactory; -import com.ctrip.framework.apollo.spi.ConfigFactoryManager; -import com.ctrip.framework.apollo.util.ConfigUtil; - /** * @author Jason Song(song_s@ctrip.com) */ -public class DefaultConfigManagerTest { - private DefaultConfigManager defaultConfigManager; - private static String someConfigContent; - - @Before - public void setUp() throws Exception { - MockInjector.setInstance(ConfigFactoryManager.class, new MockConfigFactoryManager()); - MockInjector.setInstance(ConfigUtil.class, new ConfigUtil()); - defaultConfigManager = new DefaultConfigManager(); - someConfigContent = "someContent"; - } - - @After - public void tearDown() throws Exception { - MockInjector.reset(); - } - - @Test - public void testGetConfig() throws Exception { - String someNamespace = "someName"; - String anotherNamespace = "anotherName"; - String someKey = "someKey"; - Config config = defaultConfigManager.getConfig(someNamespace); - Config anotherConfig = defaultConfigManager.getConfig(anotherNamespace); - - assertEquals(someNamespace + ":" + someKey, config.getProperty(someKey, null)); - assertEquals(anotherNamespace + ":" + someKey, anotherConfig.getProperty(someKey, null)); - } - - @Test - public void testGetConfigMultipleTimesWithSameNamespace() throws Exception { - String someNamespace = "someName"; - Config config = defaultConfigManager.getConfig(someNamespace); - Config anotherConfig = defaultConfigManager.getConfig(someNamespace); - - assertThat( - "Get config multiple times with the same namespace should return the same config instance", - config, equalTo(anotherConfig)); - } - - @Test - public void testGetConfigFile() throws Exception { - String someNamespace = "someName"; - ConfigFileFormat someConfigFileFormat = ConfigFileFormat.Properties; - - ConfigFile configFile = - defaultConfigManager.getConfigFile(someNamespace, someConfigFileFormat); - - assertEquals(someConfigFileFormat, configFile.getConfigFileFormat()); - assertEquals(someConfigContent, configFile.getContent()); - } - - @Test - public void testGetConfigFileMultipleTimesWithSameNamespace() throws Exception { - String someNamespace = "someName"; - ConfigFileFormat someConfigFileFormat = ConfigFileFormat.Properties; - - ConfigFile someConfigFile = - defaultConfigManager.getConfigFile(someNamespace, someConfigFileFormat); - ConfigFile anotherConfigFile = - defaultConfigManager.getConfigFile(someNamespace, someConfigFileFormat); - - assertThat( - "Get config file multiple times with the same namespace should return the same config file instance", - someConfigFile, equalTo(anotherConfigFile)); - - } - - public static class MockConfigFactoryManager implements ConfigFactoryManager { - - @Override - public ConfigFactory getFactory(String namespace) { - return new ConfigFactory() { - @Override - public Config create(final String namespace) { - return new AbstractConfig() { - @Override - public String getProperty(String key, String defaultValue) { - return namespace + ":" + key; - } - - @Override - public Set getPropertyNames() { - return null; - } - - @Override - public ConfigSourceType getSourceType() { - return null; - } - }; - } - - @Override - public ConfigFile createConfigFile(String namespace, final ConfigFileFormat configFileFormat) { - ConfigRepository someConfigRepository = mock(ConfigRepository.class); - return new AbstractConfigFile(namespace, someConfigRepository) { - - @Override - protected void update(Properties newProperties) { - - } - - @Override - public String getContent() { - return someConfigContent; - } - - @Override - public boolean hasContent() { - return true; - } +public class DefaultConfigManagerTest extends AbstractConfigManagerTest { - @Override - public ConfigFileFormat getConfigFileFormat() { - return configFileFormat; - } - }; - } - }; - } + @Override + protected ConfigManager getConfigManager() { + return new DefaultConfigManager(); } } diff --git a/apollo-core/src/main/java/com/ctrip/framework/foundation/internals/ServiceBootstrap.java b/apollo-core/src/main/java/com/ctrip/framework/foundation/internals/ServiceBootstrap.java index 27d62cf8..49d18435 100644 --- a/apollo-core/src/main/java/com/ctrip/framework/foundation/internals/ServiceBootstrap.java +++ b/apollo-core/src/main/java/com/ctrip/framework/foundation/internals/ServiceBootstrap.java @@ -18,14 +18,28 @@ import com.ctrip.framework.apollo.core.spi.Ordered; import com.google.common.collect.Lists; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; +import java.util.Set; public class ServiceBootstrap { + private static final String PREFIX = "META-INF/services/"; + + private static final Set loadedUrls = new HashSet<>(); + /** * @deprecated use {@link ServiceBootstrap#loadPrimary(Class)} instead */ @@ -66,4 +80,62 @@ public static S loadPrimary(Class clazz) { return candidates.get(0); } + + public static List> loadClass(Class clazz) { + return loadClass(clazz, Thread.currentThread().getContextClassLoader()); + } + + /** + * load spi class without instancing + */ + public static List> loadClass(Class clazz, ClassLoader classLoader) { + if (classLoader == null) { + return Collections.emptyList(); + } + List> serviceClassList = new ArrayList<>(); + String className = clazz.getName(); + String path = PREFIX + className; + Set serviceNames = new HashSet<>(); + try { + Enumeration urls = classLoader.getResources(path); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + if (loadedUrls.contains(url.toString())) { + continue; + } + load(url, serviceNames); + loadedUrls.add(url.toString()); + } + } catch (Throwable ignored) { + } + for (String serviceName : serviceNames) { + try { + Class serviceClass = classLoader.loadClass(serviceName); + serviceClassList.add(serviceClass); + } catch (Exception ignored) { + } + } + return serviceClassList; + } + + private static void load(URL url, Set set) throws IOException { + try (InputStream is = url.openStream(); BufferedReader reader = new BufferedReader( + new InputStreamReader(is, StandardCharsets.UTF_8))) { + while (true) { + String line = reader.readLine(); + if (line == null) { + break; + } + int ci = line.indexOf('#'); + if (ci >= 0) { + line = line.substring(0, ci); + } + line = line.trim(); + if (line.isEmpty()) { + continue; + } + set.add(line); + } + } + } } diff --git a/apollo-core/src/test/java/com/ctrip/framework/foundation/internals/ServiceBootstrapTest.java b/apollo-core/src/test/java/com/ctrip/framework/foundation/internals/ServiceBootstrapTest.java index 1cb35cb3..be0dccfc 100644 --- a/apollo-core/src/test/java/com/ctrip/framework/foundation/internals/ServiceBootstrapTest.java +++ b/apollo-core/src/test/java/com/ctrip/framework/foundation/internals/ServiceBootstrapTest.java @@ -17,11 +17,13 @@ package com.ctrip.framework.foundation.internals; import com.ctrip.framework.apollo.core.spi.Ordered; +import java.util.List; import org.junit.Test; import java.util.ServiceConfigurationError; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; /** @@ -75,6 +77,12 @@ public void loadAllOrderedWithServiceFileButNoServiceImpl() { assertTrue(ServiceBootstrap.loadAllOrdered(Interface7.class).isEmpty()); } + @Test + public void loadClassWithoutInstancing() { + List> classList = ServiceBootstrap.loadClass(Interface1.class); + assertSame(Interface1Impl.class, classList.get(0)); + } + interface Interface1 { }