From 20eb72dcf71b969f8ff31976a4233d0c9b1e0b6a Mon Sep 17 00:00:00 2001 From: Jason Song Date: Wed, 18 Feb 2026 21:56:53 +0800 Subject: [PATCH 1/6] test: overhaul automated compatibility coverage --- .github/workflows/build.yml | 172 ++++++- apollo-client-config-data/pom.xml | 10 + .../ApolloClientPropertiesFactory.java | 7 - ...ClientLongPollingExtensionInitializer.java | 6 +- .../webclient/ApolloWebClientHttpClient.java | 33 +- ...polloClientWebClientCustomizerFactory.java | 9 +- ...ClientConfigDataAutoConfigurationTest.java | 72 +++ ...oClientExtensionInitializeFactoryTest.java | 104 ++++ .../ApolloWebClientHttpClientTest.java | 119 +++++ ...ApolloConfigDataLoaderInitializerTest.java | 124 +++++ .../importer/ApolloConfigDataLoaderTest.java | 222 ++++++++ .../ApolloConfigDataLocationResolverTest.java | 72 +++ .../ConfigDataIntegrationTest.java | 286 +++++++++++ ...DeferredLoggerApplicationListenerTest.java | 94 ++++ ...tstrapRegistryHelperCompatibilityTest.java | 90 ++++ .../mockdata-TEST1.apollo.properties | 2 + .../resources/mockdata-application.properties | 6 + .../mockdata-application.yaml.properties | 1 + .../annotation/ApolloAnnotationProcessor.java | 3 +- .../framework/apollo/BaseIntegrationTest.java | 10 + .../framework/apollo/MockedConfigService.java | 41 +- .../integration/ConfigIntegrationTest.java | 247 ++++++++- .../spring/JavaConfigAnnotationTest.java | 175 +++++++ .../apollo-api-compat-it/pom.xml | 64 +++ .../api/ApolloApiCompatibilityTest.java | 220 ++++++++ .../mockdata-100004459-application.properties | 2 + .../mockdata-TEST1.apollo.properties | 2 + .../resources/mockdata-application.properties | 2 + .../mockdata-application.yaml.properties | 1 + .../mockdata-datasources.xml.properties | 1 + .../apollo-spring-boot-compat-it/pom.xml | 100 ++++ .../ApolloSpringBootCompatibilityTest.java | 482 ++++++++++++++++++ .../mockdata-100004459-application.properties | 1 + .../mockdata-TEST1.apollo.properties | 1 + .../resources/mockdata-application.properties | 8 + .../mockdata-application.yaml.properties | 1 + .../apollo-spring-compat-it/pom.xml | 96 ++++ .../SpringAnnotationCompatibilityTest.java | 287 +++++++++++ .../SpringApolloEventListenerProbe.java | 40 ++ .../SpringCompatibilityTestSupport.java | 131 +++++ .../apollo/compat/spring/SpringXmlBean.java | 57 +++ .../spring/SpringXmlCompatibilityTest.java | 88 ++++ .../mockdata-100004459-application.properties | 1 + .../mockdata-TEST1.apollo.properties | 1 + .../resources/mockdata-application.properties | 4 + .../mockdata-application.yaml.properties | 1 + .../test/resources/spring/apollo-context.xml | 18 + apollo-compat-tests/pom.xml | 59 +++ .../mockserver/ApolloTestingServer.java | 154 ++++-- .../apollo/mockserver/EmbeddedApollo.java | 14 + .../mockserver/ApolloMockServerApiTest.java | 35 ++ .../ApolloOpenApiMockIntegrationTest.java | 250 +++++++++ apollo-plugin/apollo-plugin-log4j2/pom.xml | 5 + .../ApolloClientConfigurationFactoryTest.java | 239 +++++++++ pom.xml | 7 +- 55 files changed, 4189 insertions(+), 88 deletions(-) create mode 100644 apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/ApolloClientConfigDataAutoConfigurationTest.java create mode 100644 apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientExtensionInitializeFactoryTest.java create mode 100644 apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClientTest.java create mode 100644 apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderInitializerTest.java create mode 100644 apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderTest.java create mode 100644 apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLocationResolverTest.java create mode 100644 apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/integration/ConfigDataIntegrationTest.java create mode 100644 apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/listener/ApolloDeferredLoggerApplicationListenerTest.java create mode 100644 apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/util/BootstrapRegistryHelperCompatibilityTest.java create mode 100644 apollo-client-config-data/src/test/resources/mockdata-TEST1.apollo.properties create mode 100644 apollo-client-config-data/src/test/resources/mockdata-application.properties create mode 100644 apollo-client-config-data/src/test/resources/mockdata-application.yaml.properties create mode 100644 apollo-compat-tests/apollo-api-compat-it/pom.xml create mode 100644 apollo-compat-tests/apollo-api-compat-it/src/test/java/com/ctrip/framework/apollo/compat/api/ApolloApiCompatibilityTest.java create mode 100644 apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-100004459-application.properties create mode 100644 apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-TEST1.apollo.properties create mode 100644 apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-application.properties create mode 100644 apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-application.yaml.properties create mode 100644 apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-datasources.xml.properties create mode 100644 apollo-compat-tests/apollo-spring-boot-compat-it/pom.xml create mode 100644 apollo-compat-tests/apollo-spring-boot-compat-it/src/test/java/com/ctrip/framework/apollo/compat/springboot/ApolloSpringBootCompatibilityTest.java create mode 100644 apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-100004459-application.properties create mode 100644 apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-TEST1.apollo.properties create mode 100644 apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-application.properties create mode 100644 apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-application.yaml.properties create mode 100644 apollo-compat-tests/apollo-spring-compat-it/pom.xml create mode 100644 apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringAnnotationCompatibilityTest.java create mode 100644 apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringApolloEventListenerProbe.java create mode 100644 apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringCompatibilityTestSupport.java create mode 100644 apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringXmlBean.java create mode 100644 apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringXmlCompatibilityTest.java create mode 100644 apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-100004459-application.properties create mode 100644 apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-TEST1.apollo.properties create mode 100644 apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-application.properties create mode 100644 apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-application.yaml.properties create mode 100644 apollo-compat-tests/apollo-spring-compat-it/src/test/resources/spring/apollo-context.xml create mode 100644 apollo-compat-tests/pom.xml create mode 100644 apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiMockIntegrationTest.java create mode 100644 apollo-plugin/apollo-plugin-log4j2/src/test/java/com/ctrip/framework/apollo/plugin/log4j2/ApolloClientConfigurationFactoryTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa315c00..b24a0757 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# This workflow will build a Java project with Maven -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven name: build @@ -25,40 +23,170 @@ on: branches: [ main ] jobs: - build: + compile-matrix: runs-on: ubuntu-latest strategy: matrix: jdk: [8, 11, 17] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: + distribution: temurin java-version: ${{ matrix.jdk }} - name: Cache Maven packages uses: actions/cache@v4 with: path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + key: ${{ runner.os }}-maven-compile-${{ matrix.jdk }}-${{ hashFiles('**/pom.xml') }} restore-keys: | + ${{ runner.os }}-maven-compile-${{ matrix.jdk }}- ${{ runner.os }}-maven- - - name: JDK 8 - if: matrix.jdk == '8' - uses: nick-fields/retry@v3 - with: - timeout_minutes: 3 - max_attempts: 3 - retry_wait_seconds: 1 - command: mvn -B clean package -P travis jacoco:report -Dmaven.gitcommitid.skip=true - - name: JDK 11 - if: matrix.jdk == '11' - run: mvn -B clean compile -Dmaven.gitcommitid.skip=true - - name: JDK 17 - if: matrix.jdk == '17' + - name: Compile run: mvn -B clean compile -Dmaven.gitcommitid.skip=true + + unit-integration-pr: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-unit-integration-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven-unit-integration- + ${{ runner.os }}-maven- + - name: Run unit and integration tests + run: | + mvn -B clean test -P travis jacoco:report \ + -Dmaven.gitcommitid.skip=true - name: Upload coverage to Codecov - if: matrix.jdk == '8' - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 + with: + files: ${{ github.workspace }}/apollo-*/target/site/jacoco/jacoco.xml + + compat-api: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 8 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-compat-api-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven-compat-api- + ${{ runner.os }}-maven- + - name: Build local artifacts for api compatibility + run: | + mvn -B -pl apollo-core,apollo-client,apollo-mockserver -am \ + -DskipTests install -Dmaven.gitcommitid.skip=true + - name: Run api compatibility tests + run: | + mvn -B -f apollo-compat-tests/pom.xml -pl apollo-api-compat-it \ + test -Dmaven.gitcommitid.skip=true + + compat-spring: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: spring-3.1.1-jdk8 + java: 8 + spring_framework: 3.1.1.RELEASE + java_version_prop: 1.8 + - name: spring-6.1-jdk17 + java: 17 + spring_framework: 6.1.18 + java_version_prop: 17 + name: compat-spring-${{ matrix.name }} + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ matrix.java }} + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-compat-spring-${{ matrix.name }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven-compat-spring-${{ matrix.name }}- + ${{ runner.os }}-maven- + - name: Build local artifacts for compat tests + run: | + mvn -B -pl apollo-core,apollo-client,apollo-mockserver -am \ + -DskipTests install -Dmaven.gitcommitid.skip=true + - name: Run spring compatibility tests + run: | + mvn -B -f apollo-compat-tests/pom.xml -pl apollo-spring-compat-it \ + -Dspring.framework.version=${{ matrix.spring_framework }} \ + -Djava.version=${{ matrix.java_version_prop }} \ + test -Dmaven.gitcommitid.skip=true + + compat-spring-boot: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: spring-boot-2.7-jdk8 + java: 8 + spring_boot: 2.7.18 + java_version_prop: 1.8 + compat_slf4j: 1.7.36 + compat_vintage: 5.7.0 + - name: spring-boot-3.3-jdk17 + java: 17 + spring_boot: 3.3.10 + java_version_prop: 17 + compat_slf4j: 2.0.17 + compat_vintage: 5.10.5 + - name: spring-boot-4.0-jdk17 + java: 17 + spring_boot: 4.0.0 + java_version_prop: 17 + compat_slf4j: 2.0.17 + compat_vintage: 6.0.1 + name: compat-spring-boot-${{ matrix.name }} + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 with: - file: ${{ github.workspace }}/apollo-*/target/site/jacoco/jacoco.xml + distribution: temurin + java-version: ${{ matrix.java }} + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-compat-spring-boot-${{ matrix.name }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven-compat-spring-boot-${{ matrix.name }}- + ${{ runner.os }}-maven- + - name: Build local artifacts for spring boot compatibility + run: | + mvn -B -pl apollo-core,apollo-client,apollo-mockserver,apollo-client-config-data -am \ + -DskipTests \ + install -Dmaven.gitcommitid.skip=true + - name: Run spring boot compatibility tests + run: | + mvn -B -f apollo-compat-tests/pom.xml -pl apollo-spring-boot-compat-it \ + -Dspring-boot.version=${{ matrix.spring_boot }} \ + -Djava.version=${{ matrix.java_version_prop }} \ + -Dcompat.slf4j.version=${{ matrix.compat_slf4j }} \ + -Dcompat.junit.vintage.version=${{ matrix.compat_vintage }} \ + test -Dmaven.gitcommitid.skip=true diff --git a/apollo-client-config-data/pom.xml b/apollo-client-config-data/pom.xml index acafc551..f56b74e4 100644 --- a/apollo-client-config-data/pom.xml +++ b/apollo-client-config-data/pom.xml @@ -73,5 +73,15 @@ + + com.ctrip.framework.apollo + apollo-mockserver + test + + + io.projectreactor.netty + reactor-netty-http + test + diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientPropertiesFactory.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientPropertiesFactory.java index cb008ae7..05b50076 100644 --- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientPropertiesFactory.java +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientPropertiesFactory.java @@ -17,7 +17,6 @@ package com.ctrip.framework.apollo.config.data.extension.initialize; import com.ctrip.framework.apollo.config.data.extension.properties.ApolloClientProperties; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; @@ -35,10 +34,4 @@ public ApolloClientProperties createApolloClientProperties( return binder.bind(PROPERTIES_PREFIX, Bindable.of(ApolloClientProperties.class), bindHandler).orElse(null); } - - public OAuth2ClientProperties createOauth2ClientProperties(Binder binder, - BindHandler bindHandler) { - return binder.bind("spring.security.oauth2.client", Bindable.of(OAuth2ClientProperties.class), - bindHandler).orElse(null); - } } diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloClientLongPollingExtensionInitializer.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloClientLongPollingExtensionInitializer.java index 03185e56..9cc534c8 100644 --- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloClientLongPollingExtensionInitializer.java +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloClientLongPollingExtensionInitializer.java @@ -23,11 +23,11 @@ import com.ctrip.framework.apollo.util.http.HttpClient; import com.ctrip.framework.foundation.internals.ServiceBootstrap; import java.util.List; +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.logging.DeferredLogFactory; -import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; import org.springframework.util.CollectionUtils; import org.springframework.web.reactive.function.client.WebClient; @@ -55,11 +55,11 @@ public void initialize(ApolloClientProperties apolloClientProperties, Binder bin .loadAllOrdered(ApolloClientWebClientCustomizerFactory.class); if (!CollectionUtils.isEmpty(factories)) { for (ApolloClientWebClientCustomizerFactory factory : factories) { - WebClientCustomizer webClientCustomizer = factory + Consumer webClientCustomizer = factory .createWebClientCustomizer(apolloClientProperties, binder, bindHandler, this.log, this.bootstrapContext); if (webClientCustomizer != null) { - webClientCustomizer.customize(webClientBuilder); + webClientCustomizer.accept(webClientBuilder); } } } diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClient.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClient.java index f36bcca4..7583d9aa 100644 --- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClient.java +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClient.java @@ -22,11 +22,13 @@ import com.ctrip.framework.apollo.util.http.HttpRequest; import com.ctrip.framework.apollo.util.http.HttpResponse; import com.google.gson.Gson; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.net.URI; import java.util.Map; import org.springframework.http.HttpStatus; import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @@ -64,15 +66,16 @@ private HttpResponse doGetInternal(HttpRequest httpRequest, Type response } } return requestHeadersSpec.exchangeToMono(clientResponse -> { - if (HttpStatus.OK.equals(clientResponse.statusCode())) { + int statusCode = this.resolveStatusCode(clientResponse); + if (HttpStatus.OK.value() == statusCode) { return clientResponse.bodyToMono(String.class) .map(body -> new HttpResponse(HttpStatus.OK.value(), gson.fromJson(body, responseType))); } - if (HttpStatus.NOT_MODIFIED.equals(clientResponse.statusCode())) { + if (HttpStatus.NOT_MODIFIED.value() == statusCode) { return Mono.just(new HttpResponse(HttpStatus.NOT_MODIFIED.value(), null)); } - return Mono.error(new ApolloConfigStatusCodeException(clientResponse.rawStatusCode(), + return Mono.error(new ApolloConfigStatusCodeException(statusCode, String.format("Get operation failed for %s", httpRequest.getUrl()))); }).block(); } @@ -82,4 +85,28 @@ public HttpResponse 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 { + Method statusCodeMethod = ClientResponse.class.getMethod("statusCode"); + Object statusCode = statusCodeMethod.invoke(clientResponse); + if (statusCode == null) { + throw new ApolloConfigException("Failed to resolve response status code: statusCode is null"); + } + // Both HttpStatus and HttpStatusCode expose value(), so resolve it reflectively. + Method valueMethod = statusCode.getClass().getMethod("value"); + Object value = valueMethod.invoke(statusCode); + return ((Number) value).intValue(); + } catch (Exception ex) { + throw new ApolloConfigException("Failed to resolve response status code", 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..02869f36 --- /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 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..a8f9c2c3 --- /dev/null +++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/integration/ConfigDataIntegrationTest.java @@ -0,0 +1,286 @@ +/* + * 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.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.Map; +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(); + } + + @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(1500, 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..0f4aa02a --- /dev/null +++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/listener/ApolloDeferredLoggerApplicationListenerTest.java @@ -0,0 +1,94 @@ +/* + * 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.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; + +import com.ctrip.framework.apollo.config.data.util.BootstrapRegistryHelper; +import com.ctrip.framework.apollo.core.utils.DeferredLogger; +import java.lang.reflect.Constructor; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.event.ApplicationContextInitializedEvent; +import org.springframework.boot.context.event.ApplicationFailedEvent; +import org.springframework.boot.context.event.ApplicationStartingEvent; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.ClassUtils; + +/** + * @author vdisk + */ +public class ApolloDeferredLoggerApplicationListenerTest { + + @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); + } +} + +/** + * @author vdisk + */ +class ApolloSpringApplicationRegisterListenerTest { + + @Test + public void testRegisterSpringApplicationToBootstrapContext() throws Exception { + Object bootstrapContext = newDefaultBootstrapContext(); + SpringApplication springApplication = new SpringApplication(Object.class); + + Constructor constructor = ApplicationStartingEvent.class.getConstructors()[0]; + ApplicationStartingEvent event = (ApplicationStartingEvent) constructor + .newInstance(bootstrapContext, springApplication, new String[0]); + + ApolloSpringApplicationRegisterListener listener = new ApolloSpringApplicationRegisterListener(); + listener.onApplicationEvent(event); + + SpringApplication registered = BootstrapRegistryHelper.get(bootstrapContext, SpringApplication.class); + assertSame(springApplication, registered); + } + + 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..2e4fdddf --- /dev/null +++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/util/BootstrapRegistryHelperCompatibilityTest.java @@ -0,0 +1,90 @@ +/* + * 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 static org.junit.Assert.assertTrue; + +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(); + Constructor constructor = ApplicationStartingEvent.class.getConstructors()[0]; + ApplicationStartingEvent event = (ApplicationStartingEvent) constructor + .newInstance(bootstrapContext, new SpringApplication(Object.class), new String[0]); + + 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()); + assertTrue(BootstrapRegistryHelper.class.getName().contains("BootstrapRegistryHelper")); + } + + 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..ba60b290 --- /dev/null +++ b/apollo-client-config-data/src/test/resources/mockdata-TEST1.apollo.properties @@ -0,0 +1,2 @@ +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..28802056 --- /dev/null +++ b/apollo-client-config-data/src/test/resources/mockdata-application.properties @@ -0,0 +1,6 @@ +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..3741f5b9 --- /dev/null +++ b/apollo-client-config-data/src/test/resources/mockdata-application.yaml.properties @@ -0,0 +1 @@ +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..364d6673 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 @@ -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 = this.environment.resolveRequiredPlaceholders(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); 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..41e6be64 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; @@ -36,7 +37,7 @@ /** * @author wxq */ -public class MockedConfigService implements AutoCloseable { +public class MockedConfigService { private static final String META_SERVER_PATH = "/services/config?.*"; @@ -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 @@ -221,8 +255,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..09c99d90 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, 300)); + } + + @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..2e2dd4ac --- /dev/null +++ b/apollo-compat-tests/apollo-api-compat-it/src/test/java/com/ctrip/framework/apollo/compat/api/ApolloApiCompatibilityTest.java @@ -0,0 +1,220 @@ +/* + * 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"); + } + + @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); + } +} 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..37926b4a --- /dev/null +++ b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-100004459-application.properties @@ -0,0 +1,2 @@ +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..10ca90dd --- /dev/null +++ b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-TEST1.apollo.properties @@ -0,0 +1,2 @@ +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..cd407ae2 --- /dev/null +++ b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-application.properties @@ -0,0 +1,2 @@ +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..b231c1bf --- /dev/null +++ b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-application.yaml.properties @@ -0,0 +1 @@ +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..d493da49 --- /dev/null +++ b/apollo-compat-tests/apollo-api-compat-it/src/test/resources/mockdata-datasources.xml.properties @@ -0,0 +1 @@ +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..79f3a1df --- /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,482 @@ +/* + * 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 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, "application", applicationProperties); + + Config publicConfig = ConfigService.getConfig("TEST1.apollo"); + Properties publicProperties = copyConfigProperties(publicConfig); + publicProperties.setProperty("public.only", "from-public-boot-updated"); + applyConfigChange(publicConfig, "TEST1.apollo", publicProperties); + + Config yamlConfig = ConfigService.getConfig("application.yaml"); + Properties yamlProperties = copyConfigProperties(yamlConfig); + yamlProperties.setProperty("yaml.marker", "boot-compat-updated"); + applyConfigChange(yamlConfig, "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"); + } + + @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 Properties copyConfigProperties(Config config) { + Properties properties = new Properties(); + for (String key : config.getPropertyNames()) { + properties.setProperty(key, config.getProperty(key, null)); + } + return properties; + } + + private static void applyConfigChange(Config config, String namespace, Properties properties) { + Assert.assertTrue(config instanceof DefaultConfig); + ((DefaultConfig) config).onRepositoryChange(namespace, 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..f7e25f11 --- /dev/null +++ b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-100004459-application.properties @@ -0,0 +1 @@ +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..947aa952 --- /dev/null +++ b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-TEST1.apollo.properties @@ -0,0 +1 @@ +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..4eb459b0 --- /dev/null +++ b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-application.properties @@ -0,0 +1,8 @@ +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..4da56148 --- /dev/null +++ b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-application.yaml.properties @@ -0,0 +1 @@ +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..4f7e0a7e --- /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.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +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 ArrayBlockingQueue(8); + + @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..3c67518e --- /dev/null +++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringCompatibilityTestSupport.java @@ -0,0 +1,131 @@ +/* + * 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"); + + static { + System.setProperty("app.id", "someAppId"); + System.setProperty("env", "local"); + } + + private SpringCompatibilityTestSupport() { + } + + static void beforeClass(EmbeddedApollo embeddedApollo) throws Exception { + 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, null)); + } + 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"); + } + + @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); + } +} 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..76439ae0 --- /dev/null +++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-100004459-application.properties @@ -0,0 +1 @@ +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..7f2a6f58 --- /dev/null +++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-TEST1.apollo.properties @@ -0,0 +1 @@ +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..9edc073b --- /dev/null +++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-application.properties @@ -0,0 +1,4 @@ +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..0ef4b9da --- /dev/null +++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-application.yaml.properties @@ -0,0 +1 @@ +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..f2794ae3 --- /dev/null +++ b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/spring/apollo-context.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + 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..1405d6ad --- /dev/null +++ b/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiMockIntegrationTest.java @@ -0,0 +1,250 @@ +/* + * 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.jupiter.api.Assertions.assertThrows; + +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; + +/** + * Mock integration tests that verify OpenAPI client request/response chain. + */ +public class ApolloOpenApiMockIntegrationTest { + + private HttpServer server; + private MockPortalHandler handler; + + @org.junit.jupiter.api.BeforeEach + 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(); + } + + @org.junit.jupiter.api.AfterEach + public void tearDown() { + if (server != null) { + server.stop(0); + } + } + + @org.junit.jupiter.api.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); + } + + @org.junit.jupiter.api.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\"]")); + } + + @org.junit.jupiter.api.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); + } + + @org.junit.jupiter.api.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); + } + + @org.junit.jupiter.api.Test + public void shouldWrapServerErrorsAsRuntimeException() { + handler.mock("GET", "/openapi/v1/apps", 500, "internal error"); + ApolloOpenApiClient client = newClient("error-token"); + + RuntimeException exception = assertThrows(RuntimeException.class, client::getAllApps); + assertTrue(exception.getMessage().contains("Load app information")); + assertNotNull(exception.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 + From c62ee67f191e647c8bb4d3840357bfd3bed4e33e Mon Sep 17 00:00:00 2001 From: Jason Song Date: Wed, 18 Feb 2026 22:13:40 +0800 Subject: [PATCH 2/6] fix(ci): restore retry for unit integration tests --- .github/workflows/build.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b24a0757..c8bc27de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,9 +64,12 @@ jobs: ${{ runner.os }}-maven-unit-integration- ${{ runner.os }}-maven- - name: Run unit and integration tests - run: | - mvn -B clean test -P travis jacoco:report \ - -Dmaven.gitcommitid.skip=true + uses: nick-fields/retry@v3 + with: + timeout_minutes: 3 + max_attempts: 3 + retry_wait_seconds: 1 + command: mvn -B clean test -P travis jacoco:report -Dmaven.gitcommitid.skip=true - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: From e383455000cfd53bb33fbc39a2abf05a538afe97 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Wed, 18 Feb 2026 22:21:06 +0800 Subject: [PATCH 3/6] chore: fix license headers and update changelog --- CHANGES.md | 1 + .../resources/mockdata-TEST1.apollo.properties | 15 +++++++++++++++ .../resources/mockdata-application.properties | 15 +++++++++++++++ .../mockdata-application.yaml.properties | 15 +++++++++++++++ .../mockdata-100004459-application.properties | 15 +++++++++++++++ .../resources/mockdata-TEST1.apollo.properties | 15 +++++++++++++++ .../resources/mockdata-application.properties | 15 +++++++++++++++ .../mockdata-application.yaml.properties | 15 +++++++++++++++ .../mockdata-datasources.xml.properties | 15 +++++++++++++++ .../mockdata-100004459-application.properties | 15 +++++++++++++++ .../resources/mockdata-TEST1.apollo.properties | 15 +++++++++++++++ .../resources/mockdata-application.properties | 15 +++++++++++++++ .../mockdata-application.yaml.properties | 15 +++++++++++++++ .../mockdata-100004459-application.properties | 15 +++++++++++++++ .../resources/mockdata-TEST1.apollo.properties | 15 +++++++++++++++ .../resources/mockdata-application.properties | 15 +++++++++++++++ .../mockdata-application.yaml.properties | 15 +++++++++++++++ .../src/test/resources/spring/apollo-context.xml | 16 ++++++++++++++++ 18 files changed, 257 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 2060a6a5..00d7bcdd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Apollo Java 2.5.0 * [Feature Added a new feature to get instance count by namespace.](https://github.com/apolloconfig/apollo-java/pull/103) * [Feature Support retry in open api client.](https://github.com/apolloconfig/apollo-java/pull/105) * [Support Spring Boot 4.0 bootstrap context package relocation for apollo-client-config-data](https://github.com/apolloconfig/apollo-java/pull/115) +* [Test Overhaul automated compatibility coverage across API/Spring/Spring Boot scenarios](https://github.com/apolloconfig/apollo-java/pull/123) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/5?closed=1) 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 index ba60b290..5a235fc1 100644 --- a/apollo-client-config-data/src/test/resources/mockdata-TEST1.apollo.properties +++ b/apollo-client-config-data/src/test/resources/mockdata-TEST1.apollo.properties @@ -1,2 +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 index 28802056..8f3a4cca 100644 --- a/apollo-client-config-data/src/test/resources/mockdata-application.properties +++ b/apollo-client-config-data/src/test/resources/mockdata-application.properties @@ -1,3 +1,18 @@ +# +# 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 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 index 3741f5b9..f1073f69 100644 --- a/apollo-client-config-data/src/test/resources/mockdata-application.yaml.properties +++ b/apollo-client-config-data/src/test/resources/mockdata-application.yaml.properties @@ -1 +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-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 index 37926b4a..60093afa 100644 --- 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 @@ -1,2 +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 index 10ca90dd..c1b4e2ea 100644 --- 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 @@ -1,2 +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 index cd407ae2..538be071 100644 --- 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 @@ -1,2 +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 index b231c1bf..6d148bfd 100644 --- 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 @@ -1 +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 index d493da49..094903d9 100644 --- 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 @@ -1 +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/src/test/resources/mockdata-100004459-application.properties b/apollo-compat-tests/apollo-spring-boot-compat-it/src/test/resources/mockdata-100004459-application.properties index f7e25f11..3955d17c 100644 --- 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 @@ -1 +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 index 947aa952..d1d85ce7 100644 --- 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 @@ -1 +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 index 4eb459b0..5df7e520 100644 --- 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 @@ -1,3 +1,18 @@ +# +# 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 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 index 4da56148..fbe6a7fd 100644 --- 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 @@ -1 +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/src/test/resources/mockdata-100004459-application.properties b/apollo-compat-tests/apollo-spring-compat-it/src/test/resources/mockdata-100004459-application.properties index 76439ae0..cef7d1fe 100644 --- 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 @@ -1 +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 index 7f2a6f58..cab0be75 100644 --- 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 @@ -1 +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 index 9edc073b..f6ce58f4 100644 --- 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 @@ -1,3 +1,18 @@ +# +# 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 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 index 0ef4b9da..ac64a146 100644 --- 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 @@ -1 +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 index f2794ae3..d0e44792 100644 --- 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 @@ -1,4 +1,20 @@ + Date: Wed, 18 Feb 2026 22:40:47 +0800 Subject: [PATCH 4/6] fix: address coderabbit stability and compatibility findings --- ...ClientConfigDataAutoConfigurationTest.java | 2 +- .../ConfigDataIntegrationTest.java | 2 +- ...DeferredLoggerApplicationListenerTest.java | 44 ++---------- ...SpringApplicationRegisterListenerTest.java | 67 +++++++++++++++++++ ...tstrapRegistryHelperCompatibilityTest.java | 20 ++++-- .../annotation/ApolloAnnotationProcessor.java | 11 ++- .../integration/ConfigIntegrationTest.java | 2 +- .../api/ApolloApiCompatibilityTest.java | 4 +- .../ApolloSpringBootCompatibilityTest.java | 18 +++-- .../SpringApolloEventListenerProbe.java | 4 +- .../SpringCompatibilityTestSupport.java | 4 +- .../ApolloOpenApiMockIntegrationTest.java | 29 +++++--- 12 files changed, 134 insertions(+), 73 deletions(-) create mode 100644 apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/listener/ApolloSpringApplicationRegisterListenerTest.java 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 index 02869f36..4ad4e050 100644 --- 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 @@ -65,7 +65,7 @@ public ApolloClientProperties customApolloClientProperties() { } @Bean - public PropertySourcesProcessor customPropertySourcesProcessor() { + public static PropertySourcesProcessor customPropertySourcesProcessor() { return new ConfigPropertySourcesProcessor(); } } 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 index a8f9c2c3..5f2634e6 100644 --- 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 @@ -156,7 +156,7 @@ public void testApolloConfigChangeListenerWithInterestedKeyPrefixes() throws Exc assertEquals("55", environment.getProperty("redis.cache.commandTimeout")); addOrModifyForAllAppIds("application", "apollo.unrelated.key", "value"); - ConfigChangeEvent unrelatedEvent = listenerProbe.pollEvent(1500, TimeUnit.MILLISECONDS); + ConfigChangeEvent unrelatedEvent = listenerProbe.pollEvent(3000, TimeUnit.MILLISECONDS); assertNull(unrelatedEvent); } 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 index 0f4aa02a..60aadece 100644 --- 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 @@ -16,25 +16,26 @@ */ package com.ctrip.framework.apollo.config.data.listener; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.Mockito.mock; -import com.ctrip.framework.apollo.config.data.util.BootstrapRegistryHelper; import com.ctrip.framework.apollo.core.utils.DeferredLogger; -import java.lang.reflect.Constructor; -import org.junit.jupiter.api.Test; +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.boot.context.event.ApplicationStartingEvent; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.util.ClassUtils; /** * @author vdisk */ public class ApolloDeferredLoggerApplicationListenerTest { + @After + public void tearDown() { + DeferredLogger.disable(); + } + @Test public void testReplayDeferredLogsOnApplicationContextInitialized() { DeferredLogger.enable(); @@ -61,34 +62,3 @@ public void testReplayDeferredLogsOnApplicationFailed() { listener.onApplicationEvent(event); } } - -/** - * @author vdisk - */ -class ApolloSpringApplicationRegisterListenerTest { - - @Test - public void testRegisterSpringApplicationToBootstrapContext() throws Exception { - Object bootstrapContext = newDefaultBootstrapContext(); - SpringApplication springApplication = new SpringApplication(Object.class); - - Constructor constructor = ApplicationStartingEvent.class.getConstructors()[0]; - ApplicationStartingEvent event = (ApplicationStartingEvent) constructor - .newInstance(bootstrapContext, springApplication, new String[0]); - - ApolloSpringApplicationRegisterListener listener = new ApolloSpringApplicationRegisterListener(); - listener.onApplicationEvent(event); - - SpringApplication registered = BootstrapRegistryHelper.get(bootstrapContext, SpringApplication.class); - assertSame(springApplication, registered); - } - - 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/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 index 2e4fdddf..1c9c68ab 100644 --- 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 @@ -18,7 +18,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; import java.lang.reflect.Constructor; import java.lang.reflect.Proxy; @@ -49,9 +48,8 @@ public void testRegisterAndGetFromBootstrapContext() throws Exception { @Test public void testGetBootstrapContextFromEventAndLoaderContext() throws Exception { Object bootstrapContext = newDefaultBootstrapContext(); - Constructor constructor = ApplicationStartingEvent.class.getConstructors()[0]; - ApplicationStartingEvent event = (ApplicationStartingEvent) constructor - .newInstance(bootstrapContext, new SpringApplication(Object.class), new String[0]); + ApplicationStartingEvent event = + newApplicationStartingEvent(bootstrapContext, new SpringApplication(Object.class)); Object eventBootstrapContext = BootstrapRegistryHelper.getBootstrapContext(event); assertSame(bootstrapContext, eventBootstrapContext); @@ -75,7 +73,18 @@ public void testSpringBoot4PresenceDetection() { .isPresent("org.springframework.boot.bootstrap.ConfigurableBootstrapContext", BootstrapRegistryHelperCompatibilityTest.class.getClassLoader()); assertEquals(expected, BootstrapRegistryHelper.isSpringBoot4Present()); - assertTrue(BootstrapRegistryHelper.class.getName().contains("BootstrapRegistryHelper")); + } + + 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 { @@ -87,4 +96,3 @@ private Object newDefaultBootstrapContext() throws Exception { return Class.forName(className).getConstructor().newInstance(); } } - 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 364d6673..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,7 +132,7 @@ private void processApolloConfigChangeListener(final Object bean, final Method m ReflectionUtils.makeAccessible(method); String appId = StringUtils.defaultIfBlank(annotation.appId(), configUtil.getAppId()); - String resolvedAppId = this.environment.resolveRequiredPlaceholders(appId); + String resolvedAppId = resolveAppId(appId); String[] namespaces = annotation.value(); String[] annotatedInterestedKeys = annotation.interestedKeys(); String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes(); @@ -271,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/integration/ConfigIntegrationTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/integration/ConfigIntegrationTest.java index 09c99d90..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 @@ -164,7 +164,7 @@ public void testConfigChangeShouldOnlyAffectSpecifiedAppId() throws Exception { assertEquals("default-v1", defaultAppConfig.getProperty(MULTI_APP_KEY, null)); assertEquals("another-v2", anotherAppConfig.getProperty(MULTI_APP_KEY, null)); - assertNull(pollFuture(defaultAppFuture, 300)); + assertNull(pollFuture(defaultAppFuture, 1000)); } @Test 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 index 2e2dd4ac..65688d60 100644 --- 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 @@ -192,11 +192,13 @@ private static void clearApolloClientCaches() throws Exception { 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 == null) { + return; + } if (container instanceof Map) { ((Map) container).clear(); return; 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 index 79f3a1df..94f72bb9 100644 --- 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 @@ -82,6 +82,7 @@ @DirtiesContext public class ApolloSpringBootCompatibilityTest { + private static final String SOME_APP_ID = "someAppId"; private static final String ANOTHER_APP_ID = "100004459"; @ClassRule @@ -138,17 +139,17 @@ public void shouldCoverSpringBootDemoScenarios() throws Exception { applicationProperties.setProperty("compat.timeout", "801"); applicationProperties.setProperty("jsonBeanProperty", "[{\"someString\":\"gamma-boot\",\"someInt\":303}]"); - applyConfigChange(applicationConfig, "application", applicationProperties); + 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, "TEST1.apollo", publicProperties); + 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, "application.yaml", yamlProperties); + applyConfigChange(yamlConfig, SOME_APP_ID, "application.yaml", yamlProperties); Properties anotherAppProperties = copyConfigProperties(compatAnnotatedBean.getAnotherAppConfig()); anotherAppProperties.setProperty("compat.origin", "changed-origin-boot"); @@ -431,11 +432,13 @@ private static void clearApolloClientCaches() throws Exception { 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 == null) { + return; + } if (container instanceof Map) { ((Map) container).clear(); return; @@ -452,16 +455,11 @@ private static void clearField(Object target, String fieldName) throws Exception private static Properties copyConfigProperties(Config config) { Properties properties = new Properties(); for (String key : config.getPropertyNames()) { - properties.setProperty(key, config.getProperty(key, null)); + properties.setProperty(key, config.getProperty(key, "")); } return properties; } - private static void applyConfigChange(Config config, String namespace, Properties properties) { - Assert.assertTrue(config instanceof DefaultConfig); - ((DefaultConfig) config).onRepositoryChange(namespace, properties); - } - private static void applyConfigChange(Config config, String appId, String namespace, Properties properties) { Assert.assertTrue(config instanceof DefaultConfig); 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 index 4f7e0a7e..b614a91e 100644 --- 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 @@ -17,15 +17,15 @@ package com.ctrip.framework.apollo.compat.spring; import com.ctrip.framework.apollo.spring.events.ApolloConfigChangeEvent; -import java.util.concurrent.ArrayBlockingQueue; 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 ArrayBlockingQueue(8); + private final BlockingQueue namespaces = new LinkedBlockingQueue(); @Override public void onApplicationEvent(ApplicationEvent event) { 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 index 3c67518e..67d411af 100644 --- 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 @@ -111,11 +111,13 @@ private static void clearApolloClientCaches() throws Exception { 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 == null) { + return; + } if (container instanceof Map) { ((Map) container).clear(); return; 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 index 1405d6ad..b12a98de 100644 --- 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 @@ -19,7 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.Assert.fail; import com.ctrip.framework.apollo.openapi.dto.OpenAppDTO; import com.ctrip.framework.apollo.openapi.dto.OpenCreateAppDTO; @@ -41,6 +41,9 @@ 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. @@ -50,7 +53,7 @@ public class ApolloOpenApiMockIntegrationTest { private HttpServer server; private MockPortalHandler handler; - @org.junit.jupiter.api.BeforeEach + @Before public void setUp() throws Exception { handler = new MockPortalHandler(); server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); @@ -58,14 +61,14 @@ public void setUp() throws Exception { server.start(); } - @org.junit.jupiter.api.AfterEach + @After public void tearDown() { if (server != null) { server.stop(0); } } - @org.junit.jupiter.api.Test + @Test public void shouldCallFindAppsWithAuthorizationHeaderAndQuery() throws Exception { handler.mock("GET", "/openapi/v1/apps", 200, "[{\"appId\":\"SampleApp\",\"name\":\"SampleApp\",\"ownerName\":\"apollo\"}]"); @@ -84,7 +87,7 @@ public void shouldCallFindAppsWithAuthorizationHeaderAndQuery() throws Exception assertEquals(token, request.authorization); } - @org.junit.jupiter.api.Test + @Test public void shouldSerializeCreateAppRequestBody() throws Exception { handler.mock("POST", "/openapi/v1/apps", 200, ""); ApolloOpenApiClient client = newClient("create-token"); @@ -109,7 +112,7 @@ public void shouldSerializeCreateAppRequestBody() throws Exception { assertTrue(capturedRequest.body.contains("\"admins\":[\"apollo\"]")); } - @org.junit.jupiter.api.Test + @Test public void shouldUseDefaultClusterAndNamespace() throws Exception { handler.mock("GET", "/openapi/v1/envs/DEV/apps/SampleApp/clusters/default/namespaces/application", 200, @@ -130,7 +133,7 @@ public void shouldUseDefaultClusterAndNamespace() throws Exception { assertEquals("namespace-token", request.authorization); } - @org.junit.jupiter.api.Test + @Test public void shouldParseOrganizations() throws Exception { handler.mock("GET", "/openapi/v1/organizations", 200, "[{\"orgId\":\"100001\",\"orgName\":\"Apollo Team\"}]"); @@ -148,14 +151,18 @@ public void shouldParseOrganizations() throws Exception { assertEquals("org-token", request.authorization); } - @org.junit.jupiter.api.Test + @Test public void shouldWrapServerErrorsAsRuntimeException() { handler.mock("GET", "/openapi/v1/apps", 500, "internal error"); ApolloOpenApiClient client = newClient("error-token"); - RuntimeException exception = assertThrows(RuntimeException.class, client::getAllApps); - assertTrue(exception.getMessage().contains("Load app information")); - assertNotNull(exception.getCause()); + 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) { From 264b7bb83a49e717cb5fd993e60ff03f5edd436e Mon Sep 17 00:00:00 2001 From: Jason Song Date: Wed, 18 Feb 2026 23:03:58 +0800 Subject: [PATCH 5/6] fix: address new review findings from bots --- .github/workflows/build.yml | 2 +- .../webclient/ApolloWebClientHttpClient.java | 29 ++++++-- ...polloClientWebClientCustomizerFactory.java | 68 ++++++++++++++++++- .../ConfigDataIntegrationTest.java | 8 ++- .../framework/apollo/MockedConfigService.java | 3 +- .../api/ApolloApiCompatibilityTest.java | 2 +- .../ApolloSpringBootCompatibilityTest.java | 2 +- .../SpringCompatibilityTestSupport.java | 6 +- 8 files changed, 106 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8bc27de..652c16bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,7 +66,7 @@ jobs: - name: Run unit and integration tests uses: nick-fields/retry@v3 with: - timeout_minutes: 3 + timeout_minutes: 8 max_attempts: 3 retry_wait_seconds: 1 command: mvn -B clean test -P travis jacoco:report -Dmaven.gitcommitid.skip=true diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClient.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClient.java index 7583d9aa..33191142 100644 --- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClient.java +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClient.java @@ -26,6 +26,8 @@ import java.lang.reflect.Type; import java.net.URI; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import org.springframework.http.HttpStatus; import org.springframework.util.CollectionUtils; import org.springframework.web.reactive.function.client.ClientResponse; @@ -37,6 +39,10 @@ */ public class ApolloWebClientHttpClient implements HttpClient { + private static final Method CLIENT_RESPONSE_STATUS_CODE_METHOD = resolveClientResponseStatusCodeMethod(); + private static final ConcurrentMap, Method> STATUS_CODE_VALUE_METHOD_CACHE = + new ConcurrentHashMap, Method>(); + private final WebClient webClient; private final Gson gson; @@ -96,17 +102,32 @@ public HttpResponse doGet(HttpRequest httpRequest, Type responseType) */ private int resolveStatusCode(Object clientResponse) { try { - Method statusCodeMethod = ClientResponse.class.getMethod("statusCode"); - Object statusCode = statusCodeMethod.invoke(clientResponse); + Object statusCode = CLIENT_RESPONSE_STATUS_CODE_METHOD.invoke(clientResponse); if (statusCode == null) { throw new ApolloConfigException("Failed to resolve response status code: statusCode is null"); } - // Both HttpStatus and HttpStatusCode expose value(), so resolve it reflectively. - Method valueMethod = statusCode.getClass().getMethod("value"); + 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 b02ef297..b44c752b 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,6 +18,7 @@ import com.ctrip.framework.apollo.config.data.extension.properties.ApolloClientProperties; import com.ctrip.framework.apollo.core.spi.Ordered; +import java.lang.reflect.Method; import java.util.function.Consumer; import org.apache.commons.logging.Log; import org.springframework.boot.context.properties.bind.BindHandler; @@ -45,7 +46,68 @@ public interface ApolloClientWebClientCustomizerFactory extends Ordered { * @return customizer instance or null */ @Nullable - Consumer createWebClientCustomizer(ApolloClientProperties apolloClientProperties, - Binder binder, BindHandler bindHandler, Log log, - Object bootstrapContext); + default Consumer createWebClientCustomizer( + ApolloClientProperties apolloClientProperties, + Binder binder, + BindHandler bindHandler, + Log log, + Object bootstrapContext) { + // Keep compatibility for third-party extensions compiled with the old + // createWebClientCustomizer(...):WebClientCustomizer signature. + Method legacyMethod = findLegacyCustomizerMethod(this.getClass()); + if (legacyMethod == null) { + return null; + } + try { + Object customizer = legacyMethod + .invoke(this, apolloClientProperties, binder, bindHandler, log, bootstrapContext); + return asBuilderConsumer(customizer); + } catch (Exception ex) { + throw new IllegalStateException("Failed to invoke legacy createWebClientCustomizer method", ex); + } + } + + static Method findLegacyCustomizerMethod(Class implementationType) { + for (Method method : implementationType.getMethods()) { + if (!"createWebClientCustomizer".equals(method.getName()) + || method.getDeclaringClass() == ApolloClientWebClientCustomizerFactory.class) { + continue; + } + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length == 5 + && ApolloClientProperties.class == parameterTypes[0] + && Binder.class == parameterTypes[1] + && BindHandler.class == parameterTypes[2] + && Log.class == parameterTypes[3] + && Object.class == parameterTypes[4]) { + method.setAccessible(true); + return method; + } + } + return null; + } + + @SuppressWarnings("unchecked") + static Consumer asBuilderConsumer(Object customizer) { + if (customizer == null) { + return null; + } + if (customizer instanceof Consumer) { + return (Consumer) customizer; + } + try { + Method customizeMethod = customizer.getClass().getMethod("customize", WebClient.Builder.class); + customizeMethod.setAccessible(true); + return builder -> { + try { + customizeMethod.invoke(customizer, builder); + } catch (Exception ex) { + throw new IllegalStateException("Failed to invoke legacy WebClient customizer", ex); + } + }; + } catch (NoSuchMethodException ex) { + throw new IllegalStateException( + "Unsupported customizer type from createWebClientCustomizer: " + customizer.getClass(), ex); + } + } } 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 index 5f2634e6..69bbc8cb 100644 --- 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 @@ -35,10 +35,11 @@ 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 java.util.Map; +import org.junit.After; import org.junit.ClassRule; import org.junit.Before; import org.junit.Test; @@ -114,6 +115,11 @@ public void beforeEach() { embeddedApollo.resetOverriddenProperties(); } + @After + public void afterEach() throws Exception { + resetApolloStaticState(); + } + @Autowired private Environment environment; 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 41e6be64..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 @@ -37,7 +37,7 @@ /** * @author wxq */ -public class MockedConfigService { +public class MockedConfigService implements AutoCloseable { private static final String META_SERVER_PATH = "/services/config?.*"; @@ -255,6 +255,7 @@ public void mockLongPollNotifications( ); } + @Override public void close() { if (this.server.isRunning()) { this.server.stop(); 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 index 65688d60..fbd87baf 100644 --- 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 @@ -207,7 +207,7 @@ private static void clearField(Object target, String fieldName) throws Exception ((Table) container).clear(); return; } - Method clearMethod = container.getClass().getDeclaredMethod("clear"); + Method clearMethod = container.getClass().getMethod("clear"); clearMethod.setAccessible(true); clearMethod.invoke(container); } 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 index 94f72bb9..8fb77ca0 100644 --- 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 @@ -447,7 +447,7 @@ private static void clearField(Object target, String fieldName) throws Exception ((Table) container).clear(); return; } - Method clearMethod = container.getClass().getDeclaredMethod("clear"); + 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/SpringCompatibilityTestSupport.java b/apollo-compat-tests/apollo-spring-compat-it/src/test/java/com/ctrip/framework/apollo/compat/spring/SpringCompatibilityTestSupport.java index 67d411af..e61aee43 100644 --- 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 @@ -47,6 +47,8 @@ private SpringCompatibilityTestSupport() { } static void beforeClass(EmbeddedApollo embeddedApollo) throws Exception { + System.setProperty("app.id", "someAppId"); + System.setProperty("env", "local"); embeddedApollo.resetOverriddenProperties(); resetApolloState(); } @@ -71,7 +73,7 @@ static void waitForCondition(String failureMessage, Callable condition) static Properties copyConfigProperties(Config config) { Properties properties = new Properties(); for (String key : config.getPropertyNames()) { - properties.setProperty(key, config.getProperty(key, null)); + properties.setProperty(key, config.getProperty(key, "")); } return properties; } @@ -126,7 +128,7 @@ private static void clearField(Object target, String fieldName) throws Exception ((Table) container).clear(); return; } - Method clearMethod = container.getClass().getDeclaredMethod("clear"); + Method clearMethod = container.getClass().getMethod("clear"); clearMethod.setAccessible(true); clearMethod.invoke(container); } From bd5d73f00978ec4220db1c078e622bf4328a6158 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Wed, 18 Feb 2026 23:19:01 +0800 Subject: [PATCH 6/6] fix: simplify customizer SPI and tighten CI retry timeout --- .github/workflows/build.yml | 2 +- ...polloClientWebClientCustomizerFactory.java | 68 +------------------ .../SpringCompatibilityTestSupport.java | 5 -- 3 files changed, 4 insertions(+), 71 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 652c16bd..570714f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,7 +66,7 @@ jobs: - name: Run unit and integration tests uses: nick-fields/retry@v3 with: - timeout_minutes: 8 + timeout_minutes: 4 max_attempts: 3 retry_wait_seconds: 1 command: mvn -B clean test -P travis jacoco:report -Dmaven.gitcommitid.skip=true 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 b44c752b..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,7 +18,6 @@ import com.ctrip.framework.apollo.config.data.extension.properties.ApolloClientProperties; import com.ctrip.framework.apollo.core.spi.Ordered; -import java.lang.reflect.Method; import java.util.function.Consumer; import org.apache.commons.logging.Log; import org.springframework.boot.context.properties.bind.BindHandler; @@ -46,68 +45,7 @@ public interface ApolloClientWebClientCustomizerFactory extends Ordered { * @return customizer instance or null */ @Nullable - default Consumer createWebClientCustomizer( - ApolloClientProperties apolloClientProperties, - Binder binder, - BindHandler bindHandler, - Log log, - Object bootstrapContext) { - // Keep compatibility for third-party extensions compiled with the old - // createWebClientCustomizer(...):WebClientCustomizer signature. - Method legacyMethod = findLegacyCustomizerMethod(this.getClass()); - if (legacyMethod == null) { - return null; - } - try { - Object customizer = legacyMethod - .invoke(this, apolloClientProperties, binder, bindHandler, log, bootstrapContext); - return asBuilderConsumer(customizer); - } catch (Exception ex) { - throw new IllegalStateException("Failed to invoke legacy createWebClientCustomizer method", ex); - } - } - - static Method findLegacyCustomizerMethod(Class implementationType) { - for (Method method : implementationType.getMethods()) { - if (!"createWebClientCustomizer".equals(method.getName()) - || method.getDeclaringClass() == ApolloClientWebClientCustomizerFactory.class) { - continue; - } - Class[] parameterTypes = method.getParameterTypes(); - if (parameterTypes.length == 5 - && ApolloClientProperties.class == parameterTypes[0] - && Binder.class == parameterTypes[1] - && BindHandler.class == parameterTypes[2] - && Log.class == parameterTypes[3] - && Object.class == parameterTypes[4]) { - method.setAccessible(true); - return method; - } - } - return null; - } - - @SuppressWarnings("unchecked") - static Consumer asBuilderConsumer(Object customizer) { - if (customizer == null) { - return null; - } - if (customizer instanceof Consumer) { - return (Consumer) customizer; - } - try { - Method customizeMethod = customizer.getClass().getMethod("customize", WebClient.Builder.class); - customizeMethod.setAccessible(true); - return builder -> { - try { - customizeMethod.invoke(customizer, builder); - } catch (Exception ex) { - throw new IllegalStateException("Failed to invoke legacy WebClient customizer", ex); - } - }; - } catch (NoSuchMethodException ex) { - throw new IllegalStateException( - "Unsupported customizer type from createWebClientCustomizer: " + customizer.getClass(), ex); - } - } + Consumer createWebClientCustomizer(ApolloClientProperties apolloClientProperties, + Binder binder, BindHandler bindHandler, Log log, + Object bootstrapContext); } 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 index e61aee43..0f2024d9 100644 --- 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 @@ -38,11 +38,6 @@ final class SpringCompatibilityTestSupport { 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", "someAppId"); - System.setProperty("env", "local"); - } - private SpringCompatibilityTestSupport() { }