doGet(HttpRequest httpRequest, Type responseType)
throws ApolloConfigException {
return this.doGetInternal(httpRequest, responseType);
}
+
+ /**
+ * Resolve HTTP status code across Spring WebFlux 5/6/7.
+ *
+ * ClientResponse#statusCode has different return types across major versions
+ * (HttpStatus in Spring 5, HttpStatusCode in Spring 6/7). Calling it directly would bind
+ * to one method descriptor at compile time and could fail on another runtime version.
+ * Reflection keeps this bridge binary-compatible for Boot 2/3/4 compatibility tests.
+ */
+ private int resolveStatusCode(Object clientResponse) {
+ try {
+ Object statusCode = CLIENT_RESPONSE_STATUS_CODE_METHOD.invoke(clientResponse);
+ if (statusCode == null) {
+ throw new ApolloConfigException("Failed to resolve response status code: statusCode is null");
+ }
+ Method valueMethod = STATUS_CODE_VALUE_METHOD_CACHE.computeIfAbsent(statusCode.getClass(),
+ ApolloWebClientHttpClient::resolveStatusCodeValueMethod);
+ Object value = valueMethod.invoke(statusCode);
+ return ((Number) value).intValue();
+ } catch (Exception ex) {
+ throw new ApolloConfigException("Failed to resolve response status code", ex);
+ }
+ }
+
+ private static Method resolveClientResponseStatusCodeMethod() {
+ try {
+ return ClientResponse.class.getMethod("statusCode");
+ } catch (NoSuchMethodException ex) {
+ throw new ExceptionInInitializerError(ex);
+ }
+ }
+
+ private static Method resolveStatusCodeValueMethod(Class> statusCodeType) {
+ try {
+ return statusCodeType.getMethod("value");
+ } catch (NoSuchMethodException ex) {
+ throw new IllegalStateException("Failed to resolve value() method from " + statusCodeType, ex);
+ }
+ }
}
diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/customizer/spi/ApolloClientWebClientCustomizerFactory.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/customizer/spi/ApolloClientWebClientCustomizerFactory.java
index f7feb1ff..b02ef297 100644
--- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/customizer/spi/ApolloClientWebClientCustomizerFactory.java
+++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/customizer/spi/ApolloClientWebClientCustomizerFactory.java
@@ -18,11 +18,12 @@
import com.ctrip.framework.apollo.config.data.extension.properties.ApolloClientProperties;
import com.ctrip.framework.apollo.core.spi.Ordered;
+import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Binder;
-import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
import org.springframework.lang.Nullable;
+import org.springframework.web.reactive.function.client.WebClient;
/**
* @author vdisk
@@ -30,7 +31,7 @@
public interface ApolloClientWebClientCustomizerFactory extends Ordered {
/**
- * create a WebClientCustomizer instance
+ * create a webclient builder customizer
*
* @param apolloClientProperties apollo client binded properties
* @param binder properties binder
@@ -41,10 +42,10 @@ public interface ApolloClientWebClientCustomizerFactory extends Ordered {
* Spring Boot 3.x or
* org.springframework.boot.bootstrap.ConfigurableBootstrapContext
* for Spring Boot 4.x)
- * @return WebClientCustomizer instance or null
+ * @return customizer instance or null
*/
@Nullable
- WebClientCustomizer createWebClientCustomizer(ApolloClientProperties apolloClientProperties,
+ Consumer createWebClientCustomizer(ApolloClientProperties apolloClientProperties,
Binder binder, BindHandler bindHandler, Log log,
Object bootstrapContext);
}
diff --git a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/ApolloClientConfigDataAutoConfigurationTest.java b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/ApolloClientConfigDataAutoConfigurationTest.java
new file mode 100644
index 00000000..4ad4e050
--- /dev/null
+++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/ApolloClientConfigDataAutoConfigurationTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.config.data;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.ctrip.framework.apollo.config.data.extension.properties.ApolloClientProperties;
+import com.ctrip.framework.apollo.spring.config.ConfigPropertySourcesProcessor;
+import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.junit.Test;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author vdisk
+ */
+public class ApolloClientConfigDataAutoConfigurationTest {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(ApolloClientConfigDataAutoConfiguration.class));
+
+ @Test
+ public void testDefaultBeansLoaded() {
+ contextRunner.run(context -> {
+ assertThat(context).hasSingleBean(ApolloClientProperties.class);
+ assertThat(context).hasSingleBean(PropertySourcesProcessor.class);
+ assertThat(context.getBean(PropertySourcesProcessor.class))
+ .isInstanceOf(ConfigPropertySourcesProcessor.class);
+ });
+ }
+
+ @Test
+ public void testConditionalOnMissingBean() {
+ contextRunner.withUserConfiguration(CustomBeansConfiguration.class).run(context -> {
+ assertThat(context).hasSingleBean(ApolloClientProperties.class);
+ assertThat(context.getBean(ApolloClientProperties.class))
+ .isSameAs(context.getBean("customApolloClientProperties"));
+ assertThat(context.getBean(PropertySourcesProcessor.class))
+ .isSameAs(context.getBean("customPropertySourcesProcessor"));
+ });
+ }
+
+ @Configuration
+ static class CustomBeansConfiguration {
+
+ @Bean
+ public ApolloClientProperties customApolloClientProperties() {
+ return new ApolloClientProperties();
+ }
+
+ @Bean
+ public static PropertySourcesProcessor customPropertySourcesProcessor() {
+ return new ConfigPropertySourcesProcessor();
+ }
+ }
+}
diff --git a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientExtensionInitializeFactoryTest.java b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientExtensionInitializeFactoryTest.java
new file mode 100644
index 00000000..715dc172
--- /dev/null
+++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientExtensionInitializeFactoryTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.config.data.extension.initialize;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.ctrip.framework.apollo.config.data.injector.ApolloConfigDataInjectorCustomizer;
+import com.ctrip.framework.apollo.util.http.HttpClient;
+import java.lang.reflect.Field;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
+import org.springframework.boot.logging.DeferredLogFactory;
+
+/**
+ * @author vdisk
+ */
+public class ApolloClientExtensionInitializeFactoryTest {
+
+ private final DeferredLogFactory logFactory = destination -> destination.get();
+
+ @Before
+ public void setUp() throws Exception {
+ clearInjectorCustomizerCaches();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ clearInjectorCustomizerCaches();
+ }
+
+ @Test
+ public void testInitializeExtensionDisabledByDefault() {
+ ApolloClientExtensionInitializeFactory factory =
+ new ApolloClientExtensionInitializeFactory(logFactory, new Object());
+ Binder binder = new Binder(new MapConfigurationPropertySource(new LinkedHashMap<>()));
+
+ factory.initializeExtension(binder, null);
+
+ assertFalse(ApolloConfigDataInjectorCustomizer.isRegistered(HttpClient.class));
+ }
+
+ @Test
+ public void testInitializeLongPollingExtension() {
+ ApolloClientExtensionInitializeFactory factory =
+ new ApolloClientExtensionInitializeFactory(logFactory, new Object());
+ Map map = new LinkedHashMap<>();
+ map.put("apollo.client.extension.enabled", "true");
+ map.put("apollo.client.extension.messaging-type", "long_polling");
+ Binder binder = new Binder(new MapConfigurationPropertySource(map));
+
+ factory.initializeExtension(binder, null);
+
+ assertTrue(ApolloConfigDataInjectorCustomizer.isRegistered(HttpClient.class));
+ }
+
+ @Test
+ public void testInitializeWebsocketExtensionThrowsException() {
+ ApolloClientExtensionInitializeFactory factory =
+ new ApolloClientExtensionInitializeFactory(logFactory, new Object());
+ Map map = new LinkedHashMap<>();
+ map.put("apollo.client.extension.enabled", "true");
+ map.put("apollo.client.extension.messaging-type", "websocket");
+ Binder binder = new Binder(new MapConfigurationPropertySource(map));
+
+ try {
+ factory.initializeExtension(binder, null);
+ fail("Expected UnsupportedOperationException");
+ } catch (UnsupportedOperationException ex) {
+ assertTrue(ex.getMessage().contains("websocket support is not complete yet"));
+ }
+ }
+
+ private void clearInjectorCustomizerCaches() throws Exception {
+ Field instanceSuppliers =
+ ApolloConfigDataInjectorCustomizer.class.getDeclaredField("INSTANCE_SUPPLIERS");
+ instanceSuppliers.setAccessible(true);
+ ((Map, ?>) instanceSuppliers.get(null)).clear();
+
+ Field instances = ApolloConfigDataInjectorCustomizer.class.getDeclaredField("INSTANCES");
+ instances.setAccessible(true);
+ ((Map, ?>) instances.get(null)).clear();
+ }
+}
diff --git a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClientTest.java b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClientTest.java
new file mode 100644
index 00000000..ec912803
--- /dev/null
+++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClientTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.config.data.extension.webclient;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.ctrip.framework.apollo.exceptions.ApolloConfigStatusCodeException;
+import com.ctrip.framework.apollo.util.http.HttpRequest;
+import com.ctrip.framework.apollo.util.http.HttpResponse;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.web.reactive.function.client.WebClient;
+
+/**
+ * @author vdisk
+ */
+public class ApolloWebClientHttpClientTest {
+
+ private HttpServer server;
+ private ApolloWebClientHttpClient httpClient;
+
+ @Before
+ public void setUp() throws IOException {
+ server = HttpServer.create(new InetSocketAddress(0), 0);
+ server.start();
+ httpClient = new ApolloWebClientHttpClient(WebClient.builder().build());
+ }
+
+ @After
+ public void tearDown() {
+ server.stop(0);
+ }
+
+ @Test
+ public void testDoGetWith200AndHeaders() {
+ AtomicReference headerValue = new AtomicReference<>();
+ server.createContext("/ok", exchange -> {
+ headerValue.set(exchange.getRequestHeaders().getFirst("x-apollo-test"));
+ writeResponse(exchange, 200, "{\"value\":\"v1\"}");
+ });
+
+ HttpRequest request = new HttpRequest(url("/ok"));
+ request.setHeaders(Collections.singletonMap("x-apollo-test", "header-value"));
+ HttpResponse response = httpClient.doGet(request, ResponseBody.class);
+
+ assertEquals(200, response.getStatusCode());
+ assertEquals("v1", response.getBody().value);
+ assertEquals("header-value", headerValue.get());
+ }
+
+ @Test
+ public void testDoGetWith304() {
+ server.createContext("/not-modified", exchange -> writeResponse(exchange, 304, ""));
+
+ HttpRequest request = new HttpRequest(url("/not-modified"));
+ HttpResponse response = httpClient.doGet(request, ResponseBody.class);
+
+ assertEquals(304, response.getStatusCode());
+ assertNull(response.getBody());
+ }
+
+ @Test
+ public void testDoGetWithUnexpectedStatusCode() {
+ server.createContext("/error", exchange -> writeResponse(exchange, 500, "internal"));
+
+ HttpRequest request = new HttpRequest(url("/error"));
+
+ try {
+ httpClient.doGet(request, ResponseBody.class);
+ fail("Expected ApolloConfigStatusCodeException");
+ } catch (ApolloConfigStatusCodeException ex) {
+ assertEquals(500, ex.getStatusCode());
+ assertTrue(ex.getMessage().contains("Get operation failed"));
+ }
+ }
+
+ private String url(String path) {
+ return "http://127.0.0.1:" + server.getAddress().getPort() + path;
+ }
+
+ private void writeResponse(HttpExchange exchange, int code, String body) throws IOException {
+ byte[] data = body.getBytes(StandardCharsets.UTF_8);
+ exchange.sendResponseHeaders(code, data.length);
+ try (OutputStream outputStream = exchange.getResponseBody()) {
+ outputStream.write(data);
+ }
+ }
+
+ private static class ResponseBody {
+ private String value;
+ }
+}
+
diff --git a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderInitializerTest.java b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderInitializerTest.java
new file mode 100644
index 00000000..bc4c0be3
--- /dev/null
+++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderInitializerTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.config.data.importer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.ctrip.framework.apollo.config.data.injector.ApolloConfigDataInjectorCustomizer;
+import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
+import java.lang.reflect.Field;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
+import org.springframework.boot.logging.DeferredLogFactory;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.core.env.PropertySource;
+
+/**
+ * @author vdisk
+ */
+public class ApolloConfigDataLoaderInitializerTest {
+
+ private final DeferredLogFactory logFactory = destination -> destination.get();
+
+ @Before
+ public void setUp() throws Exception {
+ resetInitializedFlag();
+ clearInjectorCustomizerCaches();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ resetInitializedFlag();
+ clearInjectorCustomizerCaches();
+ }
+
+ @Test
+ public void testInitApolloClientOnlyOnce() throws Exception {
+ Object bootstrapContext = newDefaultBootstrapContext();
+ Binder binder = new Binder(new MapConfigurationPropertySource(new LinkedHashMap<>()));
+ ApolloConfigDataLoaderInitializer initializer =
+ new ApolloConfigDataLoaderInitializer(logFactory, binder, null, bootstrapContext);
+
+ List> firstPropertySources = initializer.initApolloClient();
+ List> secondPropertySources = initializer.initApolloClient();
+
+ assertEquals(2, firstPropertySources.size());
+ assertTrue(secondPropertySources.isEmpty());
+ }
+
+ @Test
+ public void testForceDisableBootstrapWhenBootstrapEnabledInConfigDataMode() throws Exception {
+ Object bootstrapContext = newDefaultBootstrapContext();
+ Map properties = new LinkedHashMap<>();
+ properties.put(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "true");
+ Binder binder = new Binder(new MapConfigurationPropertySource(properties));
+ ApolloConfigDataLoaderInitializer initializer =
+ new ApolloConfigDataLoaderInitializer(logFactory, binder, null, bootstrapContext);
+
+ List> propertySources = initializer.initApolloClient();
+
+ assertEquals(2, propertySources.size());
+ assertTrue(propertySources.get(1) instanceof MapPropertySource);
+ MapPropertySource mapPropertySource = (MapPropertySource) propertySources.get(1);
+ assertEquals("false",
+ mapPropertySource.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED));
+ assertEquals("false",
+ mapPropertySource.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED));
+ }
+
+ private void resetInitializedFlag() throws Exception {
+ Field initializedField = ApolloConfigDataLoaderInitializer.class.getDeclaredField("INITIALIZED");
+ initializedField.setAccessible(true);
+ initializedField.setBoolean(null, false);
+ }
+
+ private void clearInjectorCustomizerCaches() throws Exception {
+ Field instanceSuppliers =
+ ApolloConfigDataInjectorCustomizer.class.getDeclaredField("INSTANCE_SUPPLIERS");
+ instanceSuppliers.setAccessible(true);
+ ((Map, ?>) instanceSuppliers.get(null)).clear();
+
+ Field instances = ApolloConfigDataInjectorCustomizer.class.getDeclaredField("INSTANCES");
+ instances.setAccessible(true);
+ ((Map, ?>) instances.get(null)).clear();
+ }
+
+ private Object newDefaultBootstrapContext() throws Exception {
+ String className = "org.springframework.boot.DefaultBootstrapContext";
+ if (isClassPresent("org.springframework.boot.bootstrap.DefaultBootstrapContext")) {
+ className = "org.springframework.boot.bootstrap.DefaultBootstrapContext";
+ }
+ Class> bootstrapContextClass = Class.forName(className);
+ return bootstrapContextClass.getConstructor().newInstance();
+ }
+
+ private boolean isClassPresent(String className) {
+ try {
+ Class.forName(className);
+ return true;
+ } catch (ClassNotFoundException ex) {
+ return false;
+ }
+ }
+}
diff --git a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderTest.java b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderTest.java
new file mode 100644
index 00000000..354ac722
--- /dev/null
+++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.config.data.importer;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.ctrip.framework.apollo.Config;
+import com.ctrip.framework.apollo.ConfigService;
+import com.ctrip.framework.apollo.build.ApolloInjector;
+import com.ctrip.framework.apollo.config.data.injector.ApolloMockInjectorCustomizer;
+import com.ctrip.framework.apollo.config.data.util.BootstrapRegistryHelper;
+import com.ctrip.framework.apollo.internals.ConfigManager;
+import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants;
+import com.ctrip.framework.apollo.spi.ConfigFactory;
+import com.ctrip.framework.apollo.spi.ConfigFactoryManager;
+import com.ctrip.framework.apollo.spi.ConfigRegistry;
+import com.google.common.collect.Table;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.boot.context.config.ConfigData;
+import org.springframework.boot.context.config.ConfigDataLoaderContext;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
+import org.springframework.boot.logging.DeferredLogFactory;
+import org.springframework.core.env.PropertySource;
+
+/**
+ * @author vdisk
+ */
+public class ApolloConfigDataLoaderTest {
+
+ private final DeferredLogFactory logFactory = destination -> destination.get();
+
+ @Before
+ public void setUp() throws Exception {
+ clearApolloClientCaches();
+ resetInitializer();
+ resetConfigService();
+ ApolloMockInjectorCustomizer.clear();
+ System.setProperty("app.id", "loader-test-app");
+ System.setProperty("env", "local");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ ApolloMockInjectorCustomizer.clear();
+ resetInitializer();
+ resetConfigService();
+ clearApolloClientCaches();
+ System.clearProperty("app.id");
+ System.clearProperty("env");
+ }
+
+ @Test
+ public void testLoadPropertySourceOrderAndInitializerReuse() throws Exception {
+ ApolloMockInjectorCustomizer.register(ConfigFactory.class, this::newConfigFactory);
+
+ Object bootstrapContext = newDefaultBootstrapContext();
+ Binder binder = new Binder(new MapConfigurationPropertySource(new LinkedHashMap<>()));
+ BootstrapRegistryHelper.registerIfAbsent(bootstrapContext, Binder.class, binder);
+ ConfigDataLoaderContext context = newContextWithBootstrapContext(bootstrapContext);
+
+ ApolloConfigDataLoader loader = new ApolloConfigDataLoader(logFactory);
+
+ ConfigData firstConfigData = loader.load(context, new ApolloConfigDataResource("application"));
+ assertEquals(3, firstConfigData.getPropertySources().size());
+ assertEquals("application", firstConfigData.getPropertySources().get(0).getName());
+ assertEquals(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME,
+ firstConfigData.getPropertySources().get(1).getName());
+ assertEquals(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME,
+ firstConfigData.getPropertySources().get(2).getName());
+
+ ConfigData secondConfigData = loader.load(context, new ApolloConfigDataResource("TEST1.apollo"));
+ assertEquals(1, secondConfigData.getPropertySources().size());
+ PropertySource> secondPropertySource = secondConfigData.getPropertySources().get(0);
+ assertEquals("TEST1.apollo", secondPropertySource.getName());
+ assertEquals("v2", secondPropertySource.getProperty("key2"));
+ }
+
+ private ConfigFactory newConfigFactory() {
+ Map configMap = new HashMap<>();
+ configMap.put("application", mockConfig(singletonConfig("key1", "v1")));
+ configMap.put("TEST1.apollo", mockConfig(singletonConfig("key2", "v2")));
+ return new ConfigFactory() {
+ @Override
+ public Config create(String namespace) {
+ return create("loader-test-app", namespace);
+ }
+
+ @Override
+ public Config create(String appId, String namespace) {
+ Config config = configMap.get(namespace);
+ return config != null ? config : mockConfig(new HashMap<>());
+ }
+
+ @Override
+ public com.ctrip.framework.apollo.ConfigFile createConfigFile(String namespace,
+ com.ctrip.framework.apollo.core.enums.ConfigFileFormat configFileFormat) {
+ return null;
+ }
+
+ @Override
+ public com.ctrip.framework.apollo.ConfigFile createConfigFile(String appId, String namespace,
+ com.ctrip.framework.apollo.core.enums.ConfigFileFormat configFileFormat) {
+ return null;
+ }
+ };
+ }
+
+ private Config mockConfig(Map properties) {
+ Config config = mock(Config.class);
+ Set propertyNames = properties.keySet();
+ when(config.getPropertyNames()).thenReturn(propertyNames);
+ when(config.getProperty(anyString(), nullable(String.class))).thenAnswer(invocation -> {
+ String key = invocation.getArgument(0, String.class);
+ String defaultValue = invocation.getArgument(1);
+ return properties.getOrDefault(key, defaultValue);
+ });
+ return config;
+ }
+
+ private Map singletonConfig(String key, String value) {
+ Map map = new HashMap<>();
+ map.put(key, value);
+ return map;
+ }
+
+ private ConfigDataLoaderContext newContextWithBootstrapContext(Object bootstrapContext) {
+ return (ConfigDataLoaderContext) Proxy.newProxyInstance(
+ ConfigDataLoaderContext.class.getClassLoader(),
+ new Class[]{ConfigDataLoaderContext.class},
+ (proxy, method, args) -> {
+ if ("getBootstrapContext".equals(method.getName())) {
+ return bootstrapContext;
+ }
+ throw new UnsupportedOperationException("Unexpected method: " + method.getName());
+ });
+ }
+
+ private void resetInitializer() throws Exception {
+ Field initializedField = ApolloConfigDataLoaderInitializer.class.getDeclaredField("INITIALIZED");
+ initializedField.setAccessible(true);
+ initializedField.setBoolean(null, false);
+ }
+
+ private void resetConfigService() throws Exception {
+ Method resetMethod = ConfigService.class.getDeclaredMethod("reset");
+ resetMethod.setAccessible(true);
+ resetMethod.invoke(null);
+ }
+
+ private void clearApolloClientCaches() throws Exception {
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configs");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configLocks");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFiles");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFileLocks");
+ clearField(ApolloInjector.getInstance(ConfigFactoryManager.class), "m_factories");
+ clearField(ApolloInjector.getInstance(ConfigRegistry.class), "m_instances");
+ }
+
+ @SuppressWarnings("unchecked")
+ private void clearField(Object target, String fieldName) throws Exception {
+ Field field = target.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Object container = field.get(target);
+ if (container instanceof Map) {
+ ((Map, ?>) container).clear();
+ return;
+ }
+ if (container instanceof Table) {
+ ((Table, ?, ?>) container).clear();
+ return;
+ }
+ Method clearMethod = container.getClass().getDeclaredMethod("clear");
+ clearMethod.setAccessible(true);
+ clearMethod.invoke(container);
+ }
+
+ private Object newDefaultBootstrapContext() throws Exception {
+ String className = "org.springframework.boot.DefaultBootstrapContext";
+ if (isClassPresent("org.springframework.boot.bootstrap.DefaultBootstrapContext")) {
+ className = "org.springframework.boot.bootstrap.DefaultBootstrapContext";
+ }
+ Class> bootstrapContextClass = Class.forName(className);
+ return bootstrapContextClass.getConstructor().newInstance();
+ }
+
+ private boolean isClassPresent(String className) {
+ try {
+ Class.forName(className);
+ return true;
+ } catch (ClassNotFoundException ex) {
+ return false;
+ }
+ }
+}
diff --git a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLocationResolverTest.java b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLocationResolverTest.java
new file mode 100644
index 00000000..67e53124
--- /dev/null
+++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLocationResolverTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.config.data.importer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+import org.junit.Test;
+import org.springframework.boot.context.config.ConfigDataLocation;
+import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
+import org.springframework.boot.context.config.Profiles;
+import org.springframework.boot.logging.DeferredLogFactory;
+import org.springframework.core.Ordered;
+
+/**
+ * @author vdisk
+ */
+public class ApolloConfigDataLocationResolverTest {
+
+ private final DeferredLogFactory logFactory = destination -> destination.get();
+
+ @Test
+ public void testResolveDefaultNamespaceWhenLocationWithoutNamespace() {
+ ApolloConfigDataLocationResolver resolver = new ApolloConfigDataLocationResolver(logFactory);
+ ConfigDataLocation location = ConfigDataLocation.of("apollo://");
+ ConfigDataLocationResolverContext context = null;
+ Profiles profiles = null;
+
+ List resources =
+ resolver.resolveProfileSpecific(context, location, profiles);
+
+ assertEquals(1, resources.size());
+ assertEquals("application", resources.get(0).getNamespace());
+ }
+
+ @Test
+ public void testResolveExplicitNamespace() {
+ ApolloConfigDataLocationResolver resolver = new ApolloConfigDataLocationResolver(logFactory);
+ ConfigDataLocation location = ConfigDataLocation.of("apollo://TEST1.apollo");
+ ConfigDataLocationResolverContext context = null;
+ Profiles profiles = null;
+
+ List resources =
+ resolver.resolveProfileSpecific(context, location, profiles);
+
+ assertEquals(1, resources.size());
+ assertEquals("TEST1.apollo", resources.get(0).getNamespace());
+ }
+
+ @Test
+ public void testOrderAndResolvable() {
+ ApolloConfigDataLocationResolver resolver = new ApolloConfigDataLocationResolver(logFactory);
+
+ assertEquals(Ordered.HIGHEST_PRECEDENCE + 100, resolver.getOrder());
+ assertTrue(resolver.isResolvable(null, ConfigDataLocation.of("apollo://application")));
+ }
+}
diff --git a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/integration/ConfigDataIntegrationTest.java b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/integration/ConfigDataIntegrationTest.java
new file mode 100644
index 00000000..69bbc8cb
--- /dev/null
+++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/integration/ConfigDataIntegrationTest.java
@@ -0,0 +1,292 @@
+/*
+ * 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.config.data.integration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.ctrip.framework.apollo.ConfigService;
+import com.ctrip.framework.apollo.build.ApolloInjector;
+import com.ctrip.framework.apollo.core.ConfigConsts;
+import com.ctrip.framework.apollo.config.data.injector.ApolloConfigDataInjectorCustomizer;
+import com.ctrip.framework.apollo.config.data.injector.ApolloMockInjectorCustomizer;
+import com.ctrip.framework.apollo.internals.ConfigManager;
+import com.ctrip.framework.apollo.mockserver.EmbeddedApollo;
+import com.ctrip.framework.apollo.model.ConfigChangeEvent;
+import com.ctrip.framework.apollo.spi.ConfigFactoryManager;
+import com.ctrip.framework.apollo.spi.ConfigRegistry;
+import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
+import com.google.common.collect.Table;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.ClassRule;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.rules.ExternalResource;
+import org.junit.rules.RuleChain;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+/**
+ * @author vdisk
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringBootTest(classes = ConfigDataIntegrationTest.TestConfiguration.class,
+ webEnvironment = SpringBootTest.WebEnvironment.NONE,
+ properties = {
+ "app.id=someAppId",
+ "env=local",
+ "spring.config.import=apollo://application,apollo://TEST1.apollo,apollo://application.yaml",
+ "listeners=application,TEST1.apollo,application.yaml"
+ })
+@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
+public class ConfigDataIntegrationTest {
+
+ private static final String TEST_APP_ID = "someAppId";
+ private static final String TEST_ENV = "local";
+
+ private static final EmbeddedApollo embeddedApollo = new EmbeddedApollo();
+
+ private static final ExternalResource apolloStateResource = new ExternalResource() {
+ private String originalAppId;
+ private String originalEnv;
+
+ @Override
+ protected void before() throws Throwable {
+ originalAppId = System.getProperty("app.id");
+ originalEnv = System.getProperty("env");
+ System.setProperty("app.id", TEST_APP_ID);
+ System.setProperty("env", TEST_ENV);
+ resetApolloStaticState();
+ }
+
+ @Override
+ protected void after() {
+ try {
+ resetApolloStaticState();
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ } finally {
+ restoreOrClear("app.id", originalAppId);
+ restoreOrClear("env", originalEnv);
+ }
+ }
+ };
+
+ @ClassRule
+ public static final RuleChain apolloRuleChain = RuleChain
+ .outerRule(apolloStateResource)
+ .around(embeddedApollo);
+
+ @Before
+ public void beforeEach() {
+ embeddedApollo.resetOverriddenProperties();
+ }
+
+ @After
+ public void afterEach() throws Exception {
+ resetApolloStaticState();
+ }
+
+ @Autowired
+ private Environment environment;
+
+ @Autowired(required = false)
+ private FeatureEnabledBean featureEnabledBean;
+
+ @Autowired
+ private ListenerProbe listenerProbe;
+
+ @Autowired
+ private RedisCacheProperties redisCacheProperties;
+
+ @Test
+ public void testImportMultipleNamespacesAndConditionalOnProperty() {
+ assertEquals("ok", environment.getProperty("application.only"));
+ assertEquals("ok", environment.getProperty("test1.only"));
+ assertEquals("ok", environment.getProperty("yaml.only"));
+ assertEquals("from-yaml", environment.getProperty("priority.value"));
+ assertNotNull(featureEnabledBean);
+ assertTrue(redisCacheProperties.isEnabled());
+ assertEquals(35, redisCacheProperties.getCommandTimeout());
+ }
+
+ @Test
+ public void testApolloConfigChangeListenerWithInterestedKeyPrefixes() throws Exception {
+ assertEquals("35", environment.getProperty("redis.cache.commandTimeout"));
+
+ addOrModifyForAllAppIds("application", "redis.cache.commandTimeout", "45");
+ ConfigChangeEvent interestedEvent = listenerProbe.pollEvent(10, TimeUnit.SECONDS);
+ assertNotNull(interestedEvent);
+ assertTrue(interestedEvent.changedKeys().contains("redis.cache.commandTimeout"));
+ assertEquals(45, ConfigService.getConfig("application")
+ .getIntProperty("redis.cache.commandTimeout", -1).intValue());
+
+ addOrModifyForAllAppIds("application.yaml", ConfigConsts.CONFIG_FILE_CONTENT_KEY,
+ "priority:\n value: from-yaml\nyaml:\n only: ok\nredis:\n cache:\n commandTimeout: 55\n");
+ ConfigChangeEvent yamlInterestedEvent = listenerProbe.pollEvent(10, TimeUnit.SECONDS);
+ assertNotNull(yamlInterestedEvent);
+ assertTrue(yamlInterestedEvent.changedKeys().contains("redis.cache.commandTimeout"));
+ assertEquals("55", environment.getProperty("redis.cache.commandTimeout"));
+
+ addOrModifyForAllAppIds("application", "apollo.unrelated.key", "value");
+ ConfigChangeEvent unrelatedEvent = listenerProbe.pollEvent(3000, TimeUnit.MILLISECONDS);
+ assertNull(unrelatedEvent);
+ }
+
+ @EnableAutoConfiguration
+ @EnableConfigurationProperties(RedisCacheProperties.class)
+ @Configuration
+ static class TestConfiguration {
+
+ @Bean
+ @ConditionalOnProperty(value = "feature.enabled", havingValue = "true")
+ public FeatureEnabledBean featureEnabledBean() {
+ return new FeatureEnabledBean();
+ }
+
+ @Bean
+ public ListenerProbe listenerProbe() {
+ return new ListenerProbe();
+ }
+ }
+
+ static class FeatureEnabledBean {
+ }
+
+ @ConfigurationProperties(prefix = "redis.cache")
+ static class RedisCacheProperties {
+
+ private boolean enabled;
+ private int commandTimeout;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public int getCommandTimeout() {
+ return commandTimeout;
+ }
+
+ public void setCommandTimeout(int commandTimeout) {
+ this.commandTimeout = commandTimeout;
+ }
+ }
+
+ static class ListenerProbe {
+
+ private final BlockingQueue queue = new ArrayBlockingQueue<>(10);
+
+ @ApolloConfigChangeListener(value = "${listeners}",
+ interestedKeyPrefixes = {"redis.cache."})
+ private void onChange(ConfigChangeEvent changeEvent) {
+ queue.offer(changeEvent);
+ }
+
+ ConfigChangeEvent pollEvent(long timeout, TimeUnit unit) throws InterruptedException {
+ return queue.poll(timeout, unit);
+ }
+ }
+
+ private static void resetApolloStaticState() throws Exception {
+ ApolloMockInjectorCustomizer.clear();
+
+ Field instanceSuppliers =
+ ApolloConfigDataInjectorCustomizer.class.getDeclaredField("INSTANCE_SUPPLIERS");
+ instanceSuppliers.setAccessible(true);
+ ((Map, ?>) instanceSuppliers.get(null)).clear();
+
+ Field instances = ApolloConfigDataInjectorCustomizer.class.getDeclaredField("INSTANCES");
+ instances.setAccessible(true);
+ ((Map, ?>) instances.get(null)).clear();
+
+ Class> initializerClass = Class.forName(
+ "com.ctrip.framework.apollo.config.data.importer.ApolloConfigDataLoaderInitializer");
+ Field initialized = initializerClass.getDeclaredField("INITIALIZED");
+ initialized.setAccessible(true);
+ initialized.setBoolean(null, false);
+
+ Method resetMethod = ConfigService.class.getDeclaredMethod("reset");
+ resetMethod.setAccessible(true);
+ resetMethod.invoke(null);
+ clearApolloClientCaches();
+ }
+
+ private static void clearApolloClientCaches() throws Exception {
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configs");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configLocks");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFiles");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFileLocks");
+ clearField(ApolloInjector.getInstance(ConfigFactoryManager.class), "m_factories");
+ clearField(ApolloInjector.getInstance(ConfigRegistry.class), "m_instances");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void clearField(Object target, String fieldName) throws Exception {
+ Field field = target.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Object container = field.get(target);
+ if (container instanceof Map) {
+ ((Map, ?>) container).clear();
+ return;
+ }
+ if (container instanceof Table) {
+ ((Table, ?, ?>) container).clear();
+ return;
+ }
+ Method clearMethod = container.getClass().getDeclaredMethod("clear");
+ clearMethod.setAccessible(true);
+ clearMethod.invoke(container);
+ }
+
+ private static void restoreOrClear(String key, String originalValue) {
+ if (originalValue == null) {
+ System.clearProperty(key);
+ return;
+ }
+ System.setProperty(key, originalValue);
+ }
+
+ private static void addOrModifyForAllAppIds(String namespace, String key, String value) {
+ embeddedApollo.addOrModifyProperty(TEST_APP_ID, namespace, key, value);
+ embeddedApollo.addOrModifyProperty(
+ ConfigConsts.NO_APPID_PLACEHOLDER, namespace, key, value);
+ }
+
+}
diff --git a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/listener/ApolloDeferredLoggerApplicationListenerTest.java b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/listener/ApolloDeferredLoggerApplicationListenerTest.java
new file mode 100644
index 00000000..60aadece
--- /dev/null
+++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/listener/ApolloDeferredLoggerApplicationListenerTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.config.data.listener;
+
+import static org.mockito.Mockito.mock;
+
+import com.ctrip.framework.apollo.core.utils.DeferredLogger;
+import org.junit.After;
+import org.junit.Test;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.context.event.ApplicationContextInitializedEvent;
+import org.springframework.boot.context.event.ApplicationFailedEvent;
+import org.springframework.context.ConfigurableApplicationContext;
+
+/**
+ * @author vdisk
+ */
+public class ApolloDeferredLoggerApplicationListenerTest {
+
+ @After
+ public void tearDown() {
+ DeferredLogger.disable();
+ }
+
+ @Test
+ public void testReplayDeferredLogsOnApplicationContextInitialized() {
+ DeferredLogger.enable();
+ ApolloDeferredLoggerApplicationListener listener =
+ new ApolloDeferredLoggerApplicationListener();
+ ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class);
+ ApplicationContextInitializedEvent event =
+ new ApplicationContextInitializedEvent(new SpringApplication(Object.class), new String[0],
+ context);
+
+ listener.onApplicationEvent(event);
+ }
+
+ @Test
+ public void testReplayDeferredLogsOnApplicationFailed() {
+ DeferredLogger.enable();
+ ApolloDeferredLoggerApplicationListener listener =
+ new ApolloDeferredLoggerApplicationListener();
+ ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class);
+ ApplicationFailedEvent event =
+ new ApplicationFailedEvent(new SpringApplication(Object.class), new String[0], context,
+ new IllegalStateException("test"));
+
+ listener.onApplicationEvent(event);
+ }
+}
diff --git a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/listener/ApolloSpringApplicationRegisterListenerTest.java b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/listener/ApolloSpringApplicationRegisterListenerTest.java
new file mode 100644
index 00000000..9823be54
--- /dev/null
+++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/listener/ApolloSpringApplicationRegisterListenerTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.config.data.listener;
+
+import static org.junit.Assert.assertSame;
+
+import com.ctrip.framework.apollo.config.data.util.BootstrapRegistryHelper;
+import java.lang.reflect.Constructor;
+import org.junit.Test;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.context.event.ApplicationStartingEvent;
+import org.springframework.util.ClassUtils;
+
+/**
+ * @author vdisk
+ */
+public class ApolloSpringApplicationRegisterListenerTest {
+
+ @Test
+ public void testRegisterSpringApplicationToBootstrapContext() throws Exception {
+ Object bootstrapContext = newDefaultBootstrapContext();
+ SpringApplication springApplication = new SpringApplication(Object.class);
+
+ ApplicationStartingEvent event = newApplicationStartingEvent(bootstrapContext, springApplication);
+
+ ApolloSpringApplicationRegisterListener listener = new ApolloSpringApplicationRegisterListener();
+ listener.onApplicationEvent(event);
+
+ SpringApplication registered = BootstrapRegistryHelper.get(bootstrapContext, SpringApplication.class);
+ assertSame(springApplication, registered);
+ }
+
+ private ApplicationStartingEvent newApplicationStartingEvent(
+ Object bootstrapContext, SpringApplication springApplication) throws Exception {
+ for (Constructor> constructor : ApplicationStartingEvent.class.getConstructors()) {
+ Class>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes.length == 3 && SpringApplication.class.isAssignableFrom(parameterTypes[1])) {
+ return (ApplicationStartingEvent) constructor
+ .newInstance(bootstrapContext, springApplication, new String[0]);
+ }
+ }
+ throw new IllegalStateException("Unsupported ApplicationStartingEvent constructor signature");
+ }
+
+ private Object newDefaultBootstrapContext() throws Exception {
+ String className = "org.springframework.boot.DefaultBootstrapContext";
+ if (ClassUtils.isPresent("org.springframework.boot.bootstrap.DefaultBootstrapContext",
+ getClass().getClassLoader())) {
+ className = "org.springframework.boot.bootstrap.DefaultBootstrapContext";
+ }
+ return Class.forName(className).getConstructor().newInstance();
+ }
+}
diff --git a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/util/BootstrapRegistryHelperCompatibilityTest.java b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/util/BootstrapRegistryHelperCompatibilityTest.java
new file mode 100644
index 00000000..1c9c68ab
--- /dev/null
+++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/util/BootstrapRegistryHelperCompatibilityTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.config.data.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Proxy;
+import org.junit.Test;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.context.config.ConfigDataLoaderContext;
+import org.springframework.boot.context.event.ApplicationStartingEvent;
+import org.springframework.util.ClassUtils;
+
+/**
+ * @author vdisk
+ */
+public class BootstrapRegistryHelperCompatibilityTest {
+
+ @Test
+ public void testRegisterAndGetFromBootstrapContext() throws Exception {
+ Object bootstrapContext = newDefaultBootstrapContext();
+
+ BootstrapRegistryHelper.registerIfAbsent(bootstrapContext, String.class, "apollo");
+ BootstrapRegistryHelper.registerIfAbsentFromSupplier(bootstrapContext, Integer.class, () -> 100);
+
+ assertEquals("apollo", BootstrapRegistryHelper.get(bootstrapContext, String.class));
+ assertEquals(Integer.valueOf(100), BootstrapRegistryHelper.get(bootstrapContext, Integer.class));
+ assertEquals(Boolean.TRUE, BootstrapRegistryHelper.getOrElse(bootstrapContext, Boolean.class,
+ Boolean.TRUE));
+ }
+
+ @Test
+ public void testGetBootstrapContextFromEventAndLoaderContext() throws Exception {
+ Object bootstrapContext = newDefaultBootstrapContext();
+ ApplicationStartingEvent event =
+ newApplicationStartingEvent(bootstrapContext, new SpringApplication(Object.class));
+
+ Object eventBootstrapContext = BootstrapRegistryHelper.getBootstrapContext(event);
+ assertSame(bootstrapContext, eventBootstrapContext);
+
+ ConfigDataLoaderContext loaderContext = (ConfigDataLoaderContext) Proxy.newProxyInstance(
+ ConfigDataLoaderContext.class.getClassLoader(),
+ new Class[]{ConfigDataLoaderContext.class},
+ (proxy, method, args) -> {
+ if ("getBootstrapContext".equals(method.getName())) {
+ return bootstrapContext;
+ }
+ throw new UnsupportedOperationException("Unexpected method: " + method.getName());
+ });
+ Object loaderBootstrapContext = BootstrapRegistryHelper.getBootstrapContext(loaderContext);
+ assertSame(bootstrapContext, loaderBootstrapContext);
+ }
+
+ @Test
+ public void testSpringBoot4PresenceDetection() {
+ boolean expected = ClassUtils
+ .isPresent("org.springframework.boot.bootstrap.ConfigurableBootstrapContext",
+ BootstrapRegistryHelperCompatibilityTest.class.getClassLoader());
+ assertEquals(expected, BootstrapRegistryHelper.isSpringBoot4Present());
+ }
+
+ private ApplicationStartingEvent newApplicationStartingEvent(
+ Object bootstrapContext, SpringApplication springApplication) throws Exception {
+ for (Constructor> constructor : ApplicationStartingEvent.class.getConstructors()) {
+ Class>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes.length == 3 && SpringApplication.class.isAssignableFrom(parameterTypes[1])) {
+ return (ApplicationStartingEvent) constructor
+ .newInstance(bootstrapContext, springApplication, new String[0]);
+ }
+ }
+ throw new IllegalStateException("Unsupported ApplicationStartingEvent constructor signature");
+ }
+
+ private Object newDefaultBootstrapContext() throws Exception {
+ String className = "org.springframework.boot.DefaultBootstrapContext";
+ if (ClassUtils.isPresent("org.springframework.boot.bootstrap.DefaultBootstrapContext",
+ getClass().getClassLoader())) {
+ className = "org.springframework.boot.bootstrap.DefaultBootstrapContext";
+ }
+ return Class.forName(className).getConstructor().newInstance();
+ }
+}
diff --git a/apollo-client-config-data/src/test/resources/mockdata-TEST1.apollo.properties b/apollo-client-config-data/src/test/resources/mockdata-TEST1.apollo.properties
new file mode 100644
index 00000000..5a235fc1
--- /dev/null
+++ b/apollo-client-config-data/src/test/resources/mockdata-TEST1.apollo.properties
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+priority.value=from-test1
+test1.only=ok
diff --git a/apollo-client-config-data/src/test/resources/mockdata-application.properties b/apollo-client-config-data/src/test/resources/mockdata-application.properties
new file mode 100644
index 00000000..8f3a4cca
--- /dev/null
+++ b/apollo-client-config-data/src/test/resources/mockdata-application.properties
@@ -0,0 +1,21 @@
+#
+# 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.
+#
+application.only=ok
+feature.enabled=true
+listeners=application,TEST1.apollo,application.yaml
+priority.value=from-application
+redis.cache.enabled=true
+redis.cache.commandTimeout=30
diff --git a/apollo-client-config-data/src/test/resources/mockdata-application.yaml.properties b/apollo-client-config-data/src/test/resources/mockdata-application.yaml.properties
new file mode 100644
index 00000000..f1073f69
--- /dev/null
+++ b/apollo-client-config-data/src/test/resources/mockdata-application.yaml.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+content=priority:\n value: from-yaml\nyaml:\n only: ok\nredis:\n cache:\n commandTimeout: 35\n
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/annotation/ApolloAnnotationProcessor.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/annotation/ApolloAnnotationProcessor.java
index e5d8685b..1247fd26 100644
--- a/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/annotation/ApolloAnnotationProcessor.java
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/spring/annotation/ApolloAnnotationProcessor.java
@@ -108,7 +108,7 @@ private void processApolloConfig(Object bean, Field field) {
final String appId = StringUtils.defaultIfBlank(annotation.appId(), configUtil.getAppId());
final String namespace = annotation.value();
- final String resolvedAppId = this.environment.resolveRequiredPlaceholders(appId);
+ final String resolvedAppId = resolveAppId(appId);
final String resolvedNamespace = this.environment.resolveRequiredPlaceholders(namespace);
Config config = ConfigService.getConfig(resolvedAppId, resolvedNamespace);
@@ -132,6 +132,7 @@ private void processApolloConfigChangeListener(final Object bean, final Method m
ReflectionUtils.makeAccessible(method);
String appId = StringUtils.defaultIfBlank(annotation.appId(), configUtil.getAppId());
+ String resolvedAppId = resolveAppId(appId);
String[] namespaces = annotation.value();
String[] annotatedInterestedKeys = annotation.interestedKeys();
String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
@@ -146,7 +147,7 @@ private void processApolloConfigChangeListener(final Object bean, final Method m
Set resolvedNamespaces = processResolveNamespaceValue(namespaces);
for (String namespace : resolvedNamespaces) {
- Config config = ConfigService.getConfig(appId, namespace);
+ Config config = ConfigService.getConfig(resolvedAppId, namespace);
if (interestedKeys == null && interestedKeyPrefixes == null) {
config.addChangeListener(configChangeListener);
@@ -270,6 +271,13 @@ private Gson buildGson(String datePattern) {
return new GsonBuilder().setDateFormat(datePattern).create();
}
+ private String resolveAppId(String appId) {
+ if (appId == null) {
+ return null;
+ }
+ return this.environment.resolveRequiredPlaceholders(appId);
+ }
+
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.configurableBeanFactory = (ConfigurableBeanFactory) beanFactory;
diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/BaseIntegrationTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/BaseIntegrationTest.java
index 04fa4d31..98364f7f 100644
--- a/apollo-client/src/test/java/com/ctrip/framework/apollo/BaseIntegrationTest.java
+++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/BaseIntegrationTest.java
@@ -103,6 +103,16 @@ public void mockConfigs(
);
}
+ public void mockConfigs(
+ String appId,
+ String cluster,
+ String namespace,
+ int mockedStatusCode,
+ ApolloConfig apolloConfig
+ ) {
+ this.mockedConfigService.mockConfigs(appId, cluster, namespace, mockedStatusCode, apolloConfig);
+ }
+
@BeforeEach
public void setUp() throws Exception {
someAppId = "1003171";
diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/MockedConfigService.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/MockedConfigService.java
index 5ae9b33f..3b196095 100644
--- a/apollo-client/src/test/java/com/ctrip/framework/apollo/MockedConfigService.java
+++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/MockedConfigService.java
@@ -22,6 +22,7 @@
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import java.util.List;
+import java.util.regex.Pattern;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletResponse;
import org.mockserver.integration.ClientAndServer;
@@ -156,9 +157,42 @@ public void mockConfigs(
boolean failedAtFirstTime,
int mockedStatusCode,
ApolloConfig apolloConfig
+ ) {
+ mockConfigs(failedAtFirstTime, mockedStatusCode, apolloConfig, "/configs/.*");
+ }
+
+ public void mockConfigs(
+ String appId,
+ String cluster,
+ String namespace,
+ int mockedStatusCode,
+ ApolloConfig apolloConfig
+ ) {
+ mockConfigs(false, appId, cluster, namespace, mockedStatusCode, apolloConfig);
+ }
+
+ public void mockConfigs(
+ boolean failedAtFirstTime,
+ String appId,
+ String cluster,
+ String namespace,
+ int mockedStatusCode,
+ ApolloConfig apolloConfig
+ ) {
+ String path = String.format("/configs/%s/%s/%s.*",
+ Pattern.quote(appId),
+ Pattern.quote(cluster),
+ Pattern.quote(namespace));
+ mockConfigs(failedAtFirstTime, mockedStatusCode, apolloConfig, path);
+ }
+
+ private void mockConfigs(
+ boolean failedAtFirstTime,
+ int mockedStatusCode,
+ ApolloConfig apolloConfig,
+ String path
) {
// cannot use /configs/* as the path, because mock server will treat * as a wildcard
- final String path = "/configs/.*";
RequestDefinition requestDefinition = HttpRequest.request("GET").withPath(path);
// need clear
@@ -222,7 +256,7 @@ public void mockLongPollNotifications(
}
@Override
- public void close() throws Exception {
+ public void close() {
if (this.server.isRunning()) {
this.server.stop();
}
diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/integration/ConfigIntegrationTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/integration/ConfigIntegrationTest.java
index cc6aeced..5c6a5bdd 100644
--- a/apollo-client/src/test/java/com/ctrip/framework/apollo/integration/ConfigIntegrationTest.java
+++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/integration/ConfigIntegrationTest.java
@@ -17,29 +17,35 @@
package com.ctrip.framework.apollo.integration;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import com.ctrip.framework.apollo.MockedConfigService;
-import com.ctrip.framework.apollo.util.OrderedProperties;
-import java.util.*;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import javax.servlet.http.HttpServletResponse;
-
-import org.junit.jupiter.api.Test;
-
import com.ctrip.framework.apollo.BaseIntegrationTest;
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigChangeListener;
+import com.ctrip.framework.apollo.ConfigFile;
import com.ctrip.framework.apollo.ConfigService;
+import com.ctrip.framework.apollo.MockedConfigService;
+import com.ctrip.framework.apollo.PropertiesCompatibleConfigFile;
+import com.ctrip.framework.apollo.core.ConfigConsts;
import com.ctrip.framework.apollo.core.dto.ApolloConfig;
import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
+import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
+import com.ctrip.framework.apollo.enums.PropertyChangeType;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
+import com.ctrip.framework.apollo.model.ConfigFileChangeEvent;
+import com.ctrip.framework.apollo.util.OrderedProperties;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.SettableFuture;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.jupiter.api.Test;
/**
* @author Jason Song(song_s@ctrip.com)
@@ -49,6 +55,11 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
private final String someReleaseKey = "1";
private final String someOtherNamespace = "someOtherNamespace";
+ private static final String FALLBACK_ANOTHER_APP_ID = "100004459";
+ private static final String MULTI_APP_ANOTHER_APP_ID = "200000001";
+ private static final String PUBLIC_NAMESPACE = "TEST1.apollo";
+ private static final String DEFAULT_VALUE = "undefined";
+ private static final String MULTI_APP_KEY = "someKey";
@Test
public void testGetConfigWithNoLocalFileButWithRemoteConfig() throws Exception {
@@ -66,6 +77,181 @@ public void testGetConfigWithNoLocalFileButWithRemoteConfig() throws Exception {
assertEquals(someDefaultValue, config.getProperty(someNonExistedKey, someDefaultValue));
}
+ @Test
+ public void testFallbackOrderAcrossNamespaces() {
+ newMockedConfigService();
+
+ mockConfigs(someAppId, someClusterName, defaultNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(someAppId, defaultNamespace,
+ ImmutableMap.of("key.from.default", "value-default")));
+ mockConfigs(FALLBACK_ANOTHER_APP_ID, someClusterName, defaultNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(FALLBACK_ANOTHER_APP_ID, defaultNamespace,
+ ImmutableMap.of("key.from.another", "value-another")));
+ mockConfigs(someAppId, someClusterName, PUBLIC_NAMESPACE, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(someAppId, PUBLIC_NAMESPACE,
+ ImmutableMap.of("key.from.public", "value-public")));
+
+ Config appConfig = ConfigService.getAppConfig();
+ Config anotherAppConfig = ConfigService.getConfig(FALLBACK_ANOTHER_APP_ID, defaultNamespace);
+ Config publicConfig = ConfigService.getConfig(PUBLIC_NAMESPACE);
+
+ assertEquals("value-default",
+ resolveValueByFallbackOrder("key.from.default", appConfig, anotherAppConfig, publicConfig));
+ assertEquals("value-another",
+ resolveValueByFallbackOrder("key.from.another", appConfig, anotherAppConfig, publicConfig));
+ assertEquals("value-public",
+ resolveValueByFallbackOrder("key.from.public", appConfig, anotherAppConfig, publicConfig));
+ assertEquals(DEFAULT_VALUE,
+ resolveValueByFallbackOrder("key.unknown", appConfig, anotherAppConfig, publicConfig));
+ }
+
+ @Test
+ public void testGetConfigWithSameNamespaceButDifferentAppIds() {
+ newMockedConfigService();
+
+ mockConfigs(someAppId, someClusterName, defaultNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(someAppId, defaultNamespace,
+ ImmutableMap.of(MULTI_APP_KEY, "value-from-default-app")));
+ mockConfigs(MULTI_APP_ANOTHER_APP_ID, someClusterName, defaultNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(MULTI_APP_ANOTHER_APP_ID, defaultNamespace,
+ ImmutableMap.of(MULTI_APP_KEY, "value-from-another-app")));
+
+ Config defaultAppConfig = ConfigService.getConfig(someAppId, defaultNamespace);
+ Config anotherAppConfig = ConfigService.getConfig(MULTI_APP_ANOTHER_APP_ID, defaultNamespace);
+
+ assertEquals("value-from-default-app", defaultAppConfig.getProperty(MULTI_APP_KEY, null));
+ assertEquals("value-from-another-app", anotherAppConfig.getProperty(MULTI_APP_KEY, null));
+ }
+
+ @Test
+ public void testConfigChangeShouldOnlyAffectSpecifiedAppId() throws Exception {
+ MockedConfigService mockedConfigService = newMockedConfigService();
+
+ mockConfigs(someAppId, someClusterName, defaultNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(someAppId, defaultNamespace,
+ ImmutableMap.of(MULTI_APP_KEY, "default-v1")));
+ mockConfigs(MULTI_APP_ANOTHER_APP_ID, someClusterName, defaultNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(MULTI_APP_ANOTHER_APP_ID, defaultNamespace,
+ ImmutableMap.of(MULTI_APP_KEY, "another-v1")));
+ mockedConfigService.mockLongPollNotifications(50, HttpServletResponse.SC_OK,
+ Lists.newArrayList(
+ new ApolloConfigNotification(defaultNamespace, 1L)));
+
+ Config defaultAppConfig = ConfigService.getConfig(someAppId, defaultNamespace);
+ Config anotherAppConfig = ConfigService.getConfig(MULTI_APP_ANOTHER_APP_ID, defaultNamespace);
+
+ assertEquals("default-v1", defaultAppConfig.getProperty(MULTI_APP_KEY, null));
+ assertEquals("another-v1", anotherAppConfig.getProperty(MULTI_APP_KEY, null));
+
+ SettableFuture defaultAppFuture = SettableFuture.create();
+ SettableFuture anotherAppFuture = SettableFuture.create();
+
+ defaultAppConfig.addChangeListener(futureListener(defaultAppFuture));
+ anotherAppConfig.addChangeListener(futureListener(anotherAppFuture));
+
+ mockConfigs(MULTI_APP_ANOTHER_APP_ID, someClusterName, defaultNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(MULTI_APP_ANOTHER_APP_ID, defaultNamespace,
+ ImmutableMap.of(MULTI_APP_KEY, "another-v2")));
+
+ mockedConfigService.mockLongPollNotifications(50, HttpServletResponse.SC_OK,
+ Lists.newArrayList(
+ new ApolloConfigNotification(defaultNamespace, 2L)));
+
+ ConfigChangeEvent anotherAppChangeEvent = anotherAppFuture.get(5, TimeUnit.SECONDS);
+ assertNotNull(anotherAppChangeEvent);
+ assertEquals("another-v1", anotherAppChangeEvent.getChange(MULTI_APP_KEY).getOldValue());
+ assertEquals("another-v2", anotherAppChangeEvent.getChange(MULTI_APP_KEY).getNewValue());
+
+ assertEquals("default-v1", defaultAppConfig.getProperty(MULTI_APP_KEY, null));
+ assertEquals("another-v2", anotherAppConfig.getProperty(MULTI_APP_KEY, null));
+ assertNull(pollFuture(defaultAppFuture, 1000));
+ }
+
+ @Test
+ public void testConfigFileWithPropertiesXmlAndYamlFormats() throws Exception {
+ MockedConfigService mockedConfigService = newMockedConfigService();
+
+ String propertiesNamespace = "application.properties";
+ String xmlNamespace = "datasources.xml";
+ String yamlNamespace = "application.yaml";
+
+ mockConfigs(someAppId, someClusterName, propertiesNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(someAppId, propertiesNamespace,
+ ImmutableMap.of("timeout", "200", "batch", "100")));
+ mockConfigs(someAppId, someClusterName, xmlNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(someAppId, xmlNamespace,
+ ImmutableMap.of(ConfigConsts.CONFIG_FILE_CONTENT_KEY,
+ "db-v1")));
+ mockConfigs(someAppId, someClusterName, yamlNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(someAppId, yamlNamespace,
+ ImmutableMap.of(ConfigConsts.CONFIG_FILE_CONTENT_KEY,
+ "redis:\n cache:\n enabled: true\n commandTimeout: 30\n")));
+ mockedConfigService.mockLongPollNotifications(50, HttpServletResponse.SC_OK,
+ Lists.newArrayList(
+ new ApolloConfigNotification(propertiesNamespace, 1L),
+ new ApolloConfigNotification(xmlNamespace, 1L),
+ new ApolloConfigNotification(yamlNamespace, 1L)));
+
+ ConfigFile propertiesFile = ConfigService.getConfigFile("application", ConfigFileFormat.Properties);
+ ConfigFile xmlFile = ConfigService.getConfigFile("datasources", ConfigFileFormat.XML);
+ ConfigFile yamlFile = ConfigService.getConfigFile("application", ConfigFileFormat.YAML);
+
+ assertTrue(propertiesFile instanceof PropertiesCompatibleConfigFile);
+ Properties properties = ((PropertiesCompatibleConfigFile) propertiesFile).asProperties();
+ assertEquals("200", properties.getProperty("timeout"));
+ assertEquals("100", properties.getProperty("batch"));
+
+ assertTrue(xmlFile.hasContent());
+ assertEquals("db-v1", xmlFile.getContent());
+
+ assertTrue(yamlFile instanceof PropertiesCompatibleConfigFile);
+ Properties yamlProperties = ((PropertiesCompatibleConfigFile) yamlFile).asProperties();
+ assertEquals("true", yamlProperties.getProperty("redis.cache.enabled"));
+ assertEquals("30", yamlProperties.getProperty("redis.cache.commandTimeout"));
+
+ SettableFuture xmlChangeFuture = SettableFuture.create();
+ SettableFuture yamlChangeFuture = SettableFuture.create();
+
+ xmlFile.addChangeListener(changeEvent -> {
+ if (!xmlChangeFuture.isDone()) {
+ xmlChangeFuture.set(changeEvent);
+ }
+ });
+ yamlFile.addChangeListener(changeEvent -> {
+ if (!yamlChangeFuture.isDone()) {
+ yamlChangeFuture.set(changeEvent);
+ }
+ });
+
+ mockConfigs(someAppId, someClusterName, xmlNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(someAppId, xmlNamespace,
+ ImmutableMap.of(ConfigConsts.CONFIG_FILE_CONTENT_KEY,
+ "db-v2")));
+ mockConfigs(someAppId, someClusterName, yamlNamespace, HttpServletResponse.SC_OK,
+ assembleApolloConfigForApp(someAppId, yamlNamespace,
+ ImmutableMap.of(ConfigConsts.CONFIG_FILE_CONTENT_KEY,
+ "redis:\n cache:\n enabled: false\n commandTimeout: 45\n")));
+
+ mockedConfigService.mockLongPollNotifications(50, HttpServletResponse.SC_OK,
+ Lists.newArrayList(
+ new ApolloConfigNotification(xmlNamespace, 2L),
+ new ApolloConfigNotification(yamlNamespace, 2L)));
+
+ ConfigFileChangeEvent xmlChange = xmlChangeFuture.get(5, TimeUnit.SECONDS);
+ ConfigFileChangeEvent yamlChange = yamlChangeFuture.get(5, TimeUnit.SECONDS);
+
+ assertEquals("datasources.xml", xmlChange.getNamespace());
+ assertEquals(PropertyChangeType.MODIFIED, xmlChange.getChangeType());
+ assertEquals("db-v2", xmlFile.getContent());
+
+ assertEquals("application.yaml", yamlChange.getNamespace());
+ assertEquals(PropertyChangeType.MODIFIED, yamlChange.getChangeType());
+ Properties yamlPropertiesAfterRefresh = ((PropertiesCompatibleConfigFile) yamlFile)
+ .asProperties();
+ assertEquals("false", yamlPropertiesAfterRefresh.getProperty("redis.cache.enabled"));
+ assertEquals("45", yamlPropertiesAfterRefresh.getProperty("redis.cache.commandTimeout"));
+ }
+
@Test
public void testOrderGetConfigWithNoLocalFileButWithRemoteConfig() throws Exception {
setPropertiesOrderEnabled(true);
@@ -437,12 +623,47 @@ public void onChange(ConfigChangeEvent changeEvent) {
}
private ApolloConfig assembleApolloConfig(Map configurations) {
- ApolloConfig apolloConfig =
- new ApolloConfig(someAppId, someClusterName, defaultNamespace, someReleaseKey);
+ return assembleApolloConfigForApp(someAppId, defaultNamespace, configurations);
+ }
+ private ApolloConfig assembleApolloConfigForApp(
+ String appId, String namespace, Map configurations) {
+ ApolloConfig apolloConfig =
+ new ApolloConfig(appId, someClusterName, namespace, someReleaseKey);
apolloConfig.setConfigurations(configurations);
-
return apolloConfig;
}
+ private String resolveValueByFallbackOrder(
+ String key,
+ Config appConfig,
+ Config anotherAppConfig,
+ Config publicConfig) {
+ String value = appConfig.getProperty(key, DEFAULT_VALUE);
+ if (DEFAULT_VALUE.equals(value)) {
+ value = anotherAppConfig.getProperty(key, DEFAULT_VALUE);
+ }
+ if (DEFAULT_VALUE.equals(value)) {
+ value = publicConfig.getProperty(key, DEFAULT_VALUE);
+ }
+ return value;
+ }
+
+ private ConfigChangeListener futureListener(SettableFuture future) {
+ return changeEvent -> {
+ if (!future.isDone()) {
+ future.set(changeEvent);
+ }
+ };
+ }
+
+ private ConfigChangeEvent pollFuture(SettableFuture future, long timeoutInMs)
+ throws Exception {
+ try {
+ return future.get(timeoutInMs, TimeUnit.MILLISECONDS);
+ } catch (TimeoutException ignore) {
+ return null;
+ }
+ }
+
}
diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigAnnotationTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigAnnotationTest.java
index 146a3e98..c70dfbc6 100644
--- a/apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigAnnotationTest.java
+++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigAnnotationTest.java
@@ -58,6 +58,8 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anySet;
@@ -77,6 +79,7 @@
public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
private static final String FX_APOLLO_NAMESPACE = "FX.apollo";
private static final String APPLICATION_YAML_NAMESPACE = "application.yaml";
+ private static final String ANOTHER_APP_ID = "someAppId2";
private static T getBean(Class beanClass, Class>... annotatedClasses) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(annotatedClasses);
@@ -98,6 +101,8 @@ public void tearDown() throws Exception {
System.clearProperty(SystemPropertyKeyConstants.FROM_NAMESPACE_APPLICATION_KEY);
System.clearProperty(SystemPropertyKeyConstants.FROM_NAMESPACE_APPLICATION_KEY_YAML);
System.clearProperty(SystemPropertyKeyConstants.DELIMITED_NAMESPACES);
+ System.clearProperty(SystemPropertyKeyConstants.LISTENER_APP_ID);
+ System.clearProperty(SystemPropertyKeyConstants.LISTENER_NAMESPACE);
System.clearProperty(ApolloClientSystemConsts.APOLLO_PROPERTY_NAMES_CACHE_ENABLE);
super.tearDown();
}
@@ -676,6 +681,87 @@ public void testApolloMultipleConfig() throws IOException {
}
+ @Test
+ public void testApolloConfigChangeListenerWithAppId() {
+ Config applicationConfig = mock(Config.class);
+ Config anotherAppConfig = mock(Config.class);
+
+ mockConfig(someAppId, ConfigConsts.NAMESPACE_APPLICATION, applicationConfig);
+ mockConfig("someAppId2", "namespace2", anotherAppConfig);
+
+ getBean(TestApolloConfigChangeListenerWithAppIdBean.class, AppConfig12.class);
+
+ verify(anotherAppConfig, times(1)).addChangeListener(any(ConfigChangeListener.class));
+ }
+
+ @Test
+ public void testApolloConfigChangeListenerWithInterestedKeyPrefixesAndAppId() {
+ Config applicationConfig = mock(Config.class);
+ Config anotherAppConfig = mock(Config.class);
+
+ mockConfig(someAppId, ConfigConsts.NAMESPACE_APPLICATION, applicationConfig);
+ mockConfig("someAppId2", "namespace2", anotherAppConfig);
+
+ getBean(TestApolloConfigChangeListenerWithInterestedKeyPrefixesAndAppIdBean.class, AppConfig13.class);
+
+ final ArgumentCaptor interestedKeyPrefixesArgumentCaptor = ArgumentCaptor.forClass(Set.class);
+
+ verify(anotherAppConfig, times(1))
+ .addChangeListener(any(ConfigChangeListener.class), Mockito.nullable(Set.class),
+ interestedKeyPrefixesArgumentCaptor.capture());
+
+ assertEquals(1, interestedKeyPrefixesArgumentCaptor.getAllValues().size());
+ assertEquals(Sets.newHashSet("redis.cache.", "logging."),
+ interestedKeyPrefixesArgumentCaptor.getValue());
+ }
+
+ @Test
+ public void testApolloConfigChangeListenerWithAppIdRuntimeRefresh() throws Exception {
+ SimpleConfig defaultAppConfig = prepareConfig(someAppId, ConfigConsts.NAMESPACE_APPLICATION,
+ assembleProperties("runtime.default.timeout", "30"));
+ SimpleConfig anotherAppConfig = prepareConfig(ANOTHER_APP_ID, ConfigConsts.NAMESPACE_APPLICATION,
+ assembleProperties("runtime.another.timeout", "66"));
+
+ TestApolloRuntimeListenerRoutingBean bean = getBean(
+ TestApolloRuntimeListenerRoutingBean.class, TestApolloRuntimeListenerRoutingConfiguration.class);
+
+ assertEquals(30, bean.getTimeout());
+
+ defaultAppConfig.onRepositoryChange(someAppId, ConfigConsts.NAMESPACE_APPLICATION,
+ assembleProperties("runtime.default.timeout", "45"));
+
+ ConfigChangeEvent defaultEvent = bean.pollDefaultEvent(5, TimeUnit.SECONDS);
+ assertNotNull(defaultEvent);
+ assertEquals("45", defaultEvent.getChange("runtime.default.timeout").getNewValue());
+ assertNull(bean.pollAnotherAppEvent(200, TimeUnit.MILLISECONDS));
+ TimeUnit.MILLISECONDS.sleep(100);
+ assertEquals(45, bean.getTimeout());
+
+ anotherAppConfig.onRepositoryChange(ANOTHER_APP_ID, ConfigConsts.NAMESPACE_APPLICATION,
+ assembleProperties("runtime.another.timeout", "77"));
+
+ ConfigChangeEvent anotherEvent = bean.pollAnotherAppEvent(5, TimeUnit.SECONDS);
+ assertNotNull(anotherEvent);
+ assertEquals("77", anotherEvent.getChange("runtime.another.timeout").getNewValue());
+ assertNull(bean.pollDefaultEvent(200, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testApolloConfigChangeListenerResolveAppIdFromSystemProperty() {
+ Config applicationConfig = mock(Config.class);
+ mockConfig(someAppId, ConfigConsts.NAMESPACE_APPLICATION, applicationConfig);
+
+ System.setProperty(SystemPropertyKeyConstants.LISTENER_APP_ID, "someAppId2");
+ System.setProperty(SystemPropertyKeyConstants.LISTENER_NAMESPACE, "namespace2");
+
+ Config anotherAppConfig = mock(Config.class);
+ mockConfig("someAppId2", "namespace2", anotherAppConfig);
+
+ getSimpleBean(TestApolloConfigChangeListenerResolveAppIdFromSystemPropertyConfiguration.class);
+
+ verify(anotherAppConfig, times(1)).addChangeListener(any(ConfigChangeListener.class));
+ }
+
private static class SystemPropertyKeyConstants {
static final String SIMPLE_NAMESPACE = "simple.namespace";
@@ -685,6 +771,8 @@ private static class SystemPropertyKeyConstants {
static final String FROM_NAMESPACE_APPLICATION_KEY = "from.namespace.application.key";
static final String FROM_NAMESPACE_APPLICATION_KEY_YAML = "from.namespace.application.key.yaml";
static final String DELIMITED_NAMESPACES = "delimited.namespaces";
+ static final String LISTENER_APP_ID = "listener.appid";
+ static final String LISTENER_NAMESPACE = "listener.namespace";
}
@EnableApolloConfig
@@ -933,6 +1021,24 @@ public TestApolloConfigChangeListenerWithInterestedKeyPrefixesBean1 bean() {
}
}
+ @Configuration
+ @EnableApolloConfig
+ static class AppConfig12 {
+ @Bean
+ public TestApolloConfigChangeListenerWithAppIdBean bean() {
+ return new TestApolloConfigChangeListenerWithAppIdBean();
+ }
+ }
+
+ @Configuration
+ @EnableApolloConfig
+ static class AppConfig13 {
+ @Bean
+ public TestApolloConfigChangeListenerWithInterestedKeyPrefixesAndAppIdBean bean() {
+ return new TestApolloConfigChangeListenerWithInterestedKeyPrefixesAndAppIdBean();
+ }
+ }
+
static class TestApolloConfigBean1 {
@ApolloConfig
private Config config;
@@ -1093,6 +1199,75 @@ public Config getYamlConfig() {
}
}
+ static class TestApolloConfigChangeListenerWithAppIdBean {
+
+ @ApolloConfigChangeListener(appId = "someAppId2", value = "namespace2")
+ private void onChange(ConfigChangeEvent changeEvent) {
+ }
+ }
+
+ static class TestApolloConfigChangeListenerWithInterestedKeyPrefixesAndAppIdBean {
+
+ @ApolloConfigChangeListener(appId = "someAppId2", value = "namespace2",
+ interestedKeyPrefixes = {"redis.cache.", "logging."})
+ private void onChange(ConfigChangeEvent changeEvent) {
+ }
+ }
+
+ @Configuration
+ @EnableApolloConfig(multipleConfigs = {
+ @MultipleConfig(appId = ANOTHER_APP_ID, namespaces = {ConfigConsts.NAMESPACE_APPLICATION}, order = 9)})
+ static class TestApolloRuntimeListenerRoutingConfiguration {
+
+ @Bean
+ public TestApolloRuntimeListenerRoutingBean bean() {
+ return new TestApolloRuntimeListenerRoutingBean();
+ }
+ }
+
+ static class TestApolloRuntimeListenerRoutingBean {
+
+ private final BlockingQueue defaultEvents = new ArrayBlockingQueue<>(4);
+ private final BlockingQueue anotherAppEvents = new ArrayBlockingQueue<>(4);
+ private volatile int timeout;
+
+ @Value("${runtime.default.timeout:0}")
+ public void setTimeout(int timeout) {
+ this.timeout = timeout;
+ }
+
+ @ApolloConfigChangeListener(interestedKeyPrefixes = {"runtime.default."})
+ private void onDefaultAppChange(ConfigChangeEvent event) {
+ defaultEvents.offer(event);
+ }
+
+ @ApolloConfigChangeListener(appId = ANOTHER_APP_ID, interestedKeyPrefixes = {"runtime.another."})
+ private void onAnotherAppChange(ConfigChangeEvent event) {
+ anotherAppEvents.offer(event);
+ }
+
+ int getTimeout() {
+ return timeout;
+ }
+
+ ConfigChangeEvent pollDefaultEvent(long timeout, TimeUnit unit) throws InterruptedException {
+ return defaultEvents.poll(timeout, unit);
+ }
+
+ ConfigChangeEvent pollAnotherAppEvent(long timeout, TimeUnit unit) throws InterruptedException {
+ return anotherAppEvents.poll(timeout, unit);
+ }
+ }
+
+ @Configuration
+ @EnableApolloConfig
+ static class TestApolloConfigChangeListenerResolveAppIdFromSystemPropertyConfiguration {
+
+ @ApolloConfigChangeListener(appId = "${listener.appid}", value = "${listener.namespace}")
+ private void onChange(ConfigChangeEvent changeEvent) {
+ }
+ }
+
@Configuration
@EnableApolloConfig(value = {"FX_APOLLO_NAMESPACE", "APPLICATION_YAML_NAMESPACE"},
multipleConfigs = {@MultipleConfig(appId = "someAppId2", namespaces = {"namespace2"})})
diff --git a/apollo-compat-tests/apollo-api-compat-it/pom.xml b/apollo-compat-tests/apollo-api-compat-it/pom.xml
new file mode 100644
index 00000000..6f6fb11f
--- /dev/null
+++ b/apollo-compat-tests/apollo-api-compat-it/pom.xml
@@ -0,0 +1,64 @@
+
+
+
+
+ com.ctrip.framework.apollo
+ apollo-compat-tests
+ ${revision}
+ ../pom.xml
+
+ 4.0.0
+
+ apollo-api-compat-it
+ Apollo API Compatibility IT
+
+
+ 1.8
+
+
+
+
+ com.ctrip.framework.apollo
+ apollo-client
+ ${revision}
+
+
+ com.ctrip.framework.apollo
+ apollo-mockserver
+ ${revision}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ org.springframework:*
+ org.springframework.boot:*
+
+
+
+
+
+
diff --git a/apollo-compat-tests/apollo-api-compat-it/src/test/java/com/ctrip/framework/apollo/compat/api/ApolloApiCompatibilityTest.java b/apollo-compat-tests/apollo-api-compat-it/src/test/java/com/ctrip/framework/apollo/compat/api/ApolloApiCompatibilityTest.java
new file mode 100644
index 00000000..fbd87baf
--- /dev/null
+++ b/apollo-compat-tests/apollo-api-compat-it/src/test/java/com/ctrip/framework/apollo/compat/api/ApolloApiCompatibilityTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.compat.api;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import com.ctrip.framework.apollo.Config;
+import com.ctrip.framework.apollo.ConfigFile;
+import com.ctrip.framework.apollo.ConfigService;
+import com.ctrip.framework.apollo.PropertiesCompatibleConfigFile;
+import com.ctrip.framework.apollo.build.ApolloInjector;
+import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
+import com.ctrip.framework.apollo.internals.ConfigManager;
+import com.ctrip.framework.apollo.mockserver.EmbeddedApollo;
+import com.ctrip.framework.apollo.model.ConfigChangeEvent;
+import com.ctrip.framework.apollo.spi.ConfigFactoryManager;
+import com.ctrip.framework.apollo.spi.ConfigRegistry;
+import com.google.common.collect.Table;
+import com.google.common.util.concurrent.SettableFuture;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+public class ApolloApiCompatibilityTest {
+
+ @ClassRule
+ public static final EmbeddedApollo EMBEDDED_APOLLO = new EmbeddedApollo();
+
+ private static final String SOME_APP_ID = "someAppId";
+ private static final String ANOTHER_APP_ID = "100004459";
+ private static final String DEFAULT_VALUE = "undefined";
+
+ private static final String ORIGINAL_APP_ID = System.getProperty("app.id");
+ private static final String ORIGINAL_ENV = System.getProperty("env");
+
+ static {
+ System.setProperty("app.id", SOME_APP_ID);
+ System.setProperty("env", "local");
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ EMBEDDED_APOLLO.resetOverriddenProperties();
+ resetApolloState();
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ resetApolloState();
+ restoreOrClear("app.id", ORIGINAL_APP_ID);
+ restoreOrClear("env", ORIGINAL_ENV);
+ }
+
+ @Test
+ public void shouldLoadConfigsWithFallbackInNoSpringRuntime() {
+ assertClassNotPresent("org.springframework.context.ApplicationContext");
+
+ Config appConfig = ConfigService.getAppConfig();
+ Config anotherAppConfig = ConfigService.getConfig(ANOTHER_APP_ID, "application");
+ Config publicConfig = ConfigService.getConfig("TEST1.apollo");
+ Config yamlConfig = ConfigService.getConfig("application.yaml");
+
+ assertEquals("from-default-app",
+ resolveValueByFallback("primary.key", appConfig, anotherAppConfig, publicConfig, yamlConfig));
+ assertEquals("from-another-app",
+ resolveValueByFallback("fallback.only", appConfig, anotherAppConfig, publicConfig, yamlConfig));
+ assertEquals("from-public-namespace",
+ resolveValueByFallback("public.only", appConfig, anotherAppConfig, publicConfig, yamlConfig));
+ assertEquals("from-yaml-namespace",
+ resolveValueByFallback("yaml.only", appConfig, anotherAppConfig, publicConfig, yamlConfig));
+ assertEquals(DEFAULT_VALUE,
+ resolveValueByFallback("missing.key", appConfig, anotherAppConfig, publicConfig, yamlConfig));
+ }
+
+ @Test
+ public void shouldIsolateListenersForDifferentAppIdsInNoSpringRuntime() throws Exception {
+ Config defaultConfig = ConfigService.getConfig(SOME_APP_ID, "application");
+ Config anotherAppConfig = ConfigService.getConfig(ANOTHER_APP_ID, "application");
+
+ SettableFuture defaultFuture = SettableFuture.create();
+ SettableFuture anotherFuture = SettableFuture.create();
+
+ defaultConfig.addChangeListener(changeEvent -> {
+ if (!defaultFuture.isDone()) {
+ defaultFuture.set(changeEvent);
+ }
+ });
+ anotherAppConfig.addChangeListener(changeEvent -> {
+ if (!anotherFuture.isDone()) {
+ anotherFuture.set(changeEvent);
+ }
+ });
+
+ EMBEDDED_APOLLO.addOrModifyProperty(ANOTHER_APP_ID, "application", "fallback.only", "another-updated");
+
+ ConfigChangeEvent anotherChangeEvent = anotherFuture.get(5, TimeUnit.SECONDS);
+ assertNotNull(anotherChangeEvent.getChange("fallback.only"));
+ assertEquals("from-another-app", anotherChangeEvent.getChange("fallback.only").getOldValue());
+ assertEquals("another-updated", anotherChangeEvent.getChange("fallback.only").getNewValue());
+
+ assertNull(pollFuture(defaultFuture, 300));
+ assertEquals("from-default-app", defaultConfig.getProperty("primary.key", null));
+ assertEquals("another-updated", anotherAppConfig.getProperty("fallback.only", null));
+ }
+
+ @Test
+ public void shouldLoadConfigFilesInNoSpringRuntime() {
+ ConfigFile xmlConfigFile = ConfigService.getConfigFile("datasources", ConfigFileFormat.XML);
+ ConfigFile yamlConfigFile = ConfigService.getConfigFile("application", ConfigFileFormat.YAML);
+
+ assertEquals("db-v1", xmlConfigFile.getContent());
+
+ Properties yamlProperties = ((PropertiesCompatibleConfigFile) yamlConfigFile).asProperties();
+ assertEquals("from-yaml-namespace", yamlProperties.getProperty("yaml.only"));
+ assertEquals("35", yamlProperties.getProperty("redis.cache.commandTimeout"));
+ }
+
+ private static String resolveValueByFallback(String key, Config appConfig, Config anotherAppConfig,
+ Config publicConfig, Config yamlConfig) {
+ String value = appConfig.getProperty(key, DEFAULT_VALUE);
+ if (!DEFAULT_VALUE.equals(value)) {
+ return value;
+ }
+
+ value = anotherAppConfig.getProperty(key, DEFAULT_VALUE);
+ if (!DEFAULT_VALUE.equals(value)) {
+ return value;
+ }
+
+ value = publicConfig.getProperty(key, DEFAULT_VALUE);
+ if (!DEFAULT_VALUE.equals(value)) {
+ return value;
+ }
+
+ return yamlConfig.getProperty(key, DEFAULT_VALUE);
+ }
+
+ private static T pollFuture(SettableFuture future, long timeoutMillis) throws Exception {
+ try {
+ return future.get(timeoutMillis, TimeUnit.MILLISECONDS);
+ } catch (TimeoutException ex) {
+ return null;
+ }
+ }
+
+ private static void assertClassNotPresent(String className) {
+ try {
+ Class.forName(className);
+ fail("Class should not be present: " + className);
+ } catch (ClassNotFoundException ignored) {
+ // ignore
+ }
+ }
+
+ private static void resetApolloState() throws Exception {
+ Method resetMethod = ConfigService.class.getDeclaredMethod("reset");
+ resetMethod.setAccessible(true);
+ resetMethod.invoke(null);
+ clearApolloClientCaches();
+ }
+
+ private static void clearApolloClientCaches() throws Exception {
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configs");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configLocks");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFiles");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFileLocks");
+ clearField(ApolloInjector.getInstance(ConfigFactoryManager.class), "m_factories");
+ clearField(ApolloInjector.getInstance(ConfigRegistry.class), "m_instances");
+ }
+
+ private static void clearField(Object target, String fieldName) throws Exception {
+ Field field = target.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Object container = field.get(target);
+ if (container == null) {
+ return;
+ }
+ if (container instanceof Map) {
+ ((Map, ?>) container).clear();
+ return;
+ }
+ if (container instanceof Table) {
+ ((Table, ?, ?>) container).clear();
+ return;
+ }
+ Method clearMethod = container.getClass().getMethod("clear");
+ clearMethod.setAccessible(true);
+ clearMethod.invoke(container);
+ }
+
+ private static void restoreOrClear(String key, String originalValue) {
+ if (originalValue == null) {
+ System.clearProperty(key);
+ return;
+ }
+ System.setProperty(key, originalValue);
+ }
+}
diff --git a/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-100004459-application.properties b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-100004459-application.properties
new file mode 100644
index 00000000..60093afa
--- /dev/null
+++ b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-100004459-application.properties
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+fallback.only=from-another-app
+shared.order=another-second
diff --git a/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-TEST1.apollo.properties b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-TEST1.apollo.properties
new file mode 100644
index 00000000..c1b4e2ea
--- /dev/null
+++ b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-TEST1.apollo.properties
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+public.only=from-public-namespace
+shared.order=public-third
diff --git a/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-application.properties b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-application.properties
new file mode 100644
index 00000000..538be071
--- /dev/null
+++ b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-application.properties
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+primary.key=from-default-app
+shared.order=app-first
diff --git a/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-application.yaml.properties b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-application.yaml.properties
new file mode 100644
index 00000000..6d148bfd
--- /dev/null
+++ b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-application.yaml.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+content=yaml:\n only: from-yaml-namespace\nredis:\n cache:\n commandTimeout: 35\n
diff --git a/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-datasources.xml.properties b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-datasources.xml.properties
new file mode 100644
index 00000000..094903d9
--- /dev/null
+++ b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-datasources.xml.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+content=db-v1
diff --git a/apollo-compat-tests/apollo-spring-boot-compat-it/pom.xml b/apollo-compat-tests/apollo-spring-boot-compat-it/pom.xml
new file mode 100644
index 00000000..5a8efa46
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-boot-compat-it/pom.xml
@@ -0,0 +1,100 @@
+
+
+
+
+ com.ctrip.framework.apollo
+ apollo-compat-tests
+ ${revision}
+ ../pom.xml
+
+ 4.0.0
+
+ apollo-spring-boot-compat-it
+ Apollo Spring Boot Compatibility Tests
+
+
+ 1.7.21
+ 5.7.0
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${compat.slf4j.version}
+
+
+
+
+
+
+ com.ctrip.framework.apollo
+ apollo-client-config-data
+ ${revision}
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ com.ctrip.framework.apollo
+ apollo-mockserver
+ ${revision}
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-logging
+
+
+ com.jayway.jsonpath
+ json-path
+
+
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ ${compat.junit.vintage.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+
diff --git a/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/java/com/ctrip/framework/apollo/compat/springboot/ApolloSpringBootCompatibilityTest.java b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/java/com/ctrip/framework/apollo/compat/springboot/ApolloSpringBootCompatibilityTest.java
new file mode 100644
index 00000000..8fb77ca0
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/java/com/ctrip/framework/apollo/compat/springboot/ApolloSpringBootCompatibilityTest.java
@@ -0,0 +1,480 @@
+/*
+ * 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.compat.springboot;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.ctrip.framework.apollo.Config;
+import com.ctrip.framework.apollo.ConfigService;
+import com.ctrip.framework.apollo.build.ApolloInjector;
+import com.ctrip.framework.apollo.internals.ConfigManager;
+import com.ctrip.framework.apollo.internals.DefaultConfig;
+import com.ctrip.framework.apollo.mockserver.EmbeddedApollo;
+import com.ctrip.framework.apollo.model.ConfigChangeEvent;
+import com.ctrip.framework.apollo.spi.ConfigFactoryManager;
+import com.ctrip.framework.apollo.spi.ConfigRegistry;
+import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
+import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
+import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
+import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
+import com.ctrip.framework.apollo.spring.annotation.MultipleConfig;
+import com.ctrip.framework.apollo.spring.events.ApolloConfigChangeEvent;
+import com.google.common.collect.Table;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import org.junit.Assert;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringBootTest(classes = ApolloSpringBootCompatibilityTest.TestConfiguration.class,
+ webEnvironment = SpringBootTest.WebEnvironment.NONE,
+ properties = {
+ "app.id=someAppId",
+ "env=local",
+ "spring.config.import=apollo://application,apollo://TEST1.apollo,apollo://application.yaml",
+ "listeners=application,TEST1.apollo,application.yaml",
+ "org.springframework.boot.logging.LoggingSystem=none"
+ })
+@DirtiesContext
+public class ApolloSpringBootCompatibilityTest {
+
+ private static final String SOME_APP_ID = "someAppId";
+ private static final String ANOTHER_APP_ID = "100004459";
+
+ @ClassRule
+ public static final EmbeddedApollo EMBEDDED_APOLLO = new EmbeddedApollo();
+
+ @Autowired
+ private Environment environment;
+
+ @Autowired(required = false)
+ private FeatureBean featureBean;
+
+ @Autowired
+ private RedisCacheProperties redisCacheProperties;
+
+ @Autowired
+ private CompatAnnotatedBean compatAnnotatedBean;
+
+ @Autowired
+ private ApolloApplicationListenerProbe applicationListenerProbe;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ EMBEDDED_APOLLO.resetOverriddenProperties();
+ resetApolloState();
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ resetApolloState();
+ }
+
+ @Test
+ public void shouldCoverSpringBootDemoScenarios() throws Exception {
+ assertEquals("boot-compat", environment.getProperty("yaml.marker"));
+ assertNotNull(featureBean);
+ assertTrue(redisCacheProperties.isEnabled());
+ assertEquals(40, redisCacheProperties.getCommandTimeout());
+
+ assertEquals(800, compatAnnotatedBean.getTimeout());
+ assertEquals("from-public-boot", compatAnnotatedBean.getPublicOnly());
+ assertEquals("from-another-app-boot",
+ compatAnnotatedBean.getAnotherAppConfig().getProperty("compat.origin", null));
+ assertEquals("from-public-boot",
+ compatAnnotatedBean.getPublicNamespaceConfig().getProperty("public.only", null));
+ assertEquals("boot-compat",
+ compatAnnotatedBean.getYamlNamespaceConfig().getProperty("yaml.marker", null));
+ assertEquals("800",
+ compatAnnotatedBean.getApplicationConfig().getProperty("compat.timeout", null));
+ assertEquals(2, compatAnnotatedBean.getJsonBeans().size());
+ assertEquals("alpha-boot", compatAnnotatedBean.getJsonBeans().get(0).getSomeString());
+
+ Config applicationConfig = ConfigService.getConfig("application");
+ Properties applicationProperties = copyConfigProperties(applicationConfig);
+ applicationProperties.setProperty("compat.timeout", "801");
+ applicationProperties.setProperty("jsonBeanProperty",
+ "[{\"someString\":\"gamma-boot\",\"someInt\":303}]");
+ applyConfigChange(applicationConfig, SOME_APP_ID, "application", applicationProperties);
+
+ Config publicConfig = ConfigService.getConfig("TEST1.apollo");
+ Properties publicProperties = copyConfigProperties(publicConfig);
+ publicProperties.setProperty("public.only", "from-public-boot-updated");
+ applyConfigChange(publicConfig, SOME_APP_ID, "TEST1.apollo", publicProperties);
+
+ Config yamlConfig = ConfigService.getConfig("application.yaml");
+ Properties yamlProperties = copyConfigProperties(yamlConfig);
+ yamlProperties.setProperty("yaml.marker", "boot-compat-updated");
+ applyConfigChange(yamlConfig, SOME_APP_ID, "application.yaml", yamlProperties);
+
+ Properties anotherAppProperties = copyConfigProperties(compatAnnotatedBean.getAnotherAppConfig());
+ anotherAppProperties.setProperty("compat.origin", "changed-origin-boot");
+ applyConfigChange(compatAnnotatedBean.getAnotherAppConfig(), ANOTHER_APP_ID, "application",
+ anotherAppProperties);
+
+ ConfigChangeEvent defaultChange = compatAnnotatedBean.pollDefaultEvent(5, TimeUnit.SECONDS);
+ assertNotNull(defaultChange);
+ assertNotNull(defaultChange.getChange("compat.timeout"));
+
+ ConfigChangeEvent publicChange = compatAnnotatedBean.pollPublicNamespaceEvent(5, TimeUnit.SECONDS);
+ assertNotNull(publicChange);
+ assertNotNull(publicChange.getChange("public.only"));
+
+ ConfigChangeEvent yamlChange = compatAnnotatedBean.pollYamlNamespaceEvent(5, TimeUnit.SECONDS);
+ assertNotNull(yamlChange);
+ assertNotNull(yamlChange.getChange("yaml.marker"));
+
+ ConfigChangeEvent anotherAppChange = compatAnnotatedBean.pollAnotherAppEvent(5, TimeUnit.SECONDS);
+ assertNotNull(anotherAppChange);
+ assertNotNull(anotherAppChange.getChange("compat.origin"));
+
+ waitForCondition("another app config should be updated",
+ () -> "changed-origin-boot".equals(
+ compatAnnotatedBean.getAnotherAppConfig().getProperty("compat.origin", null)));
+ waitForCondition("public namespace config should be updated",
+ () -> "from-public-boot-updated".equals(
+ compatAnnotatedBean.getPublicNamespaceConfig().getProperty("public.only", null)));
+ waitForCondition("yaml namespace config should be updated",
+ () -> "boot-compat-updated".equals(
+ compatAnnotatedBean.getYamlNamespaceConfig().getProperty("yaml.marker", null)));
+ waitForCondition("application namespace config should be updated",
+ () -> "801".equals(
+ compatAnnotatedBean.getApplicationConfig().getProperty("compat.timeout", null)));
+ waitForCondition("json value should be updated",
+ () -> compatAnnotatedBean.getJsonBeans().size() == 1
+ && "gamma-boot".equals(compatAnnotatedBean.getJsonBeans().get(0).getSomeString()));
+
+ waitForCondition("ApplicationListener should receive namespace updates",
+ () -> applicationListenerProbe.hasNamespace("application")
+ && applicationListenerProbe.hasNamespace("TEST1.apollo")
+ && applicationListenerProbe.hasNamespace("application.yaml"));
+ }
+
+ @EnableAutoConfiguration
+ @EnableApolloConfig(value = {"application", "TEST1.apollo", "application.yaml"},
+ multipleConfigs = {
+ @MultipleConfig(appId = ANOTHER_APP_ID, namespaces = {"application"}, order = 9)
+ })
+ @EnableConfigurationProperties(RedisCacheProperties.class)
+ @Configuration
+ static class TestConfiguration {
+
+ @Bean
+ @ConditionalOnProperty(value = "feature.enabled", havingValue = "true")
+ public FeatureBean featureBean() {
+ return new FeatureBean();
+ }
+
+ @Bean
+ public CompatAnnotatedBean compatAnnotatedBean() {
+ return new CompatAnnotatedBean();
+ }
+
+ @Bean
+ public ApolloApplicationListenerProbe apolloApplicationListenerProbe() {
+ return new ApolloApplicationListenerProbe();
+ }
+
+ }
+
+ static class FeatureBean {
+ }
+
+ @ConfigurationProperties(prefix = "redis.cache")
+ static class RedisCacheProperties {
+
+ private boolean enabled;
+ private int commandTimeout;
+ private int expireSeconds;
+ private String clusterNodes;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public int getCommandTimeout() {
+ return commandTimeout;
+ }
+
+ public void setCommandTimeout(int commandTimeout) {
+ this.commandTimeout = commandTimeout;
+ }
+
+ public int getExpireSeconds() {
+ return expireSeconds;
+ }
+
+ public void setExpireSeconds(int expireSeconds) {
+ this.expireSeconds = expireSeconds;
+ }
+
+ public String getClusterNodes() {
+ return clusterNodes;
+ }
+
+ public void setClusterNodes(String clusterNodes) {
+ this.clusterNodes = clusterNodes;
+ }
+ }
+
+ public static class CompatAnnotatedBean {
+
+ private final BlockingQueue defaultEvents =
+ new ArrayBlockingQueue(8);
+ private final BlockingQueue publicNamespaceEvents =
+ new ArrayBlockingQueue(8);
+ private final BlockingQueue yamlNamespaceEvents =
+ new ArrayBlockingQueue(8);
+ private final BlockingQueue anotherAppEvents =
+ new ArrayBlockingQueue(8);
+
+ private volatile int timeout;
+ private volatile String publicOnly;
+ private volatile List jsonBeans = Collections.emptyList();
+
+ @ApolloConfig
+ private Config applicationConfig;
+
+ @ApolloConfig("TEST1.apollo")
+ private Config publicNamespaceConfig;
+
+ @ApolloConfig("application.yaml")
+ private Config yamlNamespaceConfig;
+
+ @ApolloConfig(appId = ANOTHER_APP_ID)
+ private Config anotherAppConfig;
+
+ @Value("${compat.timeout:0}")
+ public void setTimeout(int timeout) {
+ this.timeout = timeout;
+ }
+
+ @Value("${public.only:missing}")
+ public void setPublicOnly(String publicOnly) {
+ this.publicOnly = publicOnly;
+ }
+
+ @ApolloJsonValue("${jsonBeanProperty:[]}")
+ public void setJsonBeans(List jsonBeans) {
+ this.jsonBeans = jsonBeans;
+ }
+
+ @ApolloConfigChangeListener(value = "application", interestedKeyPrefixes = {"compat."})
+ private void onDefaultNamespaceChange(ConfigChangeEvent event) {
+ defaultEvents.offer(event);
+ }
+
+ @ApolloConfigChangeListener(value = "TEST1.apollo", interestedKeyPrefixes = {"public."})
+ private void onPublicNamespaceChange(ConfigChangeEvent event) {
+ publicNamespaceEvents.offer(event);
+ }
+
+ @ApolloConfigChangeListener(value = "application.yaml", interestedKeyPrefixes = {"yaml."})
+ private void onYamlNamespaceChange(ConfigChangeEvent event) {
+ yamlNamespaceEvents.offer(event);
+ }
+
+ @ApolloConfigChangeListener(appId = ANOTHER_APP_ID,
+ interestedKeyPrefixes = {"compat.origin"})
+ private void onAnotherAppChange(ConfigChangeEvent event) {
+ anotherAppEvents.offer(event);
+ }
+
+ int getTimeout() {
+ return timeout;
+ }
+
+ String getPublicOnly() {
+ return publicOnly;
+ }
+
+ List getJsonBeans() {
+ return jsonBeans;
+ }
+
+ Config getAnotherAppConfig() {
+ return anotherAppConfig;
+ }
+
+ Config getApplicationConfig() {
+ return applicationConfig;
+ }
+
+ Config getPublicNamespaceConfig() {
+ return publicNamespaceConfig;
+ }
+
+ Config getYamlNamespaceConfig() {
+ return yamlNamespaceConfig;
+ }
+
+ ConfigChangeEvent pollDefaultEvent(long timeout, TimeUnit unit) throws InterruptedException {
+ return defaultEvents.poll(timeout, unit);
+ }
+
+ ConfigChangeEvent pollPublicNamespaceEvent(long timeout, TimeUnit unit) throws InterruptedException {
+ return publicNamespaceEvents.poll(timeout, unit);
+ }
+
+ ConfigChangeEvent pollYamlNamespaceEvent(long timeout, TimeUnit unit) throws InterruptedException {
+ return yamlNamespaceEvents.poll(timeout, unit);
+ }
+
+ ConfigChangeEvent pollAnotherAppEvent(long timeout, TimeUnit unit) throws InterruptedException {
+ return anotherAppEvents.poll(timeout, unit);
+ }
+ }
+
+ static class ApolloApplicationListenerProbe implements ApplicationListener {
+
+ private final Set changes = Collections.synchronizedSet(new HashSet());
+ private final Set namespaces = Collections.synchronizedSet(new HashSet());
+
+ @Override
+ public void onApplicationEvent(ApplicationEvent event) {
+ if (event instanceof ApolloConfigChangeEvent) {
+ ConfigChangeEvent configChangeEvent = ((ApolloConfigChangeEvent) event).getConfigChangeEvent();
+ changes.add(configChangeEvent.getAppId() + "#" + configChangeEvent.getNamespace());
+ namespaces.add(configChangeEvent.getNamespace());
+ }
+ }
+
+ boolean hasReceived(String marker) {
+ return changes.contains(marker);
+ }
+
+ boolean hasNamespace(String namespace) {
+ return namespaces.contains(namespace);
+ }
+ }
+
+ public static class JsonBean {
+
+ private String someString;
+ private int someInt;
+
+ public String getSomeString() {
+ return someString;
+ }
+
+ public int getSomeInt() {
+ return someInt;
+ }
+ }
+
+ private static void resetApolloState() throws Exception {
+ Class> initializerClass = Class.forName(
+ "com.ctrip.framework.apollo.config.data.importer.ApolloConfigDataLoaderInitializer");
+ Field initialized = initializerClass.getDeclaredField("INITIALIZED");
+ initialized.setAccessible(true);
+ initialized.setBoolean(null, false);
+
+ Method resetMethod = ConfigService.class.getDeclaredMethod("reset");
+ resetMethod.setAccessible(true);
+ resetMethod.invoke(null);
+ clearApolloClientCaches();
+ }
+
+ private static void clearApolloClientCaches() throws Exception {
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configs");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configLocks");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFiles");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFileLocks");
+ clearField(ApolloInjector.getInstance(ConfigFactoryManager.class), "m_factories");
+ clearField(ApolloInjector.getInstance(ConfigRegistry.class), "m_instances");
+ }
+
+ private static void clearField(Object target, String fieldName) throws Exception {
+ Field field = target.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Object container = field.get(target);
+ if (container == null) {
+ return;
+ }
+ if (container instanceof Map) {
+ ((Map, ?>) container).clear();
+ return;
+ }
+ if (container instanceof Table) {
+ ((Table, ?, ?>) container).clear();
+ return;
+ }
+ Method clearMethod = container.getClass().getMethod("clear");
+ clearMethod.setAccessible(true);
+ clearMethod.invoke(container);
+ }
+
+ private static Properties copyConfigProperties(Config config) {
+ Properties properties = new Properties();
+ for (String key : config.getPropertyNames()) {
+ properties.setProperty(key, config.getProperty(key, ""));
+ }
+ return properties;
+ }
+
+ private static void applyConfigChange(Config config, String appId, String namespace,
+ Properties properties) {
+ Assert.assertTrue(config instanceof DefaultConfig);
+ ((DefaultConfig) config).onRepositoryChange(appId, namespace, properties);
+ }
+
+ private static void waitForCondition(String message, Callable condition) throws Exception {
+ long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(10);
+ while (System.currentTimeMillis() < deadline) {
+ if (Boolean.TRUE.equals(condition.call())) {
+ return;
+ }
+ TimeUnit.MILLISECONDS.sleep(100);
+ }
+ throw new AssertionError(message);
+ }
+
+}
diff --git a/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-100004459-application.properties b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-100004459-application.properties
new file mode 100644
index 00000000..3955d17c
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-100004459-application.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+compat.origin=from-another-app-boot
diff --git a/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-TEST1.apollo.properties b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-TEST1.apollo.properties
new file mode 100644
index 00000000..d1d85ce7
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-TEST1.apollo.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+public.only=from-public-boot
diff --git a/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-application.properties b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-application.properties
new file mode 100644
index 00000000..5df7e520
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-application.properties
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+feature.enabled=true
+redis.cache.enabled=true
+redis.cache.commandTimeout=40
+redis.cache.expireSeconds=100
+redis.cache.clusterNodes=1,2
+listeners=application,TEST1.apollo,application.yaml
+compat.timeout=800
+jsonBeanProperty=[{"someString":"alpha-boot","someInt":101},{"someString":"beta-boot","someInt":202}]
diff --git a/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-application.yaml.properties b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-application.yaml.properties
new file mode 100644
index 00000000..fbe6a7fd
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-application.yaml.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+content=yaml:\n marker: boot-compat\n
diff --git a/apollo-compat-tests/apollo-spring-compat-it/pom.xml b/apollo-compat-tests/apollo-spring-compat-it/pom.xml
new file mode 100644
index 00000000..5b5b3761
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-compat-it/pom.xml
@@ -0,0 +1,96 @@
+
+
+
+
+ com.ctrip.framework.apollo
+ apollo-compat-tests
+ ${revision}
+ ../pom.xml
+
+ 4.0.0
+
+ apollo-spring-compat-it
+ Apollo Spring Compatibility IT
+
+
+ 1.8
+ 3.1.1.RELEASE
+
+
+
+
+ com.ctrip.framework.apollo
+ apollo-client
+ ${revision}
+
+
+ com.ctrip.framework.apollo
+ apollo-mockserver
+ ${revision}
+ test
+
+
+ org.springframework
+ spring-context
+ ${spring.framework.version}
+
+
+ org.springframework
+ spring-aop
+ ${spring.framework.version}
+
+
+ org.springframework
+ spring-beans
+ ${spring.framework.version}
+
+
+ org.springframework
+ spring-core
+ ${spring.framework.version}
+
+
+ org.springframework
+ spring-expression
+ ${spring.framework.version}
+
+
+ org.springframework
+ spring-test
+ ${spring.framework.version}
+ test
+
+
+ cglib
+ cglib-nodep
+ 3.3.0
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+
diff --git a/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringAnnotationCompatibilityTest.java b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringAnnotationCompatibilityTest.java
new file mode 100644
index 00000000..fb1ddcae
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringAnnotationCompatibilityTest.java
@@ -0,0 +1,287 @@
+/*
+ * 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.compat.spring;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import com.ctrip.framework.apollo.Config;
+import com.ctrip.framework.apollo.ConfigService;
+import com.ctrip.framework.apollo.mockserver.EmbeddedApollo;
+import com.ctrip.framework.apollo.model.ConfigChangeEvent;
+import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
+import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
+import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
+import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
+import com.ctrip.framework.apollo.spring.annotation.MultipleConfig;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = SpringAnnotationCompatibilityTest.TestConfiguration.class)
+public class SpringAnnotationCompatibilityTest {
+
+ private static final String ANOTHER_APP_ID = "100004459";
+
+ @ClassRule
+ public static final EmbeddedApollo EMBEDDED_APOLLO = new EmbeddedApollo();
+
+ @Autowired
+ private AnnotationProbe probe;
+
+ @Autowired
+ private SpringApolloEventListenerProbe apolloEventListenerProbe;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ SpringCompatibilityTestSupport.beforeClass(EMBEDDED_APOLLO);
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ SpringCompatibilityTestSupport.afterClass();
+ }
+
+ @Test
+ public void shouldSupportAnnotationAndMultipleConfig() throws Exception {
+ assertEquals(5001, probe.getTimeout());
+ assertEquals("from-public", probe.getPublicValue());
+ assertEquals("from-yaml", probe.getYamlMarker());
+ assertEquals("from-another-app", probe.getAnotherAppConfig().getProperty("compat.origin", null));
+ assertEquals("5001", probe.getApplicationConfig().getProperty("compat.timeout", null));
+ assertEquals("from-public", probe.getPublicNamespaceConfig().getProperty("public.key", null));
+ assertEquals("from-yaml", probe.getYamlNamespaceConfig().getProperty("yaml.marker", null));
+ assertEquals(2, probe.getJsonBeans().size());
+ assertEquals("alpha", probe.getJsonBeans().get(0).getSomeString());
+
+ Config applicationConfig = ConfigService.getConfig("application");
+ Properties applicationProperties =
+ SpringCompatibilityTestSupport.copyConfigProperties(applicationConfig);
+ applicationProperties.setProperty("compat.timeout", "5002");
+ SpringCompatibilityTestSupport.applyConfigChange(applicationConfig, "application",
+ applicationProperties);
+
+ Config publicConfig = ConfigService.getConfig("TEST1.apollo");
+ Properties publicProperties = SpringCompatibilityTestSupport.copyConfigProperties(publicConfig);
+ publicProperties.setProperty("public.key", "from-public-updated");
+ SpringCompatibilityTestSupport.applyConfigChange(publicConfig, "TEST1.apollo", publicProperties);
+
+ Config yamlConfig = ConfigService.getConfig("application.yaml");
+ Properties yamlProperties = SpringCompatibilityTestSupport.copyConfigProperties(yamlConfig);
+ yamlProperties.setProperty("yaml.marker", "from-yaml-updated");
+ SpringCompatibilityTestSupport.applyConfigChange(yamlConfig, "application.yaml", yamlProperties);
+
+ Properties anotherAppProperties =
+ SpringCompatibilityTestSupport.copyConfigProperties(probe.getAnotherAppConfig());
+ anotherAppProperties.setProperty("compat.origin", "changed-origin");
+ SpringCompatibilityTestSupport.applyConfigChange(probe.getAnotherAppConfig(), ANOTHER_APP_ID,
+ "application", anotherAppProperties);
+
+ ConfigChangeEvent defaultChange = probe.pollDefaultEvent(10, TimeUnit.SECONDS);
+ assertNotNull(defaultChange);
+ assertNotNull(defaultChange.getChange("compat.timeout"));
+
+ ConfigChangeEvent publicNamespaceChange = probe.pollPublicNamespaceEvent(10, TimeUnit.SECONDS);
+ assertNotNull(publicNamespaceChange);
+ assertNotNull(publicNamespaceChange.getChange("public.key"));
+
+ ConfigChangeEvent yamlNamespaceChange = probe.pollYamlNamespaceEvent(10, TimeUnit.SECONDS);
+ assertNotNull(yamlNamespaceChange);
+ assertEquals("application.yaml", yamlNamespaceChange.getNamespace());
+
+ ConfigChangeEvent anotherAppChange = probe.pollAnotherAppEvent(10, TimeUnit.SECONDS);
+ assertNotNull(anotherAppChange);
+ assertNotNull(anotherAppChange.getChange("compat.origin"));
+
+ String namespace = apolloEventListenerProbe.pollNamespace(10, TimeUnit.SECONDS);
+ assertEquals("application", namespace);
+
+ SpringCompatibilityTestSupport.waitForCondition("public value should be updated",
+ () -> "from-public-updated".equals(
+ probe.getPublicNamespaceConfig().getProperty("public.key", null)));
+ SpringCompatibilityTestSupport.waitForCondition("yaml marker should be updated",
+ () -> "from-yaml-updated".equals(
+ probe.getYamlNamespaceConfig().getProperty("yaml.marker", null)));
+ SpringCompatibilityTestSupport.waitForCondition("application config should be updated",
+ () -> "5002".equals(probe.getApplicationConfig().getProperty("compat.timeout", null)));
+ SpringCompatibilityTestSupport.waitForCondition("another app config should be updated",
+ () -> "changed-origin".equals(probe.getAnotherAppConfig().getProperty("compat.origin", null)));
+ }
+
+ @Configuration
+ @EnableApolloConfig(value = {"application", "TEST1.apollo", "application.yaml"},
+ multipleConfigs = {@MultipleConfig(appId = ANOTHER_APP_ID, namespaces = {"application"}, order = 9)})
+ static class TestConfiguration {
+
+ @Bean
+ public AnnotationProbe annotationProbe() {
+ return new AnnotationProbe();
+ }
+
+ @Bean
+ public SpringApolloEventListenerProbe apolloEventListenerProbe() {
+ return new SpringApolloEventListenerProbe();
+ }
+ }
+
+ static class AnnotationProbe {
+
+ private final BlockingQueue defaultEvents =
+ new ArrayBlockingQueue(8);
+ private final BlockingQueue publicNamespaceEvents =
+ new ArrayBlockingQueue(8);
+ private final BlockingQueue yamlNamespaceEvents =
+ new ArrayBlockingQueue(8);
+ private final BlockingQueue anotherAppEvents =
+ new ArrayBlockingQueue(8);
+
+ private volatile int timeout;
+ private volatile String publicValue;
+ private volatile String yamlMarker;
+ private volatile List jsonBeans = Collections.emptyList();
+
+ @ApolloConfig
+ private Config applicationConfig;
+
+ @ApolloConfig("TEST1.apollo")
+ private Config publicNamespaceConfig;
+
+ @ApolloConfig("application.yaml")
+ private Config yamlNamespaceConfig;
+
+ @ApolloConfig(appId = ANOTHER_APP_ID)
+ private Config anotherAppConfig;
+
+ @Value("${compat.timeout:0}")
+ public void setTimeout(int timeout) {
+ this.timeout = timeout;
+ }
+
+ @Value("${public.key:missing}")
+ public void setPublicValue(String publicValue) {
+ this.publicValue = publicValue;
+ }
+
+ @Value("${yaml.marker:missing}")
+ public void setYamlMarker(String yamlMarker) {
+ this.yamlMarker = yamlMarker;
+ }
+
+ @ApolloJsonValue("${jsonBeanProperty:[]}")
+ public void setJsonBeans(List jsonBeans) {
+ this.jsonBeans = jsonBeans;
+ }
+
+ @ApolloConfigChangeListener
+ private void onDefaultNamespaceChange(ConfigChangeEvent event) {
+ defaultEvents.offer(event);
+ }
+
+ @ApolloConfigChangeListener("TEST1.apollo")
+ private void onPublicNamespaceChange(ConfigChangeEvent event) {
+ publicNamespaceEvents.offer(event);
+ }
+
+ @ApolloConfigChangeListener("application.yaml")
+ private void onYamlNamespaceChange(ConfigChangeEvent event) {
+ yamlNamespaceEvents.offer(event);
+ }
+
+ @ApolloConfigChangeListener(appId = ANOTHER_APP_ID,
+ interestedKeyPrefixes = {"compat.origin"})
+ private void onAnotherAppChange(ConfigChangeEvent event) {
+ anotherAppEvents.offer(event);
+ }
+
+ int getTimeout() {
+ return timeout;
+ }
+
+ String getPublicValue() {
+ return publicValue;
+ }
+
+ String getYamlMarker() {
+ return yamlMarker;
+ }
+
+ List getJsonBeans() {
+ return jsonBeans;
+ }
+
+ Config getAnotherAppConfig() {
+ return anotherAppConfig;
+ }
+
+ Config getApplicationConfig() {
+ return applicationConfig;
+ }
+
+ Config getPublicNamespaceConfig() {
+ return publicNamespaceConfig;
+ }
+
+ Config getYamlNamespaceConfig() {
+ return yamlNamespaceConfig;
+ }
+
+ ConfigChangeEvent pollDefaultEvent(long timeout, TimeUnit unit) throws InterruptedException {
+ return defaultEvents.poll(timeout, unit);
+ }
+
+ ConfigChangeEvent pollPublicNamespaceEvent(long timeout, TimeUnit unit) throws InterruptedException {
+ return publicNamespaceEvents.poll(timeout, unit);
+ }
+
+ ConfigChangeEvent pollYamlNamespaceEvent(long timeout, TimeUnit unit) throws InterruptedException {
+ return yamlNamespaceEvents.poll(timeout, unit);
+ }
+
+ ConfigChangeEvent pollAnotherAppEvent(long timeout, TimeUnit unit) throws InterruptedException {
+ return anotherAppEvents.poll(timeout, unit);
+ }
+ }
+
+ static class JsonBean {
+
+ private String someString;
+ private int someInt;
+
+ public String getSomeString() {
+ return someString;
+ }
+
+ public int getSomeInt() {
+ return someInt;
+ }
+ }
+}
diff --git a/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringApolloEventListenerProbe.java b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringApolloEventListenerProbe.java
new file mode 100644
index 00000000..b614a91e
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringApolloEventListenerProbe.java
@@ -0,0 +1,40 @@
+/*
+ * 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.compat.spring;
+
+import com.ctrip.framework.apollo.spring.events.ApolloConfigChangeEvent;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationListener;
+
+public class SpringApolloEventListenerProbe implements ApplicationListener {
+
+ private final BlockingQueue namespaces = new LinkedBlockingQueue();
+
+ @Override
+ public void onApplicationEvent(ApplicationEvent event) {
+ if (event instanceof ApolloConfigChangeEvent) {
+ namespaces.offer(((ApolloConfigChangeEvent) event).getConfigChangeEvent().getNamespace());
+ }
+ }
+
+ public String pollNamespace(long timeout, TimeUnit unit) throws InterruptedException {
+ return namespaces.poll(timeout, unit);
+ }
+}
diff --git a/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringCompatibilityTestSupport.java b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringCompatibilityTestSupport.java
new file mode 100644
index 00000000..0f2024d9
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringCompatibilityTestSupport.java
@@ -0,0 +1,130 @@
+/*
+ * 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.compat.spring;
+
+import com.ctrip.framework.apollo.Config;
+import com.ctrip.framework.apollo.ConfigService;
+import com.ctrip.framework.apollo.build.ApolloInjector;
+import com.ctrip.framework.apollo.internals.DefaultConfig;
+import com.ctrip.framework.apollo.internals.ConfigManager;
+import com.ctrip.framework.apollo.mockserver.EmbeddedApollo;
+import com.ctrip.framework.apollo.spi.ConfigFactoryManager;
+import com.ctrip.framework.apollo.spi.ConfigRegistry;
+import com.google.common.collect.Table;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import org.junit.Assert;
+
+final class SpringCompatibilityTestSupport {
+
+ private static final String ORIGINAL_APP_ID = System.getProperty("app.id");
+ private static final String ORIGINAL_ENV = System.getProperty("env");
+
+ private SpringCompatibilityTestSupport() {
+ }
+
+ static void beforeClass(EmbeddedApollo embeddedApollo) throws Exception {
+ System.setProperty("app.id", "someAppId");
+ System.setProperty("env", "local");
+ embeddedApollo.resetOverriddenProperties();
+ resetApolloState();
+ }
+
+ static void afterClass() throws Exception {
+ restoreOrClear("app.id", ORIGINAL_APP_ID);
+ restoreOrClear("env", ORIGINAL_ENV);
+ resetApolloState();
+ }
+
+ static void waitForCondition(String failureMessage, Callable condition) throws Exception {
+ long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(15);
+ while (System.currentTimeMillis() < deadline) {
+ if (Boolean.TRUE.equals(condition.call())) {
+ return;
+ }
+ TimeUnit.MILLISECONDS.sleep(100);
+ }
+ throw new AssertionError(failureMessage);
+ }
+
+ static Properties copyConfigProperties(Config config) {
+ Properties properties = new Properties();
+ for (String key : config.getPropertyNames()) {
+ properties.setProperty(key, config.getProperty(key, ""));
+ }
+ return properties;
+ }
+
+ static void applyConfigChange(Config config, String namespace, Properties properties) {
+ Assert.assertTrue(config instanceof DefaultConfig);
+ ((DefaultConfig) config).onRepositoryChange(namespace, properties);
+ }
+
+ static void applyConfigChange(Config config, String appId, String namespace,
+ Properties properties) {
+ Assert.assertTrue(config instanceof DefaultConfig);
+ ((DefaultConfig) config).onRepositoryChange(appId, namespace, properties);
+ }
+
+ private static void restoreOrClear(String key, String originalValue) {
+ if (originalValue == null) {
+ System.clearProperty(key);
+ return;
+ }
+ System.setProperty(key, originalValue);
+ }
+
+ private static void resetApolloState() throws Exception {
+ Method resetMethod = ConfigService.class.getDeclaredMethod("reset");
+ resetMethod.setAccessible(true);
+ resetMethod.invoke(null);
+ clearApolloClientCaches();
+ }
+
+ private static void clearApolloClientCaches() throws Exception {
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configs");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configLocks");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFiles");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFileLocks");
+ clearField(ApolloInjector.getInstance(ConfigFactoryManager.class), "m_factories");
+ clearField(ApolloInjector.getInstance(ConfigRegistry.class), "m_instances");
+ }
+
+ private static void clearField(Object target, String fieldName) throws Exception {
+ Field field = target.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Object container = field.get(target);
+ if (container == null) {
+ return;
+ }
+ if (container instanceof Map) {
+ ((Map, ?>) container).clear();
+ return;
+ }
+ if (container instanceof Table) {
+ ((Table, ?, ?>) container).clear();
+ return;
+ }
+ Method clearMethod = container.getClass().getMethod("clear");
+ clearMethod.setAccessible(true);
+ clearMethod.invoke(container);
+ }
+}
diff --git a/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringXmlBean.java b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringXmlBean.java
new file mode 100644
index 00000000..c77328ae
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringXmlBean.java
@@ -0,0 +1,57 @@
+/*
+ * 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.compat.spring;
+
+public class SpringXmlBean {
+
+ private int timeout;
+ private int batch;
+ private String publicKey;
+ private String yamlMarker;
+
+ public int getTimeout() {
+ return timeout;
+ }
+
+ public void setTimeout(int timeout) {
+ this.timeout = timeout;
+ }
+
+ public int getBatch() {
+ return batch;
+ }
+
+ public void setBatch(int batch) {
+ this.batch = batch;
+ }
+
+ public String getPublicKey() {
+ return publicKey;
+ }
+
+ public void setPublicKey(String publicKey) {
+ this.publicKey = publicKey;
+ }
+
+ public String getYamlMarker() {
+ return yamlMarker;
+ }
+
+ public void setYamlMarker(String yamlMarker) {
+ this.yamlMarker = yamlMarker;
+ }
+}
diff --git a/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringXmlCompatibilityTest.java b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringXmlCompatibilityTest.java
new file mode 100644
index 00000000..31c9a00f
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringXmlCompatibilityTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.compat.spring;
+
+import static org.junit.Assert.assertEquals;
+
+import com.ctrip.framework.apollo.Config;
+import com.ctrip.framework.apollo.ConfigService;
+import com.ctrip.framework.apollo.mockserver.EmbeddedApollo;
+import java.util.Properties;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(locations = "classpath:/spring/apollo-context.xml")
+public class SpringXmlCompatibilityTest {
+
+ @ClassRule
+ public static final EmbeddedApollo EMBEDDED_APOLLO = new EmbeddedApollo();
+
+ @Autowired
+ private SpringXmlBean xmlBean;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ SpringCompatibilityTestSupport.beforeClass(EMBEDDED_APOLLO);
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ SpringCompatibilityTestSupport.afterClass();
+ }
+
+ @Test
+ public void shouldSupportXmlConfig() throws Exception {
+ assertEquals(5099, xmlBean.getTimeout());
+ assertEquals(51, xmlBean.getBatch());
+ assertEquals("from-public", xmlBean.getPublicKey());
+ assertEquals("from-yaml", xmlBean.getYamlMarker());
+
+ Config applicationConfig = ConfigService.getConfig("application");
+ Properties applicationProperties =
+ SpringCompatibilityTestSupport.copyConfigProperties(applicationConfig);
+ applicationProperties.setProperty("compat.xml.timeout", "5199");
+ applicationProperties.setProperty("compat.xml.batch", "61");
+ SpringCompatibilityTestSupport.applyConfigChange(applicationConfig, "application",
+ applicationProperties);
+
+ Config publicConfig = ConfigService.getConfig("TEST1.apollo");
+ Properties publicProperties = SpringCompatibilityTestSupport.copyConfigProperties(publicConfig);
+ publicProperties.setProperty("public.key", "from-public-xml-updated");
+ SpringCompatibilityTestSupport.applyConfigChange(publicConfig, "TEST1.apollo", publicProperties);
+
+ Config yamlConfig = ConfigService.getConfig("application.yaml");
+ Properties yamlProperties = SpringCompatibilityTestSupport.copyConfigProperties(yamlConfig);
+ yamlProperties.setProperty("yaml.marker", "from-yaml-xml-updated");
+ SpringCompatibilityTestSupport.applyConfigChange(yamlConfig, "application.yaml", yamlProperties);
+
+ SpringCompatibilityTestSupport.waitForCondition("xml timeout should be updated",
+ () -> xmlBean.getTimeout() == 5199);
+ SpringCompatibilityTestSupport.waitForCondition("xml batch should be updated",
+ () -> xmlBean.getBatch() == 61);
+ SpringCompatibilityTestSupport.waitForCondition("xml public key should be updated",
+ () -> "from-public-xml-updated".equals(xmlBean.getPublicKey()));
+ SpringCompatibilityTestSupport.waitForCondition("xml yaml marker should be updated",
+ () -> "from-yaml-xml-updated".equals(xmlBean.getYamlMarker()));
+ }
+}
diff --git a/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-100004459-application.properties b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-100004459-application.properties
new file mode 100644
index 00000000..cef7d1fe
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-100004459-application.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+compat.origin=from-another-app
diff --git a/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-TEST1.apollo.properties b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-TEST1.apollo.properties
new file mode 100644
index 00000000..cab0be75
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-TEST1.apollo.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+public.key=from-public
diff --git a/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-application.properties b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-application.properties
new file mode 100644
index 00000000..f6ce58f4
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-application.properties
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+compat.timeout=5001
+compat.xml.timeout=5099
+compat.xml.batch=51
+jsonBeanProperty=[{"someString":"alpha","someInt":11},{"someString":"beta","someInt":22}]
diff --git a/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-application.yaml.properties b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-application.yaml.properties
new file mode 100644
index 00000000..ac64a146
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-application.yaml.properties
@@ -0,0 +1,16 @@
+#
+# 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.
+#
+content=yaml:\n marker: from-yaml\n
diff --git a/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/spring/apollo-context.xml b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/spring/apollo-context.xml
new file mode 100644
index 00000000..d0e44792
--- /dev/null
+++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/spring/apollo-context.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apollo-compat-tests/pom.xml b/apollo-compat-tests/pom.xml
new file mode 100644
index 00000000..aea08501
--- /dev/null
+++ b/apollo-compat-tests/pom.xml
@@ -0,0 +1,59 @@
+
+
+
+
+ com.ctrip.framework.apollo
+ apollo-java
+ ${revision}
+ ../pom.xml
+
+ 4.0.0
+
+ apollo-compat-tests
+ Apollo Compatibility Tests
+ pom
+
+
+ apollo-api-compat-it
+ apollo-spring-compat-it
+ apollo-spring-boot-compat-it
+
+
+
+ true
+ true
+ 3.2.5
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${compat.surefire.version}
+
+ false
+
+
+
+
+
+
diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java
index cb9b7400..d4f63852 100644
--- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java
+++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java
@@ -17,15 +17,18 @@
package com.ctrip.framework.apollo.mockserver;
import com.ctrip.framework.apollo.build.ApolloInjector;
+import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.core.ApolloClientSystemConsts;
import com.ctrip.framework.apollo.core.dto.ApolloConfig;
import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
import com.ctrip.framework.apollo.core.utils.ResourceUtils;
import com.ctrip.framework.apollo.internals.ConfigServiceLocator;
import com.ctrip.framework.apollo.internals.LocalFileConfigRepository;
+import com.ctrip.framework.apollo.internals.RemoteConfigLongPollService;
import com.ctrip.framework.apollo.util.ConfigUtil;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import okhttp3.mockwebserver.Dispatcher;
@@ -45,6 +48,7 @@
import java.util.Properties;
import java.util.Set;
import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
public class ApolloTestingServer implements AutoCloseable {
@@ -52,8 +56,10 @@ public class ApolloTestingServer implements AutoCloseable {
private static final Type notificationType = new TypeToken>() {
}.getType();
- private static String someAppId = "someAppId";
+ private static final String DEFAULT_APP_ID = "someAppId";
private static Method CONFIG_SERVICE_LOCATOR_CLEAR;
+ private static Method CONFIG_SERVICE_RESET;
+ private static Method REMOTE_CONFIG_LONG_POLL_STOP;
private static ConfigServiceLocator CONFIG_SERVICE_LOCATOR;
private static ConfigUtil CONFIG_UTIL;
@@ -62,8 +68,10 @@ public class ApolloTestingServer implements AutoCloseable {
private static ResourceUtils RESOURCES_UTILS;
private static final Gson GSON = new Gson();
- private final Map> addedOrModifiedPropertiesOfNamespace = Maps.newConcurrentMap();
- private final Map> deletedKeysOfNamespace = Maps.newConcurrentMap();
+ private final Map>> addedOrModifiedPropertiesOfAppAndNamespace =
+ Maps.newConcurrentMap();
+ private final Map>> deletedKeysOfAppAndNamespace =
+ Maps.newConcurrentMap();
private MockWebServer server;
@@ -77,6 +85,11 @@ public class ApolloTestingServer implements AutoCloseable {
CONFIG_SERVICE_LOCATOR = ApolloInjector.getInstance(ConfigServiceLocator.class);
CONFIG_SERVICE_LOCATOR_CLEAR = ConfigServiceLocator.class.getDeclaredMethod("initConfigServices");
CONFIG_SERVICE_LOCATOR_CLEAR.setAccessible(true);
+ CONFIG_SERVICE_RESET = ConfigService.class.getDeclaredMethod("reset");
+ CONFIG_SERVICE_RESET.setAccessible(true);
+ REMOTE_CONFIG_LONG_POLL_STOP =
+ RemoteConfigLongPollService.class.getDeclaredMethod("stopLongPollingRefresh");
+ REMOTE_CONFIG_LONG_POLL_STOP.setAccessible(true);
CONFIG_UTIL = ApolloInjector.getInstance(ConfigUtil.class);
@@ -90,7 +103,7 @@ public class ApolloTestingServer implements AutoCloseable {
}
public void start() throws IOException {
- clear();
+ clearForStart();
server = new MockWebServer();
final Dispatcher dispatcher = new Dispatcher() {
@Override
@@ -105,7 +118,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio
String appId = pathSegments.get(1);
String cluster = pathSegments.get(2);
String namespace = pathSegments.get(3);
- return new MockResponse().setResponseCode(200).setBody(loadConfigFor(namespace));
+ return new MockResponse().setResponseCode(200).setBody(loadConfigFor(appId, namespace));
}
return new MockResponse().setResponseCode(404);
}
@@ -120,7 +133,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio
public void close() {
try {
- clear();
+ clearForClose();
server.close();
} catch (Exception e) {
logger.error("stop apollo server error", e);
@@ -137,7 +150,13 @@ public boolean isStarted() {
return started;
}
- private void clear() {
+ private void clearForStart() {
+ resetApolloClientState(false);
+ resetOverriddenProperties();
+ }
+
+ private void clearForClose() {
+ resetApolloClientState(true);
resetOverriddenProperties();
}
@@ -151,32 +170,46 @@ private void mockConfigServiceUrl(String url) {
}
}
- private String loadConfigFor(String namespace) {
- final Properties prop = loadPropertiesOfNamespace(namespace);
+ private String loadConfigFor(String appId, String namespace) {
+ final Properties prop = loadPropertiesOfNamespace(appId, namespace);
Map configurations = Maps.newHashMap();
for (String propertyName : prop.stringPropertyNames()) {
configurations.put(propertyName, prop.getProperty(propertyName));
}
- ApolloConfig apolloConfig = new ApolloConfig("someAppId", "someCluster", namespace, "someReleaseKey");
+ ApolloConfig apolloConfig = new ApolloConfig(appId, "someCluster", namespace, "someReleaseKey");
- Map mergedConfigurations = mergeOverriddenProperties(namespace, configurations);
+ Map mergedConfigurations = mergeOverriddenProperties(appId, namespace, configurations);
apolloConfig.setConfigurations(mergedConfigurations);
return GSON.toJson(apolloConfig);
}
- private Properties loadPropertiesOfNamespace(String namespace) {
+ private Properties loadPropertiesOfNamespace(String appId, String namespace) {
+ String appSpecificFilename = String.format("mockdata-%s-%s.properties", appId, namespace);
+ Properties appSpecific = loadPropertiesFromResource(appSpecificFilename, appId, namespace);
+ if (appSpecific != null) {
+ return appSpecific;
+ }
+
String filename = String.format("mockdata-%s.properties", namespace);
- Object mockdataPropertiesExits = null;
+ Properties genericProperties = loadPropertiesFromResource(filename, appId, namespace);
+ if (genericProperties != null) {
+ return genericProperties;
+ }
+ return new LocalFileConfigRepository(appId, namespace).getConfig();
+ }
+
+ private Properties loadPropertiesFromResource(String filename, String appId, String namespace) {
+ Object mockdataPropertiesExists = null;
try {
- mockdataPropertiesExits = RESOURCES_UTILS_CLEAR.invoke(RESOURCES_UTILS, filename);
+ mockdataPropertiesExists = RESOURCES_UTILS_CLEAR.invoke(RESOURCES_UTILS, filename);
} catch (IllegalAccessException | InvocationTargetException e) {
logger.error("invoke resources util locator clear failed.", e);
}
- if (!Objects.isNull(mockdataPropertiesExits)) {
- logger.debug("load {} from {}", namespace, filename);
+ if (!Objects.isNull(mockdataPropertiesExists)) {
+ logger.debug("load appId [{}] namespace [{}] from {}", appId, namespace, filename);
return ResourceUtils.readConfigFile(filename, new Properties());
}
- return new LocalFileConfigRepository(someAppId, namespace).getConfig();
+ return null;
}
private String mockLongPollBody(String notificationsStr) {
@@ -192,11 +225,16 @@ private String mockLongPollBody(String notificationsStr) {
/**
* 合并用户对namespace的修改
*/
- private Map mergeOverriddenProperties(String namespace, Map configurations) {
- if (addedOrModifiedPropertiesOfNamespace.containsKey(namespace)) {
+ private Map mergeOverriddenProperties(String appId, String namespace,
+ Map configurations) {
+ Map> addedOrModifiedPropertiesOfNamespace =
+ addedOrModifiedPropertiesOfAppAndNamespace.get(appId);
+ if (addedOrModifiedPropertiesOfNamespace != null
+ && addedOrModifiedPropertiesOfNamespace.containsKey(namespace)) {
configurations.putAll(addedOrModifiedPropertiesOfNamespace.get(namespace));
}
- if (deletedKeysOfNamespace.containsKey(namespace)) {
+ Map> deletedKeysOfNamespace = deletedKeysOfAppAndNamespace.get(appId);
+ if (deletedKeysOfNamespace != null && deletedKeysOfNamespace.containsKey(namespace)) {
for (String k : deletedKeysOfNamespace.get(namespace)) {
configurations.remove(k);
}
@@ -208,33 +246,89 @@ private Map mergeOverriddenProperties(String namespace, Map> addedOrModifiedPropertiesOfNamespace =
+ addedOrModifiedPropertiesOfAppAndNamespace.computeIfAbsent(appId, key -> Maps.newConcurrentMap());
if (addedOrModifiedPropertiesOfNamespace.containsKey(namespace)) {
addedOrModifiedPropertiesOfNamespace.get(namespace).put(someKey, someValue);
- } else {
- Map m = Maps.newConcurrentMap();
- m.put(someKey, someValue);
- addedOrModifiedPropertiesOfNamespace.put(namespace, m);
+ return;
}
+ Map properties = Maps.newConcurrentMap();
+ properties.put(someKey, someValue);
+ addedOrModifiedPropertiesOfNamespace.put(namespace, properties);
}
/**
* Delete existed property
*/
public void deleteProperty(String namespace, String someKey) {
+ deleteProperty(DEFAULT_APP_ID, namespace, someKey);
+ }
+
+ /**
+ * Delete existed property for the specified appId and namespace.
+ */
+ public void deleteProperty(String appId, String namespace, String someKey) {
+ Map> deletedKeysOfNamespace =
+ deletedKeysOfAppAndNamespace.computeIfAbsent(appId, key -> Maps.newConcurrentMap());
if (deletedKeysOfNamespace.containsKey(namespace)) {
deletedKeysOfNamespace.get(namespace).add(someKey);
- } else {
- Set m = Sets.newConcurrentHashSet();
- m.add(someKey);
- deletedKeysOfNamespace.put(namespace, m);
+ return;
}
+ Set keys = Sets.newConcurrentHashSet();
+ keys.add(someKey);
+ deletedKeysOfNamespace.put(namespace, keys);
}
/**
* reset overridden properties
*/
public void resetOverriddenProperties() {
- addedOrModifiedPropertiesOfNamespace.clear();
- deletedKeysOfNamespace.clear();
+ addedOrModifiedPropertiesOfAppAndNamespace.clear();
+ deletedKeysOfAppAndNamespace.clear();
+ }
+
+ private void resetApolloClientState(boolean stopLongPolling) {
+ try {
+ RemoteConfigLongPollService longPollService =
+ ApolloInjector.getInstance(RemoteConfigLongPollService.class);
+ if (stopLongPolling) {
+ REMOTE_CONFIG_LONG_POLL_STOP.invoke(longPollService);
+ } else {
+ prepareLongPollingService();
+ }
+ clearLongPollingState(longPollService);
+ CONFIG_SERVICE_RESET.invoke(null);
+ } catch (Throwable ex) {
+ logger.warn("reset apollo client state failed.", ex);
+ }
+ }
+
+ private static void prepareLongPollingService() throws Exception {
+ RemoteConfigLongPollService longPollService =
+ ApolloInjector.getInstance(RemoteConfigLongPollService.class);
+ AtomicBoolean stopped = (AtomicBoolean) getLongPollField(longPollService, "m_longPollingStopped");
+ stopped.set(false);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void clearLongPollingState(RemoteConfigLongPollService longPollService) throws Exception {
+ ((Map) getLongPollField(longPollService, "m_longPollStarted")).clear();
+ ((Map) getLongPollField(longPollService, "m_longPollNamespaces")).clear();
+ ((Table) getLongPollField(longPollService, "m_notifications")).clear();
+ ((Map) getLongPollField(longPollService, "m_remoteNotificationMessages")).clear();
+ }
+
+ private static Object getLongPollField(RemoteConfigLongPollService longPollService, String fieldName)
+ throws Exception {
+ java.lang.reflect.Field field = RemoteConfigLongPollService.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return field.get(longPollService);
}
}
diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/EmbeddedApollo.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/EmbeddedApollo.java
index 1f045843..bb502aaf 100644
--- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/EmbeddedApollo.java
+++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/EmbeddedApollo.java
@@ -43,6 +43,13 @@ public void addOrModifyProperty(String namespace, String someKey, String someVal
apollo.addOrModifyProperty(namespace, someKey, someValue);
}
+ /**
+ * Add new property or update existed property for the specified appId and namespace.
+ */
+ public void addOrModifyProperty(String appId, String namespace, String someKey, String someValue) {
+ apollo.addOrModifyProperty(appId, namespace, someKey, someValue);
+ }
+
/**
* Delete existed property
*/
@@ -50,6 +57,13 @@ public void deleteProperty(String namespace, String someKey) {
apollo.deleteProperty(namespace, someKey);
}
+ /**
+ * Delete existed property for the specified appId and namespace.
+ */
+ public void deleteProperty(String appId, String namespace, String someKey) {
+ apollo.deleteProperty(appId, namespace, someKey);
+ }
+
/**
* reset overridden properties
*/
diff --git a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ApolloMockServerApiTest.java b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ApolloMockServerApiTest.java
index b1290606..24e7b428 100644
--- a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ApolloMockServerApiTest.java
+++ b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ApolloMockServerApiTest.java
@@ -146,4 +146,39 @@ public void onChange(ConfigChangeEvent changeEvent) {
assertNull(otherConfig.getProperty("key6", null));
assertEquals(0, changes.availablePermits());
}
+
+ @Test
+ public void testUpdatePropertiesWithDifferentAppIds() throws Exception {
+ String appIdA = "appIdA";
+ String appIdB = "appIdB";
+ String updatedValue = "value-only-for-app-b";
+
+ Config configA = ConfigService.getConfig(appIdA, anotherNamespace);
+ Config configB = ConfigService.getConfig(appIdB, anotherNamespace);
+
+ assertEquals("otherValue1", configA.getProperty("key1", null));
+ assertEquals("otherValue1", configB.getProperty("key1", null));
+
+ embeddedApollo.addOrModifyProperty(appIdB, anotherNamespace, "key1", updatedValue);
+
+ long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(5);
+ while (System.currentTimeMillis() < deadline
+ && !updatedValue.equals(configB.getProperty("key1", null))) {
+ Thread.sleep(100);
+ }
+
+ assertEquals("otherValue1", configA.getProperty("key1", null));
+ assertEquals(updatedValue, configB.getProperty("key1", null));
+
+ embeddedApollo.deleteProperty(appIdB, anotherNamespace, "key1");
+
+ deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(5);
+ while (System.currentTimeMillis() < deadline
+ && configB.getProperty("key1", null) != null) {
+ Thread.sleep(100);
+ }
+
+ assertNull(configB.getProperty("key1", null));
+ assertEquals("otherValue1", configA.getProperty("key1", null));
+ }
}
diff --git a/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiMockIntegrationTest.java b/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiMockIntegrationTest.java
new file mode 100644
index 00000000..b12a98de
--- /dev/null
+++ b/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiMockIntegrationTest.java
@@ -0,0 +1,257 @@
+/*
+ * 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.openapi.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.ctrip.framework.apollo.openapi.dto.OpenAppDTO;
+import com.ctrip.framework.apollo.openapi.dto.OpenCreateAppDTO;
+import com.ctrip.framework.apollo.openapi.dto.OpenNamespaceDTO;
+import com.ctrip.framework.apollo.openapi.dto.OpenOrganizationDto;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Mock integration tests that verify OpenAPI client request/response chain.
+ */
+public class ApolloOpenApiMockIntegrationTest {
+
+ private HttpServer server;
+ private MockPortalHandler handler;
+
+ @Before
+ public void setUp() throws Exception {
+ handler = new MockPortalHandler();
+ server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+ server.createContext("/openapi/v1/", handler);
+ server.start();
+ }
+
+ @After
+ public void tearDown() {
+ if (server != null) {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ public void shouldCallFindAppsWithAuthorizationHeaderAndQuery() throws Exception {
+ handler.mock("GET", "/openapi/v1/apps", 200,
+ "[{\"appId\":\"SampleApp\",\"name\":\"SampleApp\",\"ownerName\":\"apollo\"}]");
+ String token = "ci-openapi-token";
+ ApolloOpenApiClient client = newClient(token);
+
+ List apps = client.getAppsByIds(Collections.singletonList("SampleApp"));
+ CapturedRequest request = handler.awaitRequest(5, TimeUnit.SECONDS);
+
+ assertNotNull(apps);
+ assertEquals(1, apps.size());
+ assertEquals("SampleApp", apps.get(0).getAppId());
+ assertEquals("GET", request.method);
+ assertEquals("/openapi/v1/apps", request.path);
+ assertEquals("appIds=SampleApp", request.query);
+ assertEquals(token, request.authorization);
+ }
+
+ @Test
+ public void shouldSerializeCreateAppRequestBody() throws Exception {
+ handler.mock("POST", "/openapi/v1/apps", 200, "");
+ ApolloOpenApiClient client = newClient("create-token");
+
+ OpenAppDTO app = new OpenAppDTO();
+ app.setAppId("SampleApp");
+ app.setName("SampleApp");
+ app.setOwnerName("apollo");
+
+ OpenCreateAppDTO request = new OpenCreateAppDTO();
+ request.setApp(app);
+ request.setAdmins(Collections.singleton("apollo"));
+ request.setAssignAppRoleToSelf(true);
+ client.createApp(request);
+
+ CapturedRequest capturedRequest = handler.awaitRequest(5, TimeUnit.SECONDS);
+ assertEquals("POST", capturedRequest.method);
+ assertEquals("/openapi/v1/apps", capturedRequest.path);
+ assertEquals("create-token", capturedRequest.authorization);
+ assertTrue(capturedRequest.body.contains("\"appId\":\"SampleApp\""));
+ assertTrue(capturedRequest.body.contains("\"assignAppRoleToSelf\":true"));
+ assertTrue(capturedRequest.body.contains("\"admins\":[\"apollo\"]"));
+ }
+
+ @Test
+ public void shouldUseDefaultClusterAndNamespace() throws Exception {
+ handler.mock("GET", "/openapi/v1/envs/DEV/apps/SampleApp/clusters/default/namespaces/application",
+ 200,
+ "{\"appId\":\"SampleApp\",\"clusterName\":\"default\",\"namespaceName\":\"application\"}");
+ ApolloOpenApiClient client = newClient("namespace-token");
+
+ OpenNamespaceDTO namespaceDTO = client.getNamespace("SampleApp", "DEV", null, null, true);
+ CapturedRequest request = handler.awaitRequest(5, TimeUnit.SECONDS);
+
+ assertNotNull(namespaceDTO);
+ assertEquals("SampleApp", namespaceDTO.getAppId());
+ assertEquals("default", namespaceDTO.getClusterName());
+ assertEquals("application", namespaceDTO.getNamespaceName());
+ assertEquals("GET", request.method);
+ assertEquals("/openapi/v1/envs/DEV/apps/SampleApp/clusters/default/namespaces/application",
+ request.path);
+ assertEquals("fillItemDetail=true", request.query);
+ assertEquals("namespace-token", request.authorization);
+ }
+
+ @Test
+ public void shouldParseOrganizations() throws Exception {
+ handler.mock("GET", "/openapi/v1/organizations", 200,
+ "[{\"orgId\":\"100001\",\"orgName\":\"Apollo Team\"}]");
+ ApolloOpenApiClient client = newClient("org-token");
+
+ List organizations = client.getOrganizations();
+ CapturedRequest request = handler.awaitRequest(5, TimeUnit.SECONDS);
+
+ assertNotNull(organizations);
+ assertEquals(1, organizations.size());
+ assertEquals("100001", organizations.get(0).getOrgId());
+ assertEquals("Apollo Team", organizations.get(0).getOrgName());
+ assertEquals("GET", request.method);
+ assertEquals("/openapi/v1/organizations", request.path);
+ assertEquals("org-token", request.authorization);
+ }
+
+ @Test
+ public void shouldWrapServerErrorsAsRuntimeException() {
+ handler.mock("GET", "/openapi/v1/apps", 500, "internal error");
+ ApolloOpenApiClient client = newClient("error-token");
+
+ try {
+ client.getAllApps();
+ fail("Expected RuntimeException to be thrown");
+ } catch (RuntimeException ex) {
+ assertTrue(ex.getMessage().contains("Load app information"));
+ assertNotNull(ex.getCause());
+ }
+ }
+
+ private ApolloOpenApiClient newClient(String token) {
+ return ApolloOpenApiClient.newBuilder()
+ .withPortalUrl(String.format("http://127.0.0.1:%d", server.getAddress().getPort()))
+ .withToken(token)
+ .build();
+ }
+
+ private static class MockPortalHandler implements HttpHandler {
+
+ private final Map responses = new ConcurrentHashMap<>();
+ private final BlockingQueue requests = new LinkedBlockingQueue<>();
+
+ void mock(String method, String path, int statusCode, String body) {
+ responses.put(key(method, path), new MockResponse(statusCode, body));
+ }
+
+ CapturedRequest awaitRequest(long timeout, TimeUnit unit) throws InterruptedException {
+ CapturedRequest request = requests.poll(timeout, unit);
+ assertNotNull(request);
+ return request;
+ }
+
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ String method = exchange.getRequestMethod();
+ String path = exchange.getRequestURI().getPath();
+ String query = exchange.getRequestURI().getQuery();
+ String authorization = exchange.getRequestHeaders().getFirst("Authorization");
+ String requestBody = readRequestBody(exchange.getRequestBody());
+ requests.offer(new CapturedRequest(method, path, query, authorization, requestBody));
+
+ MockResponse response = responses.get(key(method, path));
+ if (response == null) {
+ response = new MockResponse(404, "");
+ }
+ byte[] responseBody = response.body.getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type", "application/json;charset=UTF-8");
+ exchange.sendResponseHeaders(response.statusCode, responseBody.length);
+ try (OutputStream outputStream = exchange.getResponseBody()) {
+ outputStream.write(responseBody);
+ }
+ }
+
+ private String readRequestBody(InputStream inputStream) throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ byte[] buffer = new byte[256];
+ int read;
+ while ((read = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, read);
+ }
+ return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
+ }
+
+ private String key(String method, String path) {
+ return method + " " + path;
+ }
+ }
+
+ private static class MockResponse {
+ private final int statusCode;
+ private final String body;
+
+ private MockResponse(int statusCode, String body) {
+ this.statusCode = statusCode;
+ this.body = body;
+ }
+ }
+
+ private static class CapturedRequest {
+
+ private final String method;
+ private final String path;
+ private final String query;
+ private final String authorization;
+ private final String body;
+
+ private CapturedRequest(
+ String method,
+ String path,
+ String query,
+ String authorization,
+ String body) {
+ this.method = method;
+ this.path = path;
+ this.query = query;
+ this.authorization = authorization;
+ this.body = body;
+ }
+ }
+}
diff --git a/apollo-plugin/apollo-plugin-log4j2/pom.xml b/apollo-plugin/apollo-plugin-log4j2/pom.xml
index b6461d67..b775941f 100644
--- a/apollo-plugin/apollo-plugin-log4j2/pom.xml
+++ b/apollo-plugin/apollo-plugin-log4j2/pom.xml
@@ -40,5 +40,10 @@
log4j-core
provided
+
+ com.ctrip.framework.apollo
+ apollo-mockserver
+ test
+
diff --git a/apollo-plugin/apollo-plugin-log4j2/src/test/java/com/ctrip/framework/apollo/plugin/log4j2/ApolloClientConfigurationFactoryTest.java b/apollo-plugin/apollo-plugin-log4j2/src/test/java/com/ctrip/framework/apollo/plugin/log4j2/ApolloClientConfigurationFactoryTest.java
new file mode 100644
index 00000000..a7997b62
--- /dev/null
+++ b/apollo-plugin/apollo-plugin-log4j2/src/test/java/com/ctrip/framework/apollo/plugin/log4j2/ApolloClientConfigurationFactoryTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.plugin.log4j2;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import com.ctrip.framework.apollo.Config;
+import com.ctrip.framework.apollo.ConfigFile;
+import com.ctrip.framework.apollo.ConfigFileChangeListener;
+import com.ctrip.framework.apollo.ConfigService;
+import com.ctrip.framework.apollo.build.ApolloInjector;
+import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
+import com.ctrip.framework.apollo.enums.ConfigSourceType;
+import com.ctrip.framework.apollo.spi.ConfigFactory;
+import com.ctrip.framework.apollo.spi.ConfigFactoryManager;
+import com.ctrip.framework.apollo.spi.ConfigRegistry;
+import com.ctrip.framework.apollo.internals.ConfigManager;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Map;
+import com.google.common.collect.Table;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class ApolloClientConfigurationFactoryTest {
+
+ private static final String ORIGINAL_APP_ID = System.getProperty("app.id");
+ private static final String ORIGINAL_ENV = System.getProperty("env");
+ private static final String ORIGINAL_ENABLED = System.getProperty("apollo.log4j2.enabled");
+ private static final String LOG4J2_NAMESPACE = "log4j2.xml";
+
+ static {
+ System.setProperty("app.id", "someAppId");
+ System.setProperty("env", "local");
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ resetConfigService();
+ clearApolloCaches();
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ restoreOrClear("app.id", ORIGINAL_APP_ID);
+ restoreOrClear("env", ORIGINAL_ENV);
+ restoreOrClear("apollo.log4j2.enabled", ORIGINAL_ENABLED);
+ resetConfigService();
+ clearApolloCaches();
+ }
+
+ @Test
+ public void test1ShouldReturnNullWhenPluginIsDisabled() {
+ System.setProperty("apollo.log4j2.enabled", "false");
+
+ ApolloClientConfigurationFactory factory = new ApolloClientConfigurationFactory();
+ Configuration configuration = factory.getConfiguration(new LoggerContext("disabled"), null);
+
+ assertNull(configuration);
+ }
+
+ @Test
+ public void test2ShouldReturnNullWhenNoLog4j2NamespaceContent() throws Exception {
+ System.setProperty("apollo.log4j2.enabled", "true");
+ registerConfigFile(null);
+ ConfigFile configFile = ConfigService.getConfigFile("log4j2", ConfigFileFormat.XML);
+ assertNotNull(configFile);
+ assertNull(configFile.getContent());
+
+ ApolloClientConfigurationFactory factory = new ApolloClientConfigurationFactory();
+ Configuration configuration = factory.getConfiguration(new LoggerContext("empty"), null);
+
+ assertNull(configuration);
+ }
+
+ @Test
+ public void test3ShouldBuildXmlConfigurationWhenContentExists() throws Exception {
+ System.setProperty("apollo.log4j2.enabled", "true");
+ registerConfigFile(
+ "");
+
+ ApolloClientConfigurationFactory factory = new ApolloClientConfigurationFactory();
+ Configuration configuration = factory.getConfiguration(new LoggerContext("apollo"), null);
+
+ assertNotNull(configuration);
+ }
+
+ private static void registerConfigFile(String content) throws Exception {
+ ConfigFactory factory = new StaticConfigFactory(content);
+ Method setFactoryMethod =
+ ConfigService.class.getDeclaredMethod("setConfigFactory", String.class, ConfigFactory.class);
+ setFactoryMethod.setAccessible(true);
+ setFactoryMethod.invoke(null, LOG4J2_NAMESPACE, factory);
+ }
+
+ private static void resetConfigService() throws Exception {
+ Method resetMethod = ConfigService.class.getDeclaredMethod("reset");
+ resetMethod.setAccessible(true);
+ resetMethod.invoke(null);
+ }
+
+ private static void clearApolloCaches() throws Exception {
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configs");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configLocks");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFiles");
+ clearField(ApolloInjector.getInstance(ConfigManager.class), "m_configFileLocks");
+ clearField(ApolloInjector.getInstance(ConfigFactoryManager.class), "m_factories");
+ clearField(ApolloInjector.getInstance(ConfigRegistry.class), "m_instances");
+ }
+
+ private static void clearField(Object instance, String fieldName) throws Exception {
+ Field field = instance.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Object value = field.get(instance);
+ if (value == null) {
+ return;
+ }
+ if (value instanceof Map) {
+ ((Map, ?>) value).clear();
+ return;
+ }
+ if (value instanceof Table) {
+ ((Table, ?, ?>) value).clear();
+ }
+ }
+
+ private static void restoreOrClear(String key, String originalValue) {
+ if (originalValue == null) {
+ System.clearProperty(key);
+ return;
+ }
+ System.setProperty(key, originalValue);
+ }
+
+ private static class StaticConfigFactory implements ConfigFactory {
+
+ private final String content;
+
+ private StaticConfigFactory(String content) {
+ this.content = content;
+ }
+
+ @Override
+ public Config create(String namespace) {
+ return null;
+ }
+
+ @Override
+ public Config create(String appId, String namespace) {
+ return null;
+ }
+
+ @Override
+ public ConfigFile createConfigFile(String namespace, ConfigFileFormat configFileFormat) {
+ return new StaticConfigFile(null, namespace, configFileFormat, content);
+ }
+
+ @Override
+ public ConfigFile createConfigFile(String appId, String namespace,
+ ConfigFileFormat configFileFormat) {
+ return new StaticConfigFile(appId, namespace, configFileFormat, content);
+ }
+ }
+
+ private static class StaticConfigFile implements ConfigFile {
+
+ private final String appId;
+ private final String namespace;
+ private final ConfigFileFormat format;
+ private final String content;
+
+ private StaticConfigFile(String appId, String namespace, ConfigFileFormat format, String content) {
+ this.appId = appId;
+ this.namespace = namespace;
+ this.format = format;
+ this.content = content;
+ }
+
+ @Override
+ public String getContent() {
+ return content;
+ }
+
+ @Override
+ public boolean hasContent() {
+ return content != null && !content.isEmpty();
+ }
+
+ @Override
+ public String getAppId() {
+ return appId;
+ }
+
+ @Override
+ public String getNamespace() {
+ return namespace;
+ }
+
+ @Override
+ public ConfigFileFormat getConfigFileFormat() {
+ return format;
+ }
+
+ @Override
+ public void addChangeListener(ConfigFileChangeListener listener) {
+ }
+
+ @Override
+ public boolean removeChangeListener(ConfigFileChangeListener listener) {
+ return false;
+ }
+
+ @Override
+ public ConfigSourceType getSourceType() {
+ return ConfigSourceType.REMOTE;
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index f1061ce2..4ea6a708 100644
--- a/pom.xml
+++ b/pom.xml
@@ -98,6 +98,11 @@
apollo-client
${project.version}
+
+ com.ctrip.framework.apollo
+ apollo-mockserver
+ ${project.version}
+
org.slf4j
@@ -467,4 +472,4 @@
${snapshots.repo}
-
\ No newline at end of file
+