diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt
index cbf42e119ae7..f37933dc068a 100644
--- a/eng/versioning/external_dependencies.txt
+++ b/eng/versioning/external_dependencies.txt
@@ -383,7 +383,6 @@ springboot3_org.springframework.boot:spring-boot-test;3.5.5
springboot3_org.springframework.boot:spring-boot-testcontainers;3.5.5
springboot3_org.springframework.cloud:spring-cloud-bus;4.3.0
springboot3_org.springframework.cloud:spring-cloud-context;4.3.0
-springboot3_org.springframework.cloud:spring-cloud-starter-bootstrap;4.3.0
springboot3_org.springframework.cloud:spring-cloud-starter-stream-kafka;4.3.0
springboot3_org.springframework.cloud:spring-cloud-stream-test-binder;4.3.0
springboot3_org.springframework.cloud:spring-cloud-stream;4.3.0
@@ -412,6 +411,8 @@ springboot3_org.springframework:spring-web;6.2.10
springboot3_org.springframework:spring-webmvc;6.2.10
springboot3_org.testcontainers:junit-jupiter;1.21.3
springboot3_org.testcontainers:azure;1.21.3
+springboot3_jakarta.annotation:jakarta.annotation-api;3.0.0
+springboot3_ch.qos.logback:logback-classic;1.5.18
# Used for Spring version updates
springboot3_org.springframework.boot:spring-boot-dependencies;3.5.5
springboot3_org.springframework.cloud:spring-cloud-dependencies;2025.0.0
diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt
index 299a9b0cd09e..b5e6930f0aba 100644
--- a/eng/versioning/version_client.txt
+++ b/eng/versioning/version_client.txt
@@ -211,11 +211,11 @@ com.microsoft.azure:azure-eventhubs;3.3.0;3.4.0-beta.1
com.microsoft.azure:azure-servicebus;3.6.7;3.7.0-beta.1
com.microsoft.azure:azure-media;1.0.0-beta.1;1.0.0-beta.1
com.azure.spring:azure-monitor-spring-native-test;1.0.0-beta.1;1.0.0-beta.1
-com.azure.spring:spring-cloud-azure-appconfiguration-config-web;5.23.0;5.24.0-beta.1
-com.azure.spring:spring-cloud-azure-appconfiguration-config;5.23.0;5.24.0-beta.1
-com.azure.spring:spring-cloud-azure-feature-management-web;5.23.0;5.24.0-beta.1
-com.azure.spring:spring-cloud-azure-feature-management;5.23.0;5.24.0-beta.1
-com.azure.spring:spring-cloud-azure-starter-appconfiguration-config;5.23.0;5.24.0-beta.1
+com.azure.spring:spring-cloud-azure-appconfiguration-config-web;6.0.0;6.0.0-beta.2
+com.azure.spring:spring-cloud-azure-appconfiguration-config;6.0.0;6.0.0-beta.2
+com.azure.spring:spring-cloud-azure-feature-management-web;6.0.0;6.0.0-beta.2
+com.azure.spring:spring-cloud-azure-feature-management;6.0.0;6.0.0-beta.2
+com.azure.spring:spring-cloud-azure-starter-appconfiguration-config;6.0.0;6.0.0-beta.2
com.azure.spring:spring-cloud-azure-dependencies;5.23.0;5.24.0-beta.1
com.azure.spring:spring-messaging-azure;5.23.0;5.24.0-beta.1
com.azure.spring:spring-messaging-azure-eventhubs;5.23.0;5.24.0-beta.1
diff --git a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml
index a0387c5b4390..dc8babfb9a80 100644
--- a/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml
+++ b/sdk/spring/spring-cloud-azure-actuator-autoconfigure/pom.xml
@@ -62,7 +62,7 @@
com.azure.spring
spring-cloud-azure-appconfiguration-config-web
- 5.24.0-beta.1
+ 6.0.0-beta.2
true
diff --git a/sdk/spring/spring-cloud-azure-actuator/pom.xml b/sdk/spring/spring-cloud-azure-actuator/pom.xml
index 2416914bfe38..1cddb7848f0b 100644
--- a/sdk/spring/spring-cloud-azure-actuator/pom.xml
+++ b/sdk/spring/spring-cloud-azure-actuator/pom.xml
@@ -109,7 +109,7 @@
com.azure.spring
spring-cloud-azure-appconfiguration-config-web
- 5.24.0-beta.1
+ 6.0.0-beta.2
true
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md
index 0555f70690aa..3ccaad647e43 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/CHANGELOG.md
@@ -1,6 +1,6 @@
# Release History
-## 5.24.0-beta.1 (Unreleased)
+## 6.0.0-beta.2 (Unreleased)
### Features Added
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml
index 33602fab1a1a..c5f165f719ed 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/pom.xml
@@ -9,7 +9,7 @@
4.0.0
com.azure.spring
spring-cloud-azure-appconfiguration-config-web
- 5.24.0-beta.1
+ 6.0.0-beta.2
Spring Cloud Azure App Configuration Config Web
Integration of Spring Cloud Config and Azure App Configuration Service
@@ -21,7 +21,7 @@
com.azure.spring
spring-cloud-azure-appconfiguration-config
- 5.24.0-beta.1
+ 6.0.0-beta.2
org.springframework.boot
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java
index 1b00e0f55600..8771558968af 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pullrefresh/AppConfigurationEventListener.java
@@ -9,6 +9,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
+import org.springframework.lang.NonNull;
import org.springframework.web.context.support.ServletRequestHandledEvent;
import com.azure.spring.cloud.appconfiguration.config.AppConfigurationRefresh;
@@ -32,7 +33,7 @@ public AppConfigurationEventListener(AppConfigurationRefresh appConfigurationRef
}
@Override
- public void onApplicationEvent(ServletRequestHandledEvent event) {
+ public void onApplicationEvent(@NonNull ServletRequestHandledEvent event) {
try {
if (!(event.getRequestUrl().equals(ACTUATOR + APPCONFIGURATION_REFRESH)
|| event.getRequestUrl().equals(ACTUATOR + APPCONFIGURATION_REFRESH_BUS))) {
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java
index bf846e6418df..296f554f51ec 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushbusrefresh/AppConfigurationBusRefreshEventListener.java
@@ -5,6 +5,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
+import org.springframework.lang.NonNull;
import com.azure.spring.cloud.appconfiguration.config.AppConfigurationRefresh;
@@ -31,7 +32,7 @@ public AppConfigurationBusRefreshEventListener(AppConfigurationRefresh appConfig
* @param event Event Triggering refresh, contains valid config store endpoint.
*/
@Override
- public void onApplicationEvent(AppConfigurationBusRefreshEvent event) {
+ public void onApplicationEvent(@NonNull AppConfigurationBusRefreshEvent event) {
try {
appConfigurationRefresh.expireRefreshInterval(event.getEndpoint(), event.getSyncToken());
} catch (Exception e) {
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpoint.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpoint.java
index 139f2a18b433..1a97678e0cd9 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpoint.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEndpoint.java
@@ -10,11 +10,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
+import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.http.HttpStatus;
+import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@@ -29,8 +30,7 @@
/**
* Endpoint for requesting new configurations to be loaded.
*/
-@SuppressWarnings("removal")
-@ControllerEndpoint(id = APPCONFIGURATION_REFRESH)
+@Endpoint(id = APPCONFIGURATION_REFRESH)
public class AppConfigurationRefreshEndpoint implements ApplicationEventPublisherAware {
private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationRefreshEndpoint.class);
@@ -105,7 +105,7 @@ public String refresh(HttpServletRequest request, HttpServletResponse response,
}
@Override
- public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
+ public void setApplicationEventPublisher(@NonNull ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java
index 187946abbf02..cfaf05050bc9 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/main/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/pushrefresh/AppConfigurationRefreshEventListener.java
@@ -5,6 +5,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
+import org.springframework.lang.NonNull;
import com.azure.spring.cloud.appconfiguration.config.AppConfigurationRefresh;
@@ -32,7 +33,7 @@ public AppConfigurationRefreshEventListener(AppConfigurationRefresh appConfigura
* @param event Event Triggering refresh, contains valid config store endpoint.
*/
@Override
- public void onApplicationEvent(AppConfigurationRefreshEvent event) {
+ public void onApplicationEvent(@NonNull AppConfigurationRefreshEvent event) {
try {
appConfigurationRefresh.expireRefreshInterval(event.getEndpoint(), event.getSyncToken());
} catch (Exception e) {
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationWebAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationWebAutoConfigurationTest.java
deleted file mode 100644
index 2f861d46ad5d..000000000000
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config-web/src/test/java/com/azure/spring/cloud/appconfiguration/config/web/implementation/AppConfigurationWebAutoConfigurationTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-package com.azure.spring.cloud.appconfiguration.config.web.implementation;
-
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.CONN_STRING_PROP;
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.STORE_ENDPOINT_PROP;
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.TEST_CONN_STRING;
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestConstants.TEST_STORE_NAME;
-import static com.azure.spring.cloud.appconfiguration.config.web.implementation.TestUtils.propPair;
-import static org.assertj.core.api.Assertions.assertThat;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
-import org.springframework.boot.autoconfigure.AutoConfigurations;
-import org.springframework.boot.test.context.FilteredClassLoader;
-import org.springframework.boot.test.context.runner.ApplicationContextRunner;
-import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
-import org.springframework.cloud.bus.BusProperties;
-import org.springframework.cloud.bus.event.PathDestinationFactory;
-import org.springframework.cloud.bus.event.RefreshRemoteApplicationEvent;
-import org.springframework.cloud.endpoint.RefreshEndpoint;
-
-import com.azure.spring.cloud.appconfiguration.config.AppConfigurationAutoConfiguration;
-import com.azure.spring.cloud.appconfiguration.config.implementation.config.AppConfigurationBootstrapConfiguration;
-import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration;
-
-public class AppConfigurationWebAutoConfigurationTest {
-
- private static final ApplicationContextRunner CONTEXT_RUNNER = new ApplicationContextRunner()
- .withPropertyValues(propPair(CONN_STRING_PROP, TEST_CONN_STRING),
- propPair(STORE_ENDPOINT_PROP, TEST_STORE_NAME))
- .withConfiguration(AutoConfigurations.of(AppConfigurationBootstrapConfiguration.class,
- AppConfigurationAutoConfiguration.class, AppConfigurationWebAutoConfiguration.class,
- RefreshAutoConfiguration.class, PathDestinationFactory.class, AzureGlobalPropertiesAutoConfiguration.class))
- .withUserConfiguration(BusProperties.class);
-
- @Test
- public void refreshMissing() {
- CONTEXT_RUNNER
- .withClassLoader(new FilteredClassLoader(WebEndpointProperties.class))
- .run(context -> {
- assertThat(context)
- .doesNotHaveBean("appConfigurationRefreshBusEndpoint");
- assertThat(context)
- .doesNotHaveBean("appConfigurationRefreshEndpoint");
- assertThat(context)
- .hasBean("configListener");
- });
- }
-
- @Test
- public void busRefreshMissing() {
- CONTEXT_RUNNER
- .withClassLoader(new FilteredClassLoader(RefreshRemoteApplicationEvent.class))
- .run(context -> {
- assertThat(context)
- .doesNotHaveBean("appConfigurationBusRefreshEndpoint");
- assertThat(context)
- .hasBean("appConfigurationRefreshEndpoint");
- assertThat(context)
- .hasBean("configListener");
- });
- }
-
- @Test
- public void pullRefreshListenerMissing() {
- CONTEXT_RUNNER.withClassLoader(new FilteredClassLoader(RefreshEndpoint.class))
- .run(context -> assertThat(context)
- .doesNotHaveBean("configListener"));
- }
-
- @Test
- public void pushRefresh() {
- CONTEXT_RUNNER
- .run(context -> {
- assertThat(context)
- .hasBean("appConfigurationRefreshEndpoint");
- });
- }
-
- @Test
- public void busRefresh() {
- CONTEXT_RUNNER
- .run(context -> assertThat(context)
- .hasBean("appConfigurationBusRefreshEndpoint"));
- }
-
- @Test
- public void fullRefresh() {
- CONTEXT_RUNNER
- .run(context -> {
- assertThat(context)
- .hasBean("configListener");
- assertThat(context)
- .hasBean("appConfigurationRefreshEndpoint");
- assertThat(context)
- .hasBean("appConfigurationBusRefreshEndpoint");
- });
- }
-}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md
index dc4f726273a0..be5ad0cf47eb 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/CHANGELOG.md
@@ -1,6 +1,6 @@
# Release History
-## 5.24.0-beta.1 (Unreleased)
+## 6.0.0-beta.2 (Unreleased)
### Features Added
@@ -10,6 +10,7 @@
### Other Changes
+
## 5.23.0 (2025-08-05)
Please refer to [spring/CHANGELOG.md](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/CHANGELOG.md#5230-2025-08-05) for more details.
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml
index 22511b233b44..3c0f03be9169 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/pom.xml
@@ -9,7 +9,7 @@
4.0.0
com.azure.spring
spring-cloud-azure-appconfiguration-config
- 5.24.0-beta.1
+ 6.0.0-beta.2
Spring Cloud Azure App Configuration Config
Integration of Spring Cloud Config and Azure App Configuration Service
@@ -29,11 +29,6 @@
3.5.5
true
-
- org.springframework.cloud
- spring-cloud-starter-bootstrap
- 4.3.0
-
org.springframework.cloud
spring-cloud-context
@@ -45,6 +40,11 @@
3.5.5
compile
+
+ jakarta.annotation
+ jakarta.annotation-api
+ 3.0.0
+
com.azure
@@ -182,8 +182,8 @@
org.springframework.boot:spring-boot-actuator:[3.5.5]
org.springframework.boot:spring-boot-autoconfigure:[3.5.5]
org.springframework.cloud:spring-cloud-context:[4.3.0]
+ jakarta.annotation:jakarta.annotation-api:[3.0.0]
org.springframework.boot:spring-boot-configuration-processor:[3.5.5]
- org.springframework.cloud:spring-cloud-starter-bootstrap:[4.3.0]
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationAutoConfiguration.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java
similarity index 52%
rename from sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationAutoConfiguration.java
rename to sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java
index dde0e852d65a..9b0d4be6c0a0 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationAutoConfiguration.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/AppConfigurationWatchAutoConfiguration.java
@@ -2,12 +2,14 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config;
+import org.springframework.boot.BootstrapContext;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.endpoint.RefreshEndpoint;
import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationPullRefresh;
@@ -15,41 +17,31 @@
import com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationReplicaClientFactory;
import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProviderProperties;
/**
* Setup AppConfigurationRefresh when spring.cloud.azure.appconfiguration.enabled is enabled.
*/
-@Configuration
@EnableAsync
@ConditionalOnProperty(prefix = AppConfigurationProperties.CONFIG_PREFIX, name = "enabled", matchIfMissing = true)
-public class AppConfigurationAutoConfiguration {
+@EnableConfigurationProperties({ AppConfigurationProperties.class })
+@AutoConfiguration
+@ConditionalOnClass(RefreshEndpoint.class)
+public class AppConfigurationWatchAutoConfiguration {
/**
- * Creates an instance of {@link AppConfigurationAutoConfiguration}
+ * Creates an instance of {@link AppConfigurationWatchAutoConfiguration}
*/
- public AppConfigurationAutoConfiguration() {
+ public AppConfigurationWatchAutoConfiguration() {
}
- /**
- * Auto Watch
- */
- @Configuration
- @ConditionalOnClass(RefreshEndpoint.class)
- public static class AppConfigurationWatchAutoConfiguration {
-
- /**
- * Creates an instance of {@link AppConfigurationWatchAutoConfiguration}
- */
- public AppConfigurationWatchAutoConfiguration() {
- }
+ @Bean
+ @ConditionalOnMissingBean
+ AppConfigurationRefresh appConfigurationRefresh(AppConfigurationProperties properties, BootstrapContext context) {
+ AppConfigurationReplicaClientFactory clientFactory = context
+ .get(AppConfigurationReplicaClientFactory.class);
+ ReplicaLookUp replicaLookUp = context.get(ReplicaLookUp.class);
- @Bean
- @ConditionalOnMissingBean
- AppConfigurationRefresh appConfigurationRefresh(AppConfigurationProperties properties,
- AppConfigurationProviderProperties appProperties, AppConfigurationReplicaClientFactory clientFactory, ReplicaLookUp replicaLookUp) {
- return new AppConfigurationPullRefresh(clientFactory, properties.getRefreshInterval(),
- appProperties.getDefaultMinBackoff(), replicaLookUp, new AppConfigurationRefreshUtil());
- }
+ return new AppConfigurationPullRefresh(clientFactory, properties.getRefreshInterval(), replicaLookUp,
+ new AppConfigurationRefreshUtil());
}
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java
index 5d410ab33151..4ac64874a871 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationApplicationSettingPropertySource.java
@@ -18,6 +18,7 @@
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.util.StringUtils;
+import com.azure.core.util.Context;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting;
@@ -60,7 +61,7 @@ class AppConfigurationApplicationSettingPropertySource extends AppConfigurationP
* @param keyPrefixTrimValues prefixs to trim from key values
* @throws InvalidConfigurationPropertyValueException thrown if fails to parse Json content type
*/
- public void initProperties(List keyPrefixTrimValues, boolean isRefresh) throws InvalidConfigurationPropertyValueException {
+ public void initProperties(List keyPrefixTrimValues, Context context) throws InvalidConfigurationPropertyValueException {
List labels = Arrays.asList(labelFilters);
// Reverse labels so they have the right priority order.
@@ -70,7 +71,7 @@ public void initProperties(List keyPrefixTrimValues, boolean isRefresh)
SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter + "*").setLabelFilter(label);
// * for wildcard match
- processConfigurationSettings(replicaClient.listSettings(settingSelector, isRefresh), settingSelector.getKeyFilter(),
+ processConfigurationSettings(replicaClient.listSettings(settingSelector, context), settingSelector.getKeyFilter(),
keyPrefixTrimValues);
}
}
@@ -107,7 +108,7 @@ protected void processConfigurationSettings(List settings,
* @return Key Vault Secret Value
* @throws InvalidConfigurationPropertyValueException
*/
- protected void handleKeyVaultReference(String key, SecretReferenceConfigurationSetting secretReference)
+ private void handleKeyVaultReference(String key, SecretReferenceConfigurationSetting secretReference)
throws InvalidConfigurationPropertyValueException {
// Parsing Key Vault Reference for URI
try {
@@ -115,21 +116,22 @@ protected void handleKeyVaultReference(String key, SecretReferenceConfigurationS
KeyVaultSecret secret = keyVaultClientFactory.getClient("https://" + uri.getHost()).getSecret(uri);
properties.put(key, secret.getValue());
} catch (URISyntaxException e) {
- LOGGER.error(String.format("Error Retrieving Key Vault Entry for key %s.", key));
+ LOGGER.error("Error Retrieving Key Vault Entry for key {}.", key);
throw new InvalidConfigurationPropertyValueException(key, "",
"Invalid URI found in JSON property field 'uri' unable to parse.");
} catch (RuntimeException e) {
- LOGGER.error(String.format("Error Retrieving Key Vault Entry for key %s.", key));
+ LOGGER.error("Error Retrieving Key Vault Entry for key {}.", key);
throw e;
}
}
void handleFeatureFlag(String key, FeatureFlagConfigurationSetting setting, List trimStrings)
throws InvalidConfigurationPropertyValueException {
- handleJson(setting, trimStrings);
+ // Feature Flags aren't loaded as configuration, but are loaded as feature flags when loading a snapshot.
+ return;
}
- void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues)
+ private void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues)
throws InvalidConfigurationPropertyValueException {
Map jsonSettings = JsonConfigurationParser.parseJsonSetting(setting);
for (Entry jsonSetting : jsonSettings.entrySet()) {
@@ -138,7 +140,7 @@ void handleJson(ConfigurationSetting setting, List keyPrefixTrimValues)
}
}
- protected String trimKey(String key, List trimStrings) {
+ private String trimKey(String key, List trimStrings) {
key = key.trim();
if (trimStrings != null) {
for (String trim : trimStrings) {
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java
index 44a8db8df3cc..b480b72eb922 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationConstants.java
@@ -12,16 +12,6 @@ public class AppConfigurationConstants {
*/
public static final String FEATURE_FLAG_CONTENT_TYPE = "application/vnd.microsoft.appconfig.ff+json;charset=utf-8";
- /**
- * App Configurations Key Vault Reference Content Type
- */
- public static final String KEY_VAULT_CONTENT_TYPE = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8";
-
- /**
- * Feature Management Key Prefix
- */
- public static final String FEATURE_MANAGEMENT_KEY = "feature-management.";
-
/**
* Feature Flag Prefix
*/
@@ -46,17 +36,17 @@ public class AppConfigurationConstants {
* Constant for tracing if Key Vault is configured for use.
*/
public static final String KEY_VAULT_CONFIGURED_TRACING = "UsesKeyVault";
-
+
/**
- * Constant for tracing for Replica Count
+ * Constant for tracing if Push Refresh is enabled for the store.
*/
- public static final String REPLICA_COUNT = "ReplicaCount";
+ public static final String PUSH_REFRESH = "PushRefresh";
/**
* Http Header User Agent
*/
public static final String USER_AGENT_TYPE = "User-Agent";
-
+
/**
* Http Header Correlation Context
*/
@@ -71,30 +61,10 @@ public class AppConfigurationConstants {
public static final String TELEMETRY = "telemetry";
- public static final String USERS = "users";
-
- public static final String USERS_CAPS = "Users";
-
- public static final String AUDIENCE = "Audience";
-
- public static final String GROUPS = "groups";
-
- public static final String GROUPS_CAPS = "Groups";
-
- public static final String TARGETING_FILTER = "targetingFilter";
-
- public static final String DEFAULT_ROLLOUT_PERCENTAGE = "defaultRolloutPercentage";
-
- public static final String DEFAULT_ROLLOUT_PERCENTAGE_CAPS = "DefaultRolloutPercentage";
-
public static final String DEFAULT_REQUIREMENT_TYPE = "Any";
public static final String REQUIREMENT_TYPE_SERVICE = "requirement_type";
- public static final String REQUIREMENT_TYPE = "requirement-type";
-
- public static final String FEATURE_FLAG_ID = "FeatureFlagId";
-
public static final String FEATURE_FLAG_REFERENCE = "FeatureFlagReference";
public static final String E_TAG = "ETag";
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySource.java
index 310081227eaa..97efc7c20260 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySource.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationFeatureManagementPropertySource.java
@@ -2,8 +2,13 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.implementation;
+import java.util.List;
+
import org.springframework.core.env.EnumerablePropertySource;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+
/**
* Azure App Configuration PropertySource unique per Store Label(Profile) combo.
*
@@ -12,12 +17,13 @@
*
*/
class AppConfigurationFeatureManagementPropertySource extends EnumerablePropertySource {
-
private final FeatureFlagClient featureFlagLoader;
- private static final String FEATURE_MANAGEMENT_KEY = "feature_management";
+ private static final String FEATURE_MANAGEMENT_KEY = "feature-management";
- private static final String FEATURE_FLAG_KEY = FEATURE_MANAGEMENT_KEY + ".feature_flags";
+ private static final ObjectMapper MAPPER = JsonMapper.builder().build();
+
+ private static final String FEATURE_FLAG_KEY = FEATURE_MANAGEMENT_KEY + ".feature-flags";
AppConfigurationFeatureManagementPropertySource(FeatureFlagClient featureFlagLoader) {
super(FEATURE_MANAGEMENT_KEY, featureFlagLoader);
@@ -26,14 +32,17 @@ class AppConfigurationFeatureManagementPropertySource extends EnumerableProperty
@Override
public String[] getPropertyNames() {
- String[] names = { FEATURE_FLAG_KEY };
- return names;
+ if (featureFlagLoader != null && featureFlagLoader.getFeatureFlags() != null
+ && !featureFlagLoader.getFeatureFlags().isEmpty()) {
+ return new String[] { FEATURE_FLAG_KEY };
+ }
+ return new String[0];
}
@Override
public Object getProperty(String name) {
if (FEATURE_FLAG_KEY.equals(name)) {
- return featureFlagLoader.getProperties();
+ return MAPPER.convertValue(featureFlagLoader.getFeatureFlags(), List.class);
}
return null;
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java
index 2cc9fbffe363..e9ded98208b9 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationKeyVaultClientFactory.java
@@ -10,46 +10,83 @@
import com.azure.spring.cloud.appconfiguration.config.implementation.stores.AppConfigurationSecretClientManager;
import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory;
-public class AppConfigurationKeyVaultClientFactory {
-
+/**
+ * Factory for creating and managing AppConfigurationSecretClientManager instances. This class caches clients per Key
+ * Vault host.
+ */
+class AppConfigurationKeyVaultClientFactory {
+ /**
+ * Cache of secret client managers by Key Vault host.
+ */
private final Map keyVaultClients;
+ /**
+ * Optional customizer for Key Vault secret clients.
+ */
private final SecretClientCustomizer keyVaultClientProvider;
+ /**
+ * Optional provider for custom secret resolution.
+ */
private final KeyVaultSecretProvider keyVaultSecretProvider;
+ /**
+ * Factory for creating secret client builders.
+ */
private final SecretClientBuilderFactory secretClientFactory;
-
+
+ /**
+ * Flag indicating whether credentials are configured.
+ */
private final boolean credentialsConfigured;
+ /**
+ * Flag indicating whether the factory being used for telemetry.
+ */
private final boolean isConfigured;
-
- private final int timeout;
- public AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClientProvider,
+ /**
+ * Creates a new AppConfigurationKeyVaultClientFactory.
+ *
+ * @param keyVaultClientProvider optional customizer for Key Vault secret clients
+ * @param keyVaultSecretProvider optional provider for custom secret resolution
+ * @param secretClientFactory factory for creating secret client builders
+ * @param credentialsConfigured whether credentials are configured
+ */
+ AppConfigurationKeyVaultClientFactory(SecretClientCustomizer keyVaultClientProvider,
KeyVaultSecretProvider keyVaultSecretProvider, SecretClientBuilderFactory secretClientFactory,
- boolean credentialsConfigured, int timeout) {
+ boolean credentialsConfigured) {
this.keyVaultClientProvider = keyVaultClientProvider;
this.keyVaultSecretProvider = keyVaultSecretProvider;
this.secretClientFactory = secretClientFactory;
keyVaultClients = new HashMap<>();
this.credentialsConfigured = credentialsConfigured;
isConfigured = keyVaultClientProvider != null || credentialsConfigured;
- this.timeout = timeout;
}
- public AppConfigurationSecretClientManager getClient(String host) {
+ /**
+ * Gets or creates a secret client manager for the specified Key Vault host.
+ *
+ * @param host the Key Vault host endpoint
+ * @return the secret client manager for the host
+ */
+ AppConfigurationSecretClientManager getClient(String host) {
// Check if we already have a client for this key vault, if not we will make
// one
if (!keyVaultClients.containsKey(host)) {
AppConfigurationSecretClientManager client = new AppConfigurationSecretClientManager(host,
- keyVaultClientProvider, keyVaultSecretProvider, secretClientFactory, credentialsConfigured, timeout);
+ keyVaultClientProvider, keyVaultSecretProvider, secretClientFactory, credentialsConfigured);
keyVaultClients.put(host, client);
}
return keyVaultClients.get(host);
}
- public boolean isConfigured() {
+ /**
+ * Returns if Key Vault is configured to be used.
+ *
+ * @return true if either a client provider is configured or credentials are configured
+ */
+ boolean isConfigured() {
return isConfigured;
}
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java
index c31069689625..120adc189672 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySource.java
@@ -2,58 +2,89 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.implementation;
-import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.core.env.EnumerablePropertySource;
-import com.azure.data.appconfiguration.ConfigurationClient;
-import com.azure.data.appconfiguration.models.ConfigurationSetting;
+import com.azure.core.util.Context;
/**
- * Azure App Configuration PropertySource unique per Store Label(Profile) combo.
- *
+ * Abstract base class for Azure App Configuration PropertySource implementations.
+ *
*
- * i.e. If connecting to 2 stores and have 2 labels set 4 AppConfigurationPropertySources need to be
- * created.
+ * Each PropertySource is unique per Store-Label(Profile) combination. For example, if connecting to 2 stores with 2
+ * labels each, 4 AppConfigurationPropertySources need to be created.
*
*/
-abstract class AppConfigurationPropertySource extends EnumerablePropertySource {
+abstract class AppConfigurationPropertySource extends EnumerablePropertySource {
+ /**
+ * Cache for storing configuration properties retrieved from Azure App Configuration.
+ */
protected final Map properties = new LinkedHashMap<>();
- protected final List featureConfigurationSettings = new ArrayList<>();
-
+ /**
+ * Client for communicating with Azure App Configuration service.
+ */
protected final AppConfigurationReplicaClient replicaClient;
+ /**
+ * Creates a new AppConfigurationPropertySource.
+ *
+ * @param name the name of this property source, should be unique to identify the store-label combination
+ * @param replicaClient the client for communicating with Azure App Configuration
+ */
AppConfigurationPropertySource(String name, AppConfigurationReplicaClient replicaClient) {
// The context alone does not uniquely define a PropertySource, append storeName
// and label to uniquely define a PropertySource
- super(name);
+ super(name, replicaClient);
this.replicaClient = replicaClient;
}
+ /**
+ * Returns the names of all properties in this property source.
+ *
+ * @return array of property names
+ */
@Override
public String[] getPropertyNames() {
- Set keySet = properties.keySet();
- return keySet.toArray(new String[keySet.size()]);
+ return properties.keySet().toArray(String[]::new);
}
+ /**
+ * Returns the value of the specified property.
+ *
+ * @param name the name of the property to retrieve
+ * @return the value of the property, or null if not found
+ */
@Override
public Object getProperty(String name) {
return properties.get(name);
}
+ /**
+ * Creates a comma-separated string from the given label filters.
+ *
+ * @param labelFilters array of label filters, may be null
+ * @return comma-separated string of labels, or empty string if null/empty
+ */
protected static String getLabelName(String[] labelFilters) {
- if (labelFilters == null) {
+ if (labelFilters == null || labelFilters.length == 0) {
return "";
}
return String.join(",", labelFilters);
}
- protected abstract void initProperties(List trim, boolean isRefresh) throws InvalidConfigurationPropertyValueException;
+ /**
+ * Initializes the properties for this property source by loading them from Azure App Configuration.
+ *
+ * @param trim list of key prefixes to trim from configuration keys
+ * @param context the context for loading properties, may contain additional metadata
+ * @throws InvalidConfigurationPropertyValueException if there are issues with the configuration properties
+ */
+ protected abstract void initProperties(List trim, Context context)
+ throws InvalidConfigurationPropertyValueException;
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java
deleted file mode 100644
index 04de646b0631..000000000000
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPropertySourceLocator.java
+++ /dev/null
@@ -1,304 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-package com.azure.spring.cloud.appconfiguration.config.implementation;
-
-import static org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration.BOOTSTRAP_PROPERTY_SOURCE_NAME;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
-import org.springframework.core.env.CompositePropertySource;
-import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.core.env.Environment;
-import org.springframework.core.env.PropertySource;
-import org.springframework.util.StringUtils;
-
-import com.azure.data.appconfiguration.models.ConfigurationSetting;
-import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
-import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProviderProperties;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore;
-import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector;
-
-/**
- * Locates Azure App Configuration Property Sources.
- */
-public final class AppConfigurationPropertySourceLocator implements PropertySourceLocator {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPropertySourceLocator.class);
-
- private static final String PROPERTY_SOURCE_NAME = "azure-config-store";
-
- private static final String REFRESH_ARGS_PROPERTY_SOURCE = "refreshArgs";
-
- private final List configStores;
-
- private final AppConfigurationProviderProperties appProperties;
-
- private final AppConfigurationReplicaClientFactory clientFactory;
-
- private final AppConfigurationKeyVaultClientFactory keyVaultClientFactory;
-
- private final FeatureFlagClient featureFlagClient;
-
- private final ReplicaLookUp replicaLookUp;
-
- private Duration refreshInterval;
-
- static final AtomicBoolean STARTUP = new AtomicBoolean(true);
-
- /**
- * Loads all Azure App Configuration Property Sources configured.
- *
- * @param properties Configurations for stores to be loaded.
- * @param appProperties Configurations for the library.
- * @param clientFactory factory for creating clients for connecting to Azure App Configuration.
- * @param keyVaultClientFactory factory for creating clients for connecting to Azure Key Vault
- * @param featureFlagLoader service for loadingFeatureFlags.
- */
- public AppConfigurationPropertySourceLocator(AppConfigurationProviderProperties appProperties,
- AppConfigurationReplicaClientFactory clientFactory, AppConfigurationKeyVaultClientFactory keyVaultClientFactory,
- Duration refreshInterval, List configStores, ReplicaLookUp replicaLookUp,
- FeatureFlagClient featureFlagLoader) {
- this.refreshInterval = refreshInterval;
- this.appProperties = appProperties;
- this.configStores = configStores;
- this.clientFactory = clientFactory;
- this.keyVaultClientFactory = keyVaultClientFactory;
- this.replicaLookUp = replicaLookUp;
- this.featureFlagClient = featureFlagLoader;
-
- BackoffTimeCalculator.setDefaults(appProperties.getDefaultMaxBackoff(), appProperties.getDefaultMinBackoff());
- }
-
- @Override
- public PropertySource> locate(Environment environment) {
- if (!(environment instanceof ConfigurableEnvironment)) {
- return null;
- }
- replicaLookUp.updateAutoFailoverEndpoints();
-
- ConfigurableEnvironment env = (ConfigurableEnvironment) environment;
- boolean currentlyLoaded = env.getPropertySources().stream().anyMatch(source -> {
- String storeName = configStores.get(0).getEndpoint();
- if (configStores.get(0).getSelects().size() == 0) {
- return false;
- }
- AppConfigurationKeyValueSelector selectedKey = configStores.get(0).getSelects().get(0);
- return source.getName()
- .startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME + "-" + selectedKey.getKeyFilter() + storeName + "/");
- });
- if (currentlyLoaded && !env.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
- return null;
- }
-
- List profiles = Arrays.asList(env.getActiveProfiles());
-
- CompositePropertySource composite = new CompositePropertySource(PROPERTY_SOURCE_NAME);
- Collections.reverse(configStores); // Last store has the highest precedence
-
- StateHolder newState = new StateHolder();
- newState.setNextForcedRefresh(refreshInterval);
-
- // Feature Management needs to be set in the last config store.
- for (ConfigStore configStore : configStores) {
- boolean loadNewPropertySources = STARTUP.get() || StateHolder.getLoadState(configStore.getEndpoint());
-
- if (configStore.isEnabled() && loadNewPropertySources) {
- // There is only one Feature Set for all AppConfigurationPropertySources
-
- List clients = clientFactory
- .getAvailableClients(configStore.getEndpoint(), true);
-
- boolean generatedPropertySources = false;
-
- List sourceList = new ArrayList<>();
- boolean reloadFailed = false;
-
- for (AppConfigurationReplicaClient client : clients) {
- sourceList = new ArrayList<>();
-
- if (!STARTUP.get() && reloadFailed && !AppConfigurationRefreshUtil
- .checkStoreAfterRefreshFailed(client, clientFactory, configStore.getFeatureFlags())) {
- // This store doesn't have any changes where to refresh store did. Skipping Checking next.
- continue;
- }
-
- // Reverse in order to add Profile specific properties earlier, and last profile comes first
- try {
- List sources = createSettings(client, configStore, profiles);
- List featureFlags = createFeatureFlags(client, configStore, profiles);
- sourceList.addAll(sources);
-
- LOGGER.debug("PropertySource context.");
- setupMonitoring(configStore, client, sources, newState, featureFlags);
-
- generatedPropertySources = true;
- } catch (AppConfigurationStatusException e) {
- reloadFailed = true;
- clientFactory.backoffClientClient(configStore.getEndpoint(), client.getEndpoint());
- } catch (Exception e) {
- newState = failedToGeneratePropertySource(configStore, newState, e);
-
- // Not a retiable error
- break;
- }
- if (generatedPropertySources) {
- break;
- }
- }
-
- if (generatedPropertySources) {
- // Updating list of propertySources
- sourceList.forEach(composite::addPropertySource);
- } else if (!STARTUP.get() || (configStore.isFailFast() && STARTUP.get())) {
- String message = "Failed to generate property sources for " + configStore.getEndpoint();
-
- // Refresh failed for a config store ending attempt
- failedToGeneratePropertySource(configStore, newState, new RuntimeException(message));
- }
-
- if (featureFlagClient.getProperties().size() > 0) {
- // This can be true if feature flags are enabled or if a Snapshot contained feature flags
- AppConfigurationFeatureManagementPropertySource acfmps = new AppConfigurationFeatureManagementPropertySource(
- featureFlagClient);
- composite.addPropertySource(acfmps);
- }
-
- } else if (!configStore.isEnabled() && loadNewPropertySources) {
- LOGGER.info("Not loading configurations from {} as it is not enabled.", configStore.getEndpoint());
- } else {
- LOGGER.warn("Not loading configurations from {} as it failed on startup.", configStore.getEndpoint());
- }
- }
-
- StateHolder.updateState(newState);
- STARTUP.set(false);
-
- return composite;
- }
-
- private void setupMonitoring(ConfigStore configStore, AppConfigurationReplicaClient client,
- List sources, StateHolder newState, List featureFlags) {
- AppConfigurationStoreMonitoring monitoring = configStore.getMonitoring();
-
- if (configStore.getFeatureFlags().getEnabled()) {
- newState.setStateFeatureFlag(configStore.getEndpoint(), featureFlags,
- monitoring.getFeatureFlagRefreshInterval());
- }
-
- if (monitoring.isEnabled()) {
- // Setting new ETag values for Watch
- List watchKeysSettings = monitoring.getTriggers().stream()
- .map(trigger -> client.getWatchKey(trigger.getKey(), trigger.getLabel(), !STARTUP.get())).toList();
-
- newState.setState(configStore.getEndpoint(), watchKeysSettings, monitoring.getRefreshInterval());
- }
- newState.setLoadState(configStore.getEndpoint(), true, configStore.isFailFast());
- }
-
- private StateHolder failedToGeneratePropertySource(ConfigStore configStore, StateHolder newState, Exception e) {
- String message = "Failed to generate property sources for " + configStore.getEndpoint();
- if (!STARTUP.get()) {
- // Need to check for refresh first, or reset will never happen if fail fast is true.
- LOGGER.error("Refreshing failed while reading configuration from Azure App Configuration store "
- + configStore.getEndpoint() + ".");
-
- if (refreshInterval != null) {
- // The next refresh will happen sooner if refresh interval is expired.
- newState.updateNextRefreshTime(refreshInterval, appProperties.getDefaultMinBackoff());
- }
- throw new RuntimeException(message, e);
- } else if (configStore.isFailFast()) {
- LOGGER.error("Fail fast is set and there was an error reading configuration from Azure App "
- + "Configuration store " + configStore.getEndpoint() + ".");
- delayException();
- throw new RuntimeException(message, e);
- } else {
- LOGGER.warn(
- "Unable to load configuration from Azure AppConfiguration store " + configStore.getEndpoint() + ".", e);
- newState.setLoadState(configStore.getEndpoint(), false, configStore.isFailFast());
- }
- return newState;
- }
-
- /**
- * Creates a new set of AppConfigurationPropertySources, 1 per Label.
- *
- * @param client client for connecting to App Configuration
- * @param store Config Store the PropertySource is being generated from
- * @param profiles active profiles to be used as labels. it needs to be in the last one.
- * @return a list of AppConfigurationPropertySources
- * @throws Exception creating a property source failed
- */
- private List createSettings(AppConfigurationReplicaClient client, ConfigStore store,
- List profiles) throws Exception {
- List sourceList = new ArrayList<>();
- List selects = store.getSelects();
-
- for (AppConfigurationKeyValueSelector selectedKeys : selects) {
- AppConfigurationPropertySource propertySource = null;
-
- if (StringUtils.hasText(selectedKeys.getSnapshotName())) {
- propertySource = new AppConfigurationSnapshotPropertySource(
- selectedKeys.getSnapshotName() + "/" + store.getEndpoint(), client, keyVaultClientFactory,
- selectedKeys.getSnapshotName(), featureFlagClient);
- } else {
- propertySource = new AppConfigurationApplicationSettingPropertySource(
- selectedKeys.getKeyFilter() + store.getEndpoint() + "/", client, keyVaultClientFactory,
- selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles));
- }
- propertySource.initProperties(store.getTrimKeyPrefix(), !STARTUP.get());
- sourceList.add(propertySource);
-
- }
-
- return sourceList;
- }
-
- /**
- * Creates a new set of AppConfigurationPropertySources, 1 per Label.
- *
- * @param client client for connecting to App Configuration
- * @param store Config Store the PropertySource is being generated from
- * @param profiles active profiles to be used as labels. it needs to be in the last one.
- * @return a list of AppConfigurationPropertySources
- * @throws Exception creating a property source failed
- */
- private List createFeatureFlags(AppConfigurationReplicaClient client, ConfigStore store,
- List profiles) throws Exception {
- List featureFlagWatchKeys = new ArrayList<>();
- if (store.getFeatureFlags().getEnabled()) {
- for (FeatureFlagKeyValueSelector selectedKeys : store.getFeatureFlags().getSelects()) {
- List storesFeatureFlags = featureFlagClient.loadFeatureFlags(client,
- selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles), !STARTUP.get());
- storesFeatureFlags.forEach(featureFlags -> featureFlags.setConfigStore(store));
- featureFlagWatchKeys.addAll(storesFeatureFlags);
- }
- }
- return featureFlagWatchKeys;
- }
-
- private void delayException() {
- Instant currentDate = Instant.now();
- Instant preKillTIme = appProperties.getStartDate().plusSeconds(appProperties.getPrekillTime());
- if (currentDate.isBefore(preKillTIme)) {
- long diffInMillies = Math.abs(preKillTIme.toEpochMilli() - currentDate.toEpochMilli());
- try {
- Thread.sleep(diffInMillies);
- } catch (InterruptedException e) {
- LOGGER.error("Failed to wait before fast fail.");
- }
- }
- }
-}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java
index 44c59c1583a2..cb5d5735d39b 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationPullRefresh.java
@@ -20,55 +20,81 @@
import reactor.core.publisher.Mono;
/**
- * Enables checking of Configuration updates.
+ * Component responsible for checking Azure App Configuration for updates and triggering refresh events.
*/
@Component
public class AppConfigurationPullRefresh implements AppConfigurationRefresh {
private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPullRefresh.class);
+ /**
+ * Flag to prevent concurrent refresh operations.
+ */
private final AtomicBoolean running = new AtomicBoolean(false);
+ /**
+ * Publisher for Spring refresh events.
+ */
private ApplicationEventPublisher publisher;
+ private final Long defaultMinBackoff = (long) 30;
- private final Long defaultMinBackoff;
+ /**
+ * Default minimum backoff duration in seconds when refresh operations fail.
+ */
+ private static final Long DEFAULT_MIN_BACKOFF_SECONDS = 30L;
+ /**
+ * Factory for creating App Configuration replica clients.
+ */
private final AppConfigurationReplicaClientFactory clientFactory;
+ /**
+ * Time interval between configuration refresh checks.
+ */
private final Duration refreshInterval;
-
+
+ /**
+ * Component for replica lookup and failover functionality.
+ */
private final ReplicaLookUp replicaLookUp;
-
+
+ /**
+ * Utility component for refresh operations.
+ */
private final AppConfigurationRefreshUtil refreshUtils;
/**
- * Component used for checking for and triggering configuration refreshes.
+ * Creates a new AppConfigurationPullRefresh component.
*
- * @param clientFactory Clients stores used to connect to App Configuration. * @param defaultMinBackoff default
- * @param refreshInterval time between refresh intervals
- * @param defaultMinBackoff minimum time between backoff retries minimum backoff time
+ * @param clientFactory factory for creating App Configuration clients to connect to stores
+ * @param refreshInterval time duration between refresh interval checks
+ * @param replicaLookUp component for handling replica lookup and failover
+ * @param refreshUtils utility component for refresh operations
*/
public AppConfigurationPullRefresh(AppConfigurationReplicaClientFactory clientFactory, Duration refreshInterval,
- Long defaultMinBackoff, ReplicaLookUp replicaLookUp, AppConfigurationRefreshUtil refreshUtils) {
- this.defaultMinBackoff = defaultMinBackoff;
+ ReplicaLookUp replicaLookUp, AppConfigurationRefreshUtil refreshUtils) {
this.refreshInterval = refreshInterval;
this.clientFactory = clientFactory;
this.replicaLookUp = replicaLookUp;
this.refreshUtils = refreshUtils;
-
}
+ /**
+ * Sets the Spring application event publisher for publishing refresh events.
+ *
+ * @param applicationEventPublisher the Spring event publisher to use for refresh events
+ */
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
/**
- * Checks configurations to see if configurations should be reloaded. If the refresh interval has passed and a
- * trigger has been updated configuration are reloaded.
- *
- * @return Future with a boolean of if a RefreshEvent was published. If refreshConfigurations is currently being run
- * elsewhere this method will return right away as false.
+ * Checks configurations to see if they should be reloaded. If the refresh interval has passed and a trigger has
+ * been updated, configurations are reloaded.
+ *
+ * @return a Mono containing a boolean indicating if a RefreshEvent was published. Returns {@code false} if
+ * refreshConfigurations is currently being executed elsewhere.
*/
public Mono refreshConfigurations() {
return Mono.just(refreshStores());
@@ -76,9 +102,10 @@ public Mono refreshConfigurations() {
/**
* Soft expires refresh interval. Sets amount of time to next refresh to be a random value between 0 and 15 seconds,
- * unless value is less than the amount of time to the next refresh check.
- * @param endpoint Config Store endpoint to expire refresh interval on.
- * @param syncToken syncToken to verify the latest changes are available on pull
+ * unless that value is less than the amount of time to the next refresh check.
+ *
+ * @param endpoint the Config Store endpoint to expire refresh interval on
+ * @param syncToken the syncToken to verify the latest changes are available on pull
*/
public void expireRefreshInterval(String endpoint, String syncToken) {
LOGGER.debug("Expiring refresh interval for " + endpoint);
@@ -92,22 +119,23 @@ public void expireRefreshInterval(String endpoint, String syncToken) {
/**
* Goes through each config store and checks if any of its keys need to be refreshed. If any store has a value that
- * needs to be updated a refresh event is called after every store is checked.
+ * needs to be updated, a refresh event is called after every store is checked.
*
- * @return If a refresh event is called.
+ * @return true if a refresh event is published, false otherwise
*/
private boolean refreshStores() {
if (running.compareAndSet(false, true)) {
try {
RefreshEventData eventData = refreshUtils.refreshStoresCheck(clientFactory,
- refreshInterval, defaultMinBackoff, replicaLookUp);
+ refreshInterval, DEFAULT_MIN_BACKOFF_SECONDS, replicaLookUp);
if (eventData.getDoRefresh()) {
publisher.publishEvent(new RefreshEvent(this, eventData, eventData.getMessage()));
return true;
}
} catch (Exception e) {
+ LOGGER.warn("Error occurred during configuration refresh, will retry at next interval", e);
// The next refresh will happen sooner if refresh interval is expired.
- StateHolder.getCurrentState().updateNextRefreshTime(refreshInterval, defaultMinBackoff);
+ StateHolder.getCurrentState().updateNextRefreshTime(refreshInterval, DEFAULT_MIN_BACKOFF_SECONDS);
throw e;
} finally {
running.set(false);
@@ -116,6 +144,11 @@ private boolean refreshStores() {
return false;
}
+ /**
+ * Gets the health status of all configured App Configuration stores.
+ *
+ * @return a map containing the health status of each store, keyed by store identifier
+ */
@Override
public Map getAppConfigurationStoresHealth() {
return clientFactory.getHealth();
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java
index ca1cc33c3905..f347bbe28e96 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationRefreshUtil.java
@@ -2,6 +2,8 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.appconfiguration.config.implementation;
+import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH;
+
import java.time.Duration;
import java.time.Instant;
import java.util.List;
@@ -9,24 +11,33 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.util.StringUtils;
import com.azure.core.exception.HttpResponseException;
+import com.azure.core.util.Context;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlagState;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification;
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore;
+/**
+ * Utility class for handling Azure App Configuration refresh operations.
+ */
public class AppConfigurationRefreshUtil {
- private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationPullRefresh.class);
+ private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationRefreshUtil.class);
/**
- * Goes through each config store and checks if any of its keys need to be refreshed. If any store has a value that
- * needs to be updated a refresh event is called after every store is checked.
+ * Checks all configured stores to determine if any configurations need to be refreshed.
*
- * @return If a refresh event is called.
+ * @param clientFactory factory for accessing App Configuration clients
+ * @param refreshInterval the time interval between refresh checks
+ * @param defaultMinBackoff the minimum backoff time for failed operations
+ * @param replicaLookUp component for handling replica failover
+ * @return RefreshEventData containing information about whether a refresh should occur
*/
RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientFactory, Duration refreshInterval,
Long defaultMinBackoff, ReplicaLookUp replicaLookUp) {
@@ -51,13 +62,23 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF
AppConfigurationStoreMonitoring monitor = connection.getMonitoring();
+ boolean pushRefresh = false;
+ PushNotification notification = monitor.getPushNotification();
+ if ((notification.getPrimaryToken() != null
+ && StringUtils.hasText(notification.getPrimaryToken().getName()))
+ || (notification.getSecondaryToken() != null
+ && StringUtils.hasText(notification.getPrimaryToken().getName()))) {
+ pushRefresh = true;
+ }
+ Context context = new Context("refresh", true).addData(PUSH_REFRESH, pushRefresh);
+
List clients = clientFactory.getAvailableClients(originEndpoint);
if (monitor.isEnabled() && StateHolder.getLoadState(originEndpoint)) {
for (AppConfigurationReplicaClient client : clients) {
try {
refreshWithTime(client, StateHolder.getState(originEndpoint), monitor.getRefreshInterval(),
- eventData, replicaLookUp);
+ eventData, replicaLookUp, context);
if (eventData.getDoRefresh()) {
clientFactory.setCurrentConfigStoreClient(originEndpoint, client.getEndpoint());
return eventData;
@@ -66,9 +87,11 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF
break;
} catch (HttpResponseException e) {
LOGGER.warn(
- "Failed attempting to connect to " + client.getEndpoint() + " during refresh check.");
+ "Failed to connect to App Configuration store {} during configuration refresh check. "
+ + "Status: {}, Message: {}",
+ client.getEndpoint(), e.getResponse().getStatusCode(), e.getMessage());
- clientFactory.backoffClientClient(originEndpoint, client.getEndpoint());
+ clientFactory.backoffClient(originEndpoint, client.getEndpoint());
}
}
} else {
@@ -81,7 +104,7 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF
for (AppConfigurationReplicaClient client : clients) {
try {
refreshWithTimeFeatureFlags(client, StateHolder.getStateFeatureFlag(originEndpoint),
- monitor.getFeatureFlagRefreshInterval(), eventData, replicaLookUp);
+ monitor.getFeatureFlagRefreshInterval(), eventData, replicaLookUp, context);
if (eventData.getDoRefresh()) {
clientFactory.setCurrentConfigStoreClient(originEndpoint, client.getEndpoint());
return eventData;
@@ -90,9 +113,11 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF
break;
} catch (HttpResponseException e) {
LOGGER.warn(
- "Failed attempting to connect to " + client.getEndpoint() + " during refresh check.");
+ "Failed to connect to App Configuration store {} during feature flag refresh check. "
+ + "Status: {}, Message: {}",
+ client.getEndpoint(), e.getResponse().getStatusCode(), e.getMessage());
- clientFactory.backoffClientClient(originEndpoint, client.getEndpoint());
+ clientFactory.backoffClient(originEndpoint, client.getEndpoint());
}
}
} else {
@@ -108,41 +133,39 @@ RefreshEventData refreshStoresCheck(AppConfigurationReplicaClientFactory clientF
return eventData;
}
- static boolean checkStoreAfterRefreshFailed(AppConfigurationReplicaClient client,
- AppConfigurationReplicaClientFactory clientFactory, FeatureFlagStore featureStore) {
- return refreshStoreCheck(client, clientFactory.findOriginForEndpoint(client.getEndpoint()))
- || refreshStoreFeatureFlagCheck(featureStore, client);
- }
-
/**
- * This is for a refresh fail only.
+ * Performs a refresh check for a specific store client without time constraints. This method is used for refresh
+ * failure scenarios only.
*
- * @param client Client checking for refresh
- * @param originEndpoint config store origin endpoint
- * @return A refresh should be triggered.
+ * @param client the client for checking refresh status
+ * @param originEndpoint the original config store endpoint
+ * @param context the operation context
+ * @return true if a refresh should be triggered, false otherwise
*/
- private static boolean refreshStoreCheck(AppConfigurationReplicaClient client, String originEndpoint) {
+ static boolean refreshStoreCheck(AppConfigurationReplicaClient client, String originEndpoint, Context context) {
RefreshEventData eventData = new RefreshEventData();
if (StateHolder.getLoadState(originEndpoint)) {
- refreshWithoutTime(client, StateHolder.getState(originEndpoint).getWatchKeys(), eventData);
+ refreshWithoutTime(client, StateHolder.getState(originEndpoint).getWatchKeys(), eventData, context);
}
return eventData.getDoRefresh();
}
/**
- * This is for a refresh fail only.
+ * Performs a feature flag refresh check for a specific store client. This method is used for refresh failure
+ * scenarios only.
*
- * @param featureStore Feature info for the store
- * @param client Client checking for refresh
- * @return true if a refresh should be triggered.
+ * @param featureStoreEnabled whether feature store is enabled
+ * @param client the client for checking refresh status
+ * @param context the operation context
+ * @return true if a refresh should be triggered, false otherwise
*/
- private static boolean refreshStoreFeatureFlagCheck(FeatureFlagStore featureStore,
- AppConfigurationReplicaClient client) {
+ static boolean refreshStoreFeatureFlagCheck(Boolean featureStoreEnabled,
+ AppConfigurationReplicaClient client, Context context) {
RefreshEventData eventData = new RefreshEventData();
String endpoint = client.getEndpoint();
- if (featureStore.getEnabled() && StateHolder.getStateFeatureFlag(endpoint) != null) {
- refreshWithoutTimeFeatureFlags(client, StateHolder.getStateFeatureFlag(endpoint), eventData);
+ if (featureStoreEnabled && StateHolder.getStateFeatureFlag(endpoint) != null) {
+ refreshWithoutTimeFeatureFlags(client, StateHolder.getStateFeatureFlag(endpoint), eventData, context);
} else {
LOGGER.debug("Skipping feature flag refresh check for " + endpoint);
}
@@ -150,33 +173,42 @@ private static boolean refreshStoreFeatureFlagCheck(FeatureFlagStore featureStor
}
/**
- * Checks refresh trigger for etag changes. If they have changed a RefreshEventData is published.
+ * Checks configuration refresh triggers for etag changes with time-based validation. Only performs the refresh
+ * check if the refresh interval has elapsed.
*
- * @param state The refresh state of the endpoint being checked.
- * @param refreshInterval Amount of time to wait until next check of this endpoint.
- * @param eventData Info for this refresh event.
+ * @param client the App Configuration client to use for checking
+ * @param state the current refresh state of the endpoint being checked
+ * @param refreshInterval the time duration to wait until next check of this endpoint
+ * @param eventData the refresh event data to update if changes are detected
+ * @param replicaLookUp component for updating auto-failover endpoints
+ * @param context the operation context
+ * @throws AppConfigurationStatusException if there's an error during the refresh check
*/
private static void refreshWithTime(AppConfigurationReplicaClient client, State state, Duration refreshInterval,
- RefreshEventData eventData, ReplicaLookUp replicaLookUp) throws AppConfigurationStatusException {
+ RefreshEventData eventData, ReplicaLookUp replicaLookUp, Context context)
+ throws AppConfigurationStatusException {
if (Instant.now().isAfter(state.getNextRefreshCheck())) {
replicaLookUp.updateAutoFailoverEndpoints();
- refreshWithoutTime(client, state.getWatchKeys(), eventData);
+ refreshWithoutTime(client, state.getWatchKeys(), eventData, context);
StateHolder.getCurrentState().updateStateRefresh(state, refreshInterval);
}
}
/**
- * Checks refresh trigger for etag changes. If they have changed a RefreshEventData is published.
+ * Checks configuration refresh triggers for etag changes without time validation. This method immediately checks
+ * all watch keys for changes regardless of refresh intervals.
*
- * @param client Client checking for refresh
- * @param watchKeys Watch keys for the store.
- * @param eventData Refresh event info
+ * @param client the App Configuration client to use for checking
+ * @param watchKeys the list of configuration settings to watch for changes
+ * @param eventData the refresh event data to update if changes are detected
+ * @param context the operation context
+ * @throws AppConfigurationStatusException if there's an error during the refresh check
*/
private static void refreshWithoutTime(AppConfigurationReplicaClient client, List watchKeys,
- RefreshEventData eventData) throws AppConfigurationStatusException {
+ RefreshEventData eventData, Context context) throws AppConfigurationStatusException {
for (ConfigurationSetting watchKey : watchKeys) {
- ConfigurationSetting watchedKey = client.getWatchKey(watchKey.getKey(), watchKey.getLabel(), true);
+ ConfigurationSetting watchedKey = client.getWatchKey(watchKey.getKey(), watchKey.getLabel(), context);
// If there is no result, etag will be considered empty.
// A refresh will trigger once the selector returns a value.
@@ -189,16 +221,27 @@ private static void refreshWithoutTime(AppConfigurationReplicaClient client, Lis
}
}
+ /**
+ * Checks feature flag refresh triggers with time-based validation. Only performs the refresh check if the refresh
+ * interval has elapsed.
+ *
+ * @param client the App Configuration client to use for checking
+ * @param state the current feature flag state of the endpoint being checked
+ * @param refreshInterval the time duration to wait until next check of this endpoint
+ * @param eventData the refresh event data to update if changes are detected
+ * @param replicaLookUp component for updating auto-failover endpoints
+ * @param context the operation context
+ * @throws AppConfigurationStatusException if there's an error during the refresh check
+ */
private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient client, FeatureFlagState state,
- Duration refreshInterval, RefreshEventData eventData, ReplicaLookUp replicaLookUp)
+ Duration refreshInterval, RefreshEventData eventData, ReplicaLookUp replicaLookUp, Context context)
throws AppConfigurationStatusException {
Instant date = Instant.now();
if (date.isAfter(state.getNextRefreshCheck())) {
replicaLookUp.updateAutoFailoverEndpoints();
for (FeatureFlags featureFlags : state.getWatchKeys()) {
-
- if (client.checkWatchKeys(featureFlags.getSettingSelector(), true)) {
+ if (client.checkWatchKeys(featureFlags.getSettingSelector(), context)) {
String eventDataInfo = ".appconfig.featureflag/*";
// Only one refresh Event needs to be call to update all of the
@@ -215,12 +258,21 @@ private static void refreshWithTimeFeatureFlags(AppConfigurationReplicaClient cl
}
}
+ /**
+ * Checks feature flag refresh triggers without time validation. This method immediately checks all feature flag
+ * watch keys for changes regardless of refresh intervals.
+ *
+ * @param client the App Configuration client to use for checking
+ * @param watchKeys the feature flag state containing watch keys to check for changes
+ * @param eventData the refresh event data to update if changes are detected
+ * @param context the operation context
+ * @throws AppConfigurationStatusException if there's an error during the refresh check
+ */
private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient client, FeatureFlagState watchKeys,
- RefreshEventData eventData) throws AppConfigurationStatusException {
+ RefreshEventData eventData, Context context) throws AppConfigurationStatusException {
for (FeatureFlags featureFlags : watchKeys.getWatchKeys()) {
-
- if (client.checkWatchKeys(featureFlags.getSettingSelector(), true)) {
+ if (client.checkWatchKeys(featureFlags.getSettingSelector(), context)) {
String eventDataInfo = ".appconfig.featureflag/*";
// Only one refresh Event needs to be call to update all of the
@@ -233,13 +285,23 @@ private static void refreshWithoutTimeFeatureFlags(AppConfigurationReplicaClient
}
}
+ /**
+ * Checks the etag values between watched and current configuration settings to determine if a refresh is needed.
+ *
+ * @param watchSetting the configuration setting being watched for changes
+ * @param currentTriggerConfiguration the current configuration setting from the store
+ * @param endpoint the endpoint of the configuration store
+ * @param eventData the refresh event data to update if a change is detected
+ */
private static void checkETag(ConfigurationSetting watchSetting, ConfigurationSetting currentTriggerConfiguration,
String endpoint, RefreshEventData eventData) {
if (currentTriggerConfiguration == null) {
return;
}
- LOGGER.debug(watchSetting.getETag(), " - ", currentTriggerConfiguration.getETag());
+ LOGGER.debug("Comparing eTags - watched: {} vs current: {}",
+ watchSetting.getETag(), currentTriggerConfiguration.getETag());
+
if (watchSetting.getETag() != null && !watchSetting.getETag().equals(currentTriggerConfiguration.getETag())) {
LOGGER.trace("Some keys in store [{}] matching the key [{}] and label [{}] is updated, "
+ "will send refresh event.", endpoint, watchSetting.getKey(), watchSetting.getLabel());
@@ -254,7 +316,7 @@ private static void checkETag(ConfigurationSetting watchSetting, ConfigurationSe
}
/**
- * For each refresh, multiple etags can change, but even one etag is changed, refresh is required.
+ * Data structure containing information about a refresh event.
*/
static class RefreshEventData {
@@ -264,25 +326,50 @@ static class RefreshEventData {
private boolean doRefresh = false;
+ /**
+ * Creates a new RefreshEventData with empty message and refresh flag set to false.
+ */
RefreshEventData() {
this.message = "";
}
+ /**
+ * Sets the refresh message using the standard message template.
+ *
+ * @param prefix the prefix to include in the message (typically a key name)
+ * @return this RefreshEventData instance for method chaining
+ */
RefreshEventData setMessage(String prefix) {
setFullMessage(String.format(MSG_TEMPLATE, prefix));
return this;
}
- RefreshEventData setFullMessage(String message) {
+ /**
+ * Sets the full refresh message and marks that a refresh should occur.
+ *
+ * @param message the complete message describing the refresh event
+ * @return this RefreshEventData instance for method chaining
+ */
+ private RefreshEventData setFullMessage(String message) {
this.message = message;
this.doRefresh = true;
return this;
}
+ /**
+ * Gets the refresh event message.
+ *
+ * @return the message describing what triggered the refresh
+ */
public String getMessage() {
return this.message;
}
+ /**
+ * Indicates whether a refresh should be performed.
+ *
+ * @return true if a refresh is needed, false otherwise
+ */
public boolean getDoRefresh() {
return doRefresh;
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java
index 9c43522cafcb..0cde712d5867 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClient.java
@@ -22,36 +22,43 @@
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.data.appconfiguration.models.SnapshotComposition;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
-import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo;
import io.netty.handler.codec.http.HttpResponseStatus;
/**
* Client for connecting to App Configuration when multiple replicas are in use.
+ *
+ * The client automatically retries retryable HTTP errors (429, 408, 5xx) and implements backoff logic to prevent
+ * overwhelming failing replicas.
*/
class AppConfigurationReplicaClient {
+ private static final long INITIAL_BACKOFF_OFFSET_MS = 1L;
+
+ /** HTTP status code for "Not Modified" responses */
+ private static final int HTTP_NOT_MODIFIED = 304;
private final String endpoint;
+ private final String originClient;
+
private final ConfigurationClient client;
private Instant backoffEndTime;
private int failedAttempts;
- private final TracingInfo tracingInfo;
-
/**
* Holds Configuration Client and info needed to manage backoff.
* @param endpoint client endpoint
+ * @param originClient origin client identifier
* @param client Configuration Client to App Configuration store
*/
- AppConfigurationReplicaClient(String endpoint, ConfigurationClient client, TracingInfo tracingInfo) {
+ AppConfigurationReplicaClient(String endpoint, String originClient, ConfigurationClient client) {
this.endpoint = endpoint;
+ this.originClient = originClient;
this.client = client;
- this.backoffEndTime = Instant.now().minusMillis(1);
+ this.backoffEndTime = Instant.now().minusMillis(INITIAL_BACKOFF_OFFSET_MS);
this.failedAttempts = 0;
- this.tracingInfo = tracingInfo;
}
/**
@@ -84,18 +91,26 @@ String getEndpoint() {
return endpoint;
}
+ /**
+ * @return originClient
+ */
+ String getOriginClient() {
+ return originClient;
+ }
+
/**
* Gets the Configuration Setting for the given config store that match the Setting Selector criteria. Follows
* retry-after-ms header.
*
* @param key String value of the watch key
* @param label String value of the watch key, use \0 for null.
+ * @param context Azure SDK context for request correlation
* @return The first returned configuration.
+ * @throws HttpResponseException if the request fails
*/
- ConfigurationSetting getWatchKey(String key, String label, Boolean isRefresh)
+ ConfigurationSetting getWatchKey(String key, String label, Context context)
throws HttpResponseException {
try {
- Context context = new Context("refresh", isRefresh);
ConfigurationSetting selector = new ConfigurationSetting().setKey(key).setLabel(label);
ConfigurationSetting watchKey = NormalizeNull
.normalizeNullLabel(
@@ -103,7 +118,7 @@ ConfigurationSetting getWatchKey(String key, String label, Boolean isRefresh)
this.failedAttempts = 0;
return watchKey;
} catch (HttpResponseException e) {
- throw hanndleHttpResponseException(e);
+ throw handleHttpResponseException(e);
} catch (UncheckedIOException e) {
throw new AppConfigurationStatusException(e.getMessage(), null, null);
}
@@ -113,13 +128,14 @@ ConfigurationSetting getWatchKey(String key, String label, Boolean isRefresh)
* Gets a list of Configuration Settings from the given config store that match the Setting Selector criteria.
*
* @param settingSelector Information on which setting to pull. i.e. number of results, key value...
+ * @param context Azure SDK context for request correlation
* @return List of Configuration Settings.
+ * @throws HttpResponseException if the request fails
*/
- List listSettings(SettingSelector settingSelector, Boolean isRefresh)
+ List listSettings(SettingSelector settingSelector, Context context)
throws HttpResponseException {
List configurationSettings = new ArrayList<>();
try {
- Context context = new Context("refresh", isRefresh);
PagedIterable settings = client.listConfigurationSettings(settingSelector, context);
settings.forEach(setting -> {
configurationSettings.add(NormalizeNull.normalizeNullLabel(setting));
@@ -128,17 +144,25 @@ List listSettings(SettingSelector settingSelector, Boolean
this.failedAttempts = 0;
return configurationSettings;
} catch (HttpResponseException e) {
- throw hanndleHttpResponseException(e);
+ throw handleHttpResponseException(e);
} catch (UncheckedIOException e) {
throw new AppConfigurationStatusException(e.getMessage(), null, null);
}
}
- FeatureFlags listFeatureFlags(SettingSelector settingSelector, Boolean isRefresh) throws HttpResponseException {
+ /**
+ * Lists feature flags from the Azure App Configuration store.
+ *
+ * @param settingSelector selector criteria for feature flags
+ * @param context Azure SDK context for request correlation
+ * @return FeatureFlags containing the retrieved feature flags and match conditions
+ * @throws HttpResponseException if the request fails
+ */
+ FeatureFlags listFeatureFlags(SettingSelector settingSelector, Context context)
+ throws HttpResponseException {
List configurationSettings = new ArrayList<>();
List checks = new ArrayList<>();
try {
- Context context = new Context("refresh", isRefresh);
client.listConfigurationSettings(settingSelector, context).streamByPage().forEach(pagedResponse -> {
checks.add(
new MatchConditions().setIfNoneMatch(pagedResponse.getHeaders().getValue(HttpHeaderName.ETAG)));
@@ -153,16 +177,27 @@ FeatureFlags listFeatureFlags(SettingSelector settingSelector, Boolean isRefresh
settingSelector.setMatchConditions(checks);
return new FeatureFlags(settingSelector, configurationSettings);
} catch (HttpResponseException e) {
- throw hanndleHttpResponseException(e);
+ throw handleHttpResponseException(e);
} catch (UncheckedIOException e) {
throw new AppConfigurationStatusException(e.getMessage(), null, null);
}
}
- List listSettingSnapshot(String snapshotName) {
+ /**
+ * Lists configuration settings from a specific snapshot.
+ *
+ * @param snapshotName the name of the snapshot to retrieve settings from
+ * @param context Azure SDK context for request correlation
+ * @return list of configuration settings from the snapshot
+ * @throws IllegalArgumentException if the snapshot is not of type KEY
+ * @throws HttpResponseException if the request fails
+ */
+ List listSettingSnapshot(String snapshotName, Context context) {
List configurationSettings = new ArrayList<>();
try {
- ConfigurationSnapshot snapshot = client.getSnapshot(snapshotName);
+ // Because Spring always refreshes all we still have to load snapshots on refresh to build the property
+ // sources.
+ ConfigurationSnapshot snapshot = client.getSnapshotWithResponse(snapshotName, null, context).getValue();
if (!SnapshotComposition.KEY.equals(snapshot.getSnapshotComposition())) {
throw new IllegalArgumentException("Snapshot " + snapshotName + " needs to be of type Key.");
}
@@ -172,16 +207,16 @@ List listSettingSnapshot(String snapshotName) {
settings.forEach(setting -> configurationSettings.add(NormalizeNull.normalizeNullLabel(setting)));
return configurationSettings;
} catch (HttpResponseException e) {
- throw hanndleHttpResponseException(e);
+ throw handleHttpResponseException(e);
} catch (UncheckedIOException e) {
throw new AppConfigurationStatusException(e.getMessage(), null, null);
}
}
- Boolean checkWatchKeys(SettingSelector settingSelector, Boolean isRefresh) {
- Context context = new Context("refresh", isRefresh);
- List> results = client.listConfigurationSettings(settingSelector, context)
- .streamByPage().filter(pagedResponse -> pagedResponse.getStatusCode() != 304).toList();
+ boolean checkWatchKeys(SettingSelector settingSelector, Context context) {
+ List> results = client
+ .listConfigurationSettings(settingSelector, context)
+ .streamByPage().filter(pagedResponse -> pagedResponse.getStatusCode() != HTTP_NOT_MODIFIED).toList();
return results.size() > 0;
}
@@ -195,7 +230,14 @@ void updateSyncToken(String syncToken) {
}
}
- private HttpResponseException hanndleHttpResponseException(HttpResponseException e) {
+ /**
+ * Handles HTTP response exceptions by determining if they are retryable.
+ *
+ * @param e the HTTP response exception to handle
+ * @return an AppConfigurationStatusException for retryable errors, or the original exception for non-retryable
+ * errors
+ */
+ private HttpResponseException handleHttpResponseException(HttpResponseException e) {
if (e.getResponse() != null) {
int statusCode = e.getResponse().getStatusCode();
@@ -208,8 +250,4 @@ private HttpResponseException hanndleHttpResponseException(HttpResponseException
return e;
}
- TracingInfo getTracingInfo() {
- return tracingInfo;
- }
-
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java
index 4f3fd987cc2f..63b2fd4db0d7 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientFactory.java
@@ -11,24 +11,27 @@
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore;
/**
- * Manages all client connections for all configuration stores.
+ * Manages all client connections for all configuration stores with support for replica failover.
*/
public class AppConfigurationReplicaClientFactory {
+ /** Map of connection managers keyed by origin endpoint */
private static final Map CONNECTIONS = new HashMap<>();
+ /** List of configured stores for endpoint resolution */
private final List configStores;
/**
- * Sets up Connections to all configuration stores.
+ * Sets up connections to all configuration stores with replica support.
*
- * @param clientBuilder builder for app configuration replica clients
- * @param configStores configuration info for config stores
+ * @param clientBuilder builder for creating app configuration replica clients
+ * @param configStores configuration information for all config stores
+ * @param replicaLookUp service for discovering and managing replica endpoints
*/
- public AppConfigurationReplicaClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder,
+ AppConfigurationReplicaClientFactory(AppConfigurationReplicaClientsBuilder clientBuilder,
List configStores, ReplicaLookUp replicaLookUp) {
this.configStores = configStores;
- if (CONNECTIONS.size() == 0) {
+ if (CONNECTIONS.isEmpty()) {
for (ConfigStore store : configStores) {
ConnectionManager manager = new ConnectionManager(clientBuilder, store, replicaLookUp);
CONNECTIONS.put(manager.getMainEndpoint(), manager);
@@ -37,42 +40,49 @@ public AppConfigurationReplicaClientFactory(AppConfigurationReplicaClientsBuilde
}
/**
- * @return the connections
+ * Gets all connection managers mapped by their origin endpoints.
+ *
+ * @return map of endpoint to connection manager
*/
public Map getConnections() {
return CONNECTIONS;
}
/**
- * Returns the current used endpoint for a given config store.
- * @param originEndpoint identifier of the store. The identifier is the primary endpoint of the store.
- * @return ConfigurationClient for accessing App Configuration
+ * Returns available replica clients for a given configuration store.
+ *
+ * @param originEndpoint identifier of the store (primary endpoint)
+ * @return list of available replica clients for the store
*/
List getAvailableClients(String originEndpoint) {
- return CONNECTIONS.get(originEndpoint).getAvailableClients();
+ return getAvailableClients(originEndpoint, false);
}
/**
- * Returns the current used endpoint for a given config store.
- * @param originEndpoint identifier of the store. The identifier is the primary endpoint of the store.
- * @return ConfigurationClient for accessing App Configuration
+ * Returns available replica clients for a given configuration store with current client preference.
+ *
+ * @param originEndpoint identifier of the store (primary endpoint)
+ * @param useCurrent whether to prefer the currently active client
+ * @return list of available replica clients for the store
*/
List getAvailableClients(String originEndpoint, Boolean useCurrent) {
return CONNECTIONS.get(originEndpoint).getAvailableClients(useCurrent);
}
/**
- * Sets backoff time for the current client that is being used, and attempts to get a new one.
- * @param originEndpoint identifier of the store. The identifier is the primary endpoint of the store.
- * @param endpoint replica endpoint
+ * Sets backoff time for a specific replica client due to connection failure.
+ *
+ * @param originEndpoint identifier of the store (primary endpoint)
+ * @param endpoint the specific replica endpoint that failed
*/
- void backoffClientClient(String originEndpoint, String endpoint) {
+ void backoffClient(String originEndpoint, String endpoint) {
CONNECTIONS.get(originEndpoint).backoffClient(endpoint);
}
/**
- * Gets the health of the client connections to App Configuration
- * @return map of endpoint origin it's health
+ * Gets the health status of all managed configuration store connections.
+ *
+ * @return map of origin endpoint to health status
*/
Map getHealth() {
Map health = new HashMap<>();
@@ -83,48 +93,38 @@ Map getHealth() {
}
/**
- * Returns the origin endpoint for a given endpoint. If not found will return the given endpoint;
+ * Finds the origin endpoint for a given replica endpoint.
*
- * @param endpoint App Configuration Endpoint
- * @return String Endpoint
+ * @param endpoint the replica endpoint to find the origin for
+ * @return the origin endpoint, or the input endpoint if no mapping is found
*/
String findOriginForEndpoint(String endpoint) {
for (ConfigStore store : configStores) {
- for (String replica : store.getEndpoints()) {
- if (replica.equals(endpoint)) {
- return store.getEndpoint();
- }
+ List replicas = store.getEndpoints();
+ if (replicas != null && replicas.contains(endpoint)) {
+ return store.getEndpoint();
}
}
return endpoint;
}
/**
- * Checks if a given endpoint has any configured replicas.
- * @param endpoint Endpoint to check for replicas
- * @return true if at least one other unique endpoint connects to the same configuration store
- */
- boolean hasReplicas(String endpoint) {
- String originEndpoint = findOriginForEndpoint(endpoint);
- for (ConfigStore store : configStores) {
- if (store.getEndpoint().equals(originEndpoint)) {
- if (store.getConnectionStrings().size() > 0 || store.getEndpoints().size() > 0) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
- * Sets the replica as the currently used endpoint for connecting to the config store.
- * @param originEndpoint Origin Configuration Store
- * @param replicaEndpoint Replica that was last successfully connected to.
+ * Sets the current active replica for a configuration store.
+ *
+ * @param originEndpoint the origin configuration store endpoint
+ * @param replicaEndpoint the replica endpoint that was successfully connected to
*/
void setCurrentConfigStoreClient(String originEndpoint, String replicaEndpoint) {
CONNECTIONS.get(originEndpoint).setCurrentClient(replicaEndpoint);
}
+ /**
+ * Updates the sync token for a specific replica endpoint.
+ *
+ * @param originEndpoint the origin configuration store endpoint
+ * @param endpoint the specific replica endpoint
+ * @param syncToken the new sync token to store
+ */
void updateSyncToken(String originEndpoint, String endpoint, String syncToken) {
CONNECTIONS.get(originEndpoint).updateSyncToken(endpoint, syncToken);
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java
index 3e225980e1fd..6b4d42827626 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationReplicaClientsBuilder.java
@@ -3,6 +3,8 @@
package com.azure.spring.cloud.appconfiguration.config.implementation;
import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
@@ -14,8 +16,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.convert.DurationStyle;
-import org.springframework.context.EnvironmentAware;
-import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -25,7 +25,8 @@
import com.azure.core.util.Configuration;
import com.azure.core.util.CoreUtils;
import com.azure.data.appconfiguration.ConfigurationClientBuilder;
-import com.azure.identity.ManagedIdentityCredentialBuilder;
+import com.azure.identity.DefaultAzureCredential;
+import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.spring.cloud.appconfiguration.config.ConfigurationClientCustomizer;
import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.BaseAppConfigurationPolicy;
import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.TracingInfo;
@@ -37,22 +38,22 @@
import com.azure.spring.cloud.core.service.AzureServiceType.AppConfiguration;
import com.azure.spring.cloud.service.implementation.appconfiguration.ConfigurationClientBuilderFactory;
-public class AppConfigurationReplicaClientsBuilder implements EnvironmentAware {
+public class AppConfigurationReplicaClientsBuilder {
private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationReplicaClientsBuilder.class);
/**
* Invalid Connection String error message
*/
- public static final String NON_EMPTY_MSG = "%s property should not be null or empty in the connection string of Azure Config Service.";
+ private static final String NON_EMPTY_MSG = "%s property should not be null or empty in the connection string of Azure Config Service.";
- public static final String RETRY_MODE_PROPERTY_NAME = "retry.mode";
+ private static final String RETRY_MODE_PROPERTY_NAME = "retry.mode";
- public static final String MAX_RETRIES_PROPERTY_NAME = "retry.exponential.max-retries";
+ private static final String MAX_RETRIES_PROPERTY_NAME = "retry.exponential.max-retries";
- public static final String BASE_DELAY_PROPERTY_NAME = "retry.exponential.base-delay";
+ private static final String BASE_DELAY_PROPERTY_NAME = "retry.exponential.base-delay";
- public static final String MAX_DELAY_PROPERTY_NAME = "retry.exponential.max-delay";
+ private static final String MAX_DELAY_PROPERTY_NAME = "retry.exponential.max-delay";
private static final Duration DEFAULT_MIN_RETRY_POLICY = Duration.ofMillis(800);
@@ -66,30 +67,27 @@ public class AppConfigurationReplicaClientsBuilder implements EnvironmentAware {
/**
* Invalid Formatted Connection String Error message
*/
- public static final String ENDPOINT_ERR_MSG = String.format("Connection string does not follow format %s.",
+ private static final String ENDPOINT_ERR_MSG = String.format("Connection string does not follow format %s.",
CONN_STRING_REGEXP);
private static final Pattern CONN_STRING_PATTERN = Pattern.compile(CONN_STRING_REGEXP);
- private ConfigurationClientCustomizer clientProvider;
+ private ConfigurationClientCustomizer clientCustomizer;
private final ConfigurationClientBuilderFactory clientFactory;
- private Environment env;
-
- private boolean isDev = false;
-
- private boolean isKeyVaultConfigured = false;
+ private boolean isKeyVaultConfigured;
private final boolean credentialConfigured;
- private final int defaultMaxRetries;
+ private final int defaultMaxRetries = 2;
- public AppConfigurationReplicaClientsBuilder(int defaultMaxRetries, ConfigurationClientBuilderFactory clientFactory,
- boolean credentialConfigured) {
- this.defaultMaxRetries = defaultMaxRetries;
- this.clientFactory = clientFactory;
+ AppConfigurationReplicaClientsBuilder(ConfigurationClientBuilderFactory clientFactory,
+ ConfigurationClientCustomizer clientCustomizer, boolean credentialConfigured, boolean isKeyVaultConfigured) {
this.credentialConfigured = credentialConfigured;
+ this.clientFactory = clientFactory;
+ this.clientCustomizer = clientCustomizer;
+ this.isKeyVaultConfigured = isKeyVaultConfigured;
}
/**
@@ -114,17 +112,6 @@ public static String getEndpointFromConnectionString(String connectionString) {
return endpoint;
}
- /**
- * @param clientProvider the clientProvider to set
- */
- public void setClientProvider(ConfigurationClientCustomizer clientProvider) {
- this.clientProvider = clientProvider;
- }
-
- public void setIsKeyVaultConfigured(boolean isKeyVaultConfigured) {
- this.isKeyVaultConfigured = isKeyVaultConfigured;
- }
-
/**
* Builds all the clients for a connection.
*
@@ -169,79 +156,64 @@ List buildClients(ConfigStore configStore) {
LOGGER.debug("Connecting to " + endpoint + " using Connecting String.");
ConfigurationClientBuilder builder = createBuilderInstance().connectionString(connectionString);
- clients.add(modifyAndBuildClient(builder, endpoint, connectionStrings.size() - 1));
+ clients.add(modifyAndBuildClient(builder, endpoint, configStore.getEndpoint(), connectionStrings.size() - 1));
}
} else {
+ DefaultAzureCredential defautAzureCredential = new DefaultAzureCredentialBuilder().build();
for (String endpoint : endpoints) {
ConfigurationClientBuilder builder = this.createBuilderInstance();
if (!credentialConfigured) {
- // System Assigned Identity. Needs to be checked last as all of the above should
- // have an Endpoint.
- LOGGER.debug("Connecting to {} using Azure System Assigned Identity.", endpoint);
- builder.credential(new ManagedIdentityCredentialBuilder().build());
+ builder.credential(defautAzureCredential);
}
builder.endpoint(endpoint);
- clients.add(modifyAndBuildClient(builder, endpoint, endpoints.size() - 1));
+ clients.add(modifyAndBuildClient(builder, endpoint, configStore.getEndpoint(), endpoints.size() - 1));
}
}
return clients;
}
- public AppConfigurationReplicaClient buildClient(String failoverEndpoint, ConfigStore configStore) {
+ AppConfigurationReplicaClient buildClient(String failoverEndpoint, ConfigStore configStore) {
if (StringUtils.hasText(configStore.getConnectionString())) {
ConnectionString connectionString = new ConnectionString(configStore.getConnectionString());
connectionString.setUri(failoverEndpoint);
ConfigurationClientBuilder builder = createBuilderInstance().connectionString(connectionString.toString());
- return modifyAndBuildClient(builder, failoverEndpoint, 0);
+ return modifyAndBuildClient(builder, failoverEndpoint, configStore.getEndpoint(), 0);
} else if (configStore.getConnectionStrings().size() > 0) {
ConnectionString connectionString = new ConnectionString(configStore.getConnectionStrings().get(0));
connectionString.setUri(failoverEndpoint);
ConfigurationClientBuilder builder = createBuilderInstance().connectionString(connectionString.toString());
- return modifyAndBuildClient(builder, failoverEndpoint, 0);
+ return modifyAndBuildClient(builder, failoverEndpoint, configStore.getEndpoint(), 0);
} else {
ConfigurationClientBuilder builder = createBuilderInstance();
if (!credentialConfigured) {
- // System Assigned Identity. Needs to be checked last as all of the above should
- // have an Endpoint.
- LOGGER.debug("Connecting to {} using Azure System Assigned Identity.", failoverEndpoint);
- builder.credential(new ManagedIdentityCredentialBuilder().build());
+ builder.credential(new DefaultAzureCredentialBuilder().build());
}
builder.endpoint(failoverEndpoint);
- return modifyAndBuildClient(builder, failoverEndpoint, 0);
+ return modifyAndBuildClient(builder, failoverEndpoint, configStore.getEndpoint(), 0);
}
}
- private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBuilder builder, String endpoint,
+ private AppConfigurationReplicaClient modifyAndBuildClient(ConfigurationClientBuilder builder, String endpoint, String originEndpoint,
Integer replicaCount) {
- TracingInfo tracingInfo = new TracingInfo(isDev, isKeyVaultConfigured, replicaCount,
+ TracingInfo tracingInfo = new TracingInfo(isKeyVaultConfigured, replicaCount,
Configuration.getGlobalConfiguration());
builder.addPolicy(new BaseAppConfigurationPolicy(tracingInfo));
- if (clientProvider != null) {
- clientProvider.customize(builder, endpoint);
+ if (clientCustomizer != null) {
+ clientCustomizer.customize(builder, endpoint);
}
- return new AppConfigurationReplicaClient(endpoint, builder.buildClient(), tracingInfo);
+ return new AppConfigurationReplicaClient(endpoint, originEndpoint, builder.buildClient());
}
- @Override
- public void setEnvironment(Environment environment) {
- for (String profile : environment.getActiveProfiles()) {
- if ("dev".equalsIgnoreCase(profile)) {
- this.isDev = true;
- break;
- }
- }
- this.env = environment;
- }
-
- protected ConfigurationClientBuilder createBuilderInstance() {
+ private ConfigurationClientBuilder createBuilderInstance() {
RetryStrategy retryStatagy = null;
- String mode = env.getProperty(AzureGlobalProperties.PREFIX + "." + RETRY_MODE_PROPERTY_NAME);
- String modeService = env.getProperty(AzureAppConfigurationProperties.PREFIX + "." + RETRY_MODE_PROPERTY_NAME);
+ String mode = System.getProperty(AzureGlobalProperties.PREFIX + "." + RETRY_MODE_PROPERTY_NAME);
+ String modeService = System
+ .getProperty(AzureAppConfigurationProperties.PREFIX + "." + RETRY_MODE_PROPERTY_NAME);
if ("exponential".equals(mode) || "exponential".equals(modeService) || (mode == null && modeService == null)) {
Function checkPropertyInt = parameter -> (Integer.parseInt(parameter));
@@ -322,7 +294,6 @@ private static class ConnectionString {
private final String secret;
- @SuppressWarnings("deprecation")
ConnectionString(String connectionString) {
if (CoreUtils.isNullOrEmpty(connectionString)) {
throw new IllegalArgumentException("'connectionString' cannot be null or empty.");
@@ -338,9 +309,11 @@ private static class ConnectionString {
String segment = arg.trim();
if (ENDPOINT.regionMatches(true, 0, segment, 0, ENDPOINT.length())) {
try {
- baseUri = new URL(segment.substring(ENDPOINT.length()));
+ baseUri = new URI(segment.substring(ENDPOINT.length())).toURL();
} catch (MalformedURLException ex) {
throw new IllegalArgumentException(ex);
+ } catch (URISyntaxException ex) {
+ throw new IllegalArgumentException(ex);
}
} else if (ID.regionMatches(true, 0, segment, 0, ID.length())) {
id = segment.substring(ID.length());
@@ -358,12 +331,13 @@ private static class ConnectionString {
}
}
- @SuppressWarnings("deprecation")
protected ConnectionString setUri(String uri) {
try {
- this.baseUri = new URL(uri);
+ this.baseUri = new URI(uri).toURL();
} catch (MalformedURLException ex) {
throw new IllegalArgumentException(ex);
+ } catch (URISyntaxException ex) {
+ throw new IllegalArgumentException(ex);
}
return this;
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java
index e4569b384150..e585997e05a5 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AppConfigurationSnapshotPropertySource.java
@@ -7,6 +7,7 @@
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
+import com.azure.core.util.Context;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
@@ -42,10 +43,11 @@ final class AppConfigurationSnapshotPropertySource extends AppConfigurationAppli
*
*
* @param trim prefix to trim
+ * @param isRefresh true if a refresh triggered the loading of the Snapshot.
* @throws InvalidConfigurationPropertyValueException thrown if fails to parse Json content type
*/
- public void initProperties(List trim) throws InvalidConfigurationPropertyValueException {
- processConfigurationSettings(replicaClient.listSettingSnapshot(snapshotName), null, trim);
+ public void initProperties(List trim, Context context) throws InvalidConfigurationPropertyValueException {
+ processConfigurationSettings(replicaClient.listSettingSnapshot(snapshotName, context), null, trim);
FeatureFlags featureFlags = new FeatureFlags(null, featureFlagsList);
featureFlagClient.proccessFeatureFlags(featureFlags, replicaClient.getEndpoint());
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigBoostrapRegistrar.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigBoostrapRegistrar.java
new file mode 100644
index 000000000000..fb0a090f0910
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigBoostrapRegistrar.java
@@ -0,0 +1,139 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.azure.spring.cloud.appconfiguration.config.implementation;
+
+import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
+import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
+import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.util.StringUtils;
+
+import com.azure.data.appconfiguration.ConfigurationClientBuilder;
+import com.azure.spring.cloud.appconfiguration.config.ConfigurationClientCustomizer;
+import com.azure.spring.cloud.appconfiguration.config.KeyVaultSecretProvider;
+import com.azure.spring.cloud.appconfiguration.config.SecretClientCustomizer;
+import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.appconfiguration.AzureAppConfigurationProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.keyvault.secrets.properties.AzureKeyVaultSecretProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.properties.core.AbstractAzureHttpConfigurationProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.properties.core.authentication.TokenCredentialConfigurationProperties;
+import com.azure.spring.cloud.autoconfigure.implementation.properties.utils.AzureGlobalPropertiesUtils;
+import com.azure.spring.cloud.core.customizer.AzureServiceClientBuilderCustomizer;
+import com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier;
+import com.azure.spring.cloud.service.implementation.appconfiguration.ConfigurationClientBuilderFactory;
+import com.azure.spring.cloud.service.implementation.keyvault.secrets.SecretClientBuilderFactory;
+
+class AzureAppConfigurationBootstrapRegistrar {
+
+ static void register(ConfigDataLocationResolverContext context, Binder binder,
+ AppConfigurationProperties properties, ReplicaLookUp replicaLookup) {
+
+ AzureGlobalProperties globalProperties = binder
+ .bind(AzureGlobalProperties.PREFIX, Bindable.of(AzureGlobalProperties.class))
+ .orElseGet(AzureGlobalProperties::new);
+ AzureAppConfigurationProperties appConfigurationProperties = binder
+ .bind(AzureAppConfigurationProperties.PREFIX, Bindable.of(AzureAppConfigurationProperties.class))
+ .orElseGet(AzureAppConfigurationProperties::new);
+ // the properties are used to custom the ConfigurationClientBuilder
+ AzureAppConfigurationProperties loadedProperties = AzureGlobalPropertiesUtils.loadProperties(globalProperties,
+ appConfigurationProperties);
+
+ boolean isCredentialConfigured = isCredentialConfigured(loadedProperties);
+
+ AppConfigurationKeyVaultClientFactory keyVaultClientFactory = appConfigurationKeyVaultClientFactory(context,
+ binder, isCredentialConfigured);
+ AppConfigurationReplicaClientsBuilder replicaClientsBuilder = replicaClientBuilder(context,
+ keyVaultClientFactory, loadedProperties, isCredentialConfigured);
+
+ context.getBootstrapContext().registerIfAbsent(AppConfigurationKeyVaultClientFactory.class,
+ InstanceSupplier.from(() -> keyVaultClientFactory));
+ context.getBootstrapContext().registerIfAbsent(AppConfigurationReplicaClientFactory.class,
+ InstanceSupplier.from(() -> buildClientFactory(replicaClientsBuilder, properties, replicaLookup)));
+ }
+
+ private static AppConfigurationKeyVaultClientFactory appConfigurationKeyVaultClientFactory(
+ ConfigDataLocationResolverContext context, Binder binder, boolean isCredentialConfigured)
+ throws IllegalArgumentException {
+
+ SecretClientCustomizer customizer = context.getBootstrapContext().getOrElse(SecretClientCustomizer.class, null);
+ KeyVaultSecretProvider secretProvider = context.getBootstrapContext().getOrElse(KeyVaultSecretProvider.class,
+ null);
+
+ AzureKeyVaultSecretProperties secretClientProperties = binder
+ .bind(AzureKeyVaultSecretProperties.PREFIX, Bindable.of(AzureKeyVaultSecretProperties.class))
+ .orElseGet(AzureKeyVaultSecretProperties::new);
+ SecretClientBuilderFactory secretClientBuilderFactory = new SecretClientBuilderFactory(secretClientProperties);
+
+ context.getBootstrapContext().registerIfAbsent(SecretClientBuilderFactory.class,
+ InstanceSupplier.from(() -> secretClientBuilderFactory));
+
+ return new AppConfigurationKeyVaultClientFactory(customizer, secretProvider, secretClientBuilderFactory,
+ isCredentialConfigured);
+ }
+
+ private static AppConfigurationReplicaClientFactory buildClientFactory(
+ AppConfigurationReplicaClientsBuilder clientBuilder, AppConfigurationProperties properties,
+ ReplicaLookUp replicaLookup) {
+ return new AppConfigurationReplicaClientFactory(clientBuilder, properties.getStores(), replicaLookup);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static AppConfigurationReplicaClientsBuilder replicaClientBuilder(ConfigDataLocationResolverContext context,
+ AppConfigurationKeyVaultClientFactory keyVaultClientFactory, AzureAppConfigurationProperties properties,
+ boolean isCredentialConfigured) {
+
+ InstanceSupplier> customizer = context
+ .getBootstrapContext()
+ .getRegisteredInstanceSupplier(
+ (Class>) (Class>) AzureServiceClientBuilderCustomizer.class);
+ ConfigurationClientBuilderFactory clientFactory = context.getBootstrapContext()
+ .getOrElseSupply(ConfigurationClientBuilderFactory.class, () -> {
+ ConfigurationClientBuilderFactory factory = new ConfigurationClientBuilderFactory(properties);
+ factory.setSpringIdentifier(AzureSpringIdentifier.AZURE_SPRING_APP_CONFIG);
+ if (customizer != null) {
+ factory.addBuilderCustomizer(customizer.get(context.getBootstrapContext()));
+ }
+ return factory;
+ });
+ if (customizer != null) {
+ clientFactory.addBuilderCustomizer(customizer.get(context.getBootstrapContext()));
+ }
+
+ InstanceSupplier configurationClientCustomizer = context
+ .getBootstrapContext()
+ .getRegisteredInstanceSupplier(
+ (Class) (Class>) ConfigurationClientCustomizer.class);
+
+ ConfigurationClientCustomizer clientCustomizer = null;
+ if (configurationClientCustomizer != null) {
+ clientCustomizer = configurationClientCustomizer.get(context.getBootstrapContext());
+ }
+
+ return new AppConfigurationReplicaClientsBuilder(clientFactory, clientCustomizer, isCredentialConfigured,
+ keyVaultClientFactory.isConfigured());
+ }
+
+ private static boolean isCredentialConfigured(AbstractAzureHttpConfigurationProperties properties) {
+ if (properties.getCredential() != null) {
+ TokenCredentialConfigurationProperties tokenProps = properties.getCredential();
+ if (StringUtils.hasText(tokenProps.getClientCertificatePassword())) {
+ return true;
+ } else if (StringUtils.hasText(tokenProps.getClientCertificatePath())) {
+ return true;
+ } else if (StringUtils.hasText(tokenProps.getClientId())) {
+ return true;
+ } else if (StringUtils.hasText(tokenProps.getClientSecret())) {
+ return true;
+ } else if (StringUtils.hasText(tokenProps.getUsername())) {
+ return true;
+ } else if (StringUtils.hasText(tokenProps.getPassword())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java
new file mode 100644
index 000000000000..616056a0a6b1
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLoader.java
@@ -0,0 +1,306 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.azure.spring.cloud.appconfiguration.config.implementation;
+
+import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.PUSH_REFRESH;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
+import org.springframework.boot.context.config.ConfigData;
+import org.springframework.boot.context.config.ConfigDataLoader;
+import org.springframework.boot.context.config.ConfigDataLoaderContext;
+import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
+import org.springframework.boot.logging.DeferredLog;
+import org.springframework.boot.logging.DeferredLogFactory;
+import org.springframework.core.env.EnumerablePropertySource;
+import org.springframework.util.StringUtils;
+
+import com.azure.core.util.Context;
+import com.azure.data.appconfiguration.models.ConfigurationSetting;
+import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring.PushNotification;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector;
+
+/**
+ * Azure App Configuration data loader implementation for Spring Boot's ConfigDataLoader.
+ *
+ * @since 6.0.0
+ */
+
+public class AzureAppConfigDataLoader implements ConfigDataLoader {
+
+ /**
+ * Logger instance for this class.
+ */
+ private static Log logger = new DeferredLog();
+
+ /**
+ * The Azure App Configuration data resource being processed.
+ */
+ private AzureAppConfigDataResource resource;
+
+ /**
+ * Factory for creating replica clients to connect to Azure App Configuration.
+ */
+ private AppConfigurationReplicaClientFactory replicaClientFactory;
+
+ /**
+ * Factory for creating Key Vault clients for secret resolution.
+ */
+ private AppConfigurationKeyVaultClientFactory keyVaultClientFactory;
+
+ /**
+ * State holder for managing configuration and feature flag states.
+ */
+ private StateHolder storeState = new StateHolder();
+
+ /**
+ * Client for handling feature flag operations.
+ */
+ private FeatureFlagClient featureFlagClient;
+
+ /**
+ * Request context for tracking operations and telemetry.
+ */
+ private Context requestContext;
+
+ /**
+ * Application start time for calculating delays.
+ */
+ private static final Instant START_DATE = Instant.now();
+
+ /**
+ * Pre-kill time in seconds for delaying exceptions during startup.
+ */
+ private static final Integer PREKILL_TIME = 5;
+
+ /**
+ * Constructs a new AzureAppConfigDataLoader with the specified logger factory.
+ *
+ * @param logFactory the deferred log factory for creating loggers
+ */
+ public AzureAppConfigDataLoader(DeferredLogFactory logFactory) {
+ logger = logFactory.getLog(getClass());
+ }
+
+ /**
+ * Loads configuration data from Azure App Configuration service.
+ *
+ * @param context the config data loader context
+ * @param resource the Azure App Configuration data resource
+ * @return ConfigData containing loaded property sources
+ * @throws IOException if an I/O error occurs during loading
+ * @throws ConfigDataResourceNotFoundException if the configuration resource is not found
+ */
+ @Override
+ public ConfigData load(ConfigDataLoaderContext context, AzureAppConfigDataResource resource)
+ throws IOException, ConfigDataResourceNotFoundException {
+ this.resource = resource;
+ storeState.setNextForcedRefresh(resource.getRefreshInterval());
+ if (context.getBootstrapContext().isRegistered(FeatureFlagClient.class)) {
+ featureFlagClient = context.getBootstrapContext().get(FeatureFlagClient.class);
+ } else {
+ featureFlagClient = new FeatureFlagClient();
+ context.getBootstrapContext().registerIfAbsent(FeatureFlagClient.class,
+ InstanceSupplier.from(() -> featureFlagClient));
+ }
+ // Reset telemetry usage for refresh
+ featureFlagClient.resetTelemetry();
+ List> sourceList = new ArrayList<>();
+ if (resource.isConfigStoreEnabled()) {
+ replicaClientFactory = context.getBootstrapContext()
+ .get(AppConfigurationReplicaClientFactory.class);
+ keyVaultClientFactory = context.getBootstrapContext()
+ .get(AppConfigurationKeyVaultClientFactory.class);
+
+ List clients = replicaClientFactory
+ .getAvailableClients(resource.getEndpoint(), true);
+
+ boolean reloadFailed = false;
+ boolean pushRefresh = false;
+ Exception lastException = null;
+ PushNotification notification = resource.getMonitoring().getPushNotification();
+ if ((notification.getPrimaryToken() != null
+ && StringUtils.hasText(notification.getPrimaryToken().getName()))
+ || (notification.getSecondaryToken() != null
+ && StringUtils.hasText(notification.getPrimaryToken().getName()))) {
+ pushRefresh = true;
+ }
+ // Feature Management needs to be set in the last config store.
+ requestContext = new Context("refresh", resource.isRefresh()).addData(PUSH_REFRESH, pushRefresh);
+
+ Iterator clientIterator = clients.iterator();
+
+ while (clientIterator.hasNext()) {
+ AppConfigurationReplicaClient client = clientIterator.next();
+
+ if (reloadFailed
+ && !AppConfigurationRefreshUtil.refreshStoreCheck(client,
+ replicaClientFactory.findOriginForEndpoint(client.getEndpoint()), requestContext)) {
+ // This store doesn't have any changes where to refresh store did. Skipping Checking next.
+ continue;
+ }
+
+ // Reverse in order to add Profile specific properties earlier, and last profile comes first
+ try {
+ sourceList.addAll(createSettings(client));
+ List featureFlags = createFeatureFlags(client);
+
+ logger.debug("PropertySource context.");
+ AppConfigurationStoreMonitoring monitoring = resource.getMonitoring();
+
+ storeState.setStateFeatureFlag(resource.getEndpoint(), featureFlags,
+ monitoring.getFeatureFlagRefreshInterval());
+
+ if (monitoring.isEnabled()) {
+ // Setting new ETag values for Watch
+ List watchKeysSettings = monitoring.getTriggers().stream()
+ .map(trigger -> client.getWatchKey(trigger.getKey(), trigger.getLabel(),
+ requestContext))
+ .toList();
+
+ storeState.setState(resource.getEndpoint(), watchKeysSettings, monitoring.getRefreshInterval());
+ }
+ storeState.setLoadState(resource.getEndpoint(), true); // Success - configuration loaded, exit loop
+ lastException = null;
+ // Break out of the loop since we have successfully loaded configuration
+ break;
+ } catch (AppConfigurationStatusException e) {
+ reloadFailed = true;
+ replicaClientFactory.backoffClient(resource.getEndpoint(), client.getEndpoint());
+ lastException = e;
+ // Log the specific replica failure with context
+ logReplicaFailure(client, "status exception", clientIterator.hasNext(), e);
+ } catch (Exception e) {
+ // Store the exception to potentially use if all replicas fail
+ lastException = e; // Log the specific replica failure with context
+ logReplicaFailure(client, "exception", clientIterator.hasNext(), e);
+ }
+ } // Check if all replicas failed
+ if (lastException != null && !resource.isRefresh()) {
+ // During startup, if all replicas failed, fail the application
+ logger.error("Azure App Configuration failed to load configuration during startup for store: "
+ + resource.getEndpoint() + ". Application cannot start without required configuration.");
+ failedToGeneratePropertySource(lastException);
+ } else if (lastException != null && resource.isRefresh()) {
+ // During refresh, log warning but don't fail the application
+ logger.warn("Azure App Configuration failed during refresh for store: "
+ + resource.getEndpoint() + ". Continuing with existing configuration.");
+ }
+ }
+
+ StateHolder.updateState(storeState);
+ if (featureFlagClient.getFeatureFlags().size() > 0) {
+ // Don't add feature flags if there are none, otherwise the local file can't load them.
+ sourceList.add(new AppConfigurationFeatureManagementPropertySource(featureFlagClient));
+ }
+ return new ConfigData(sourceList);
+ }
+
+ /**
+ * Handles failed property source generation when all replicas fail during application startup.
+ *
+ * @param e the exception that caused the failure
+ * @throws RuntimeException always thrown to indicate the startup failure
+ */
+ private void failedToGeneratePropertySource(Exception e) {
+ logger.error("Configuration loading failed during application startup from Azure App Configuration store "
+ + resource.getEndpoint() + ". Application cannot start without required configuration.");
+ delayException();
+ throw new RuntimeException("Failed to generate property sources for " + resource.getEndpoint(), e);
+ }
+
+ /**
+ * Creates a new set of AppConfigurationPropertySources, 1 per Label.
+ *
+ * @param client client for connecting to App Configuration
+ * @return a list of AppConfigurationPropertySources
+ * @throws Exception creating a property source failed
+ */
+ private List createSettings(AppConfigurationReplicaClient client)
+ throws Exception {
+ List sourceList = new ArrayList<>();
+ List selects = resource.getSelects();
+ List profiles = resource.getProfiles().getActive();
+
+ for (AppConfigurationKeyValueSelector selectedKeys : selects) {
+ AppConfigurationPropertySource propertySource = null;
+
+ if (StringUtils.hasText(selectedKeys.getSnapshotName())) {
+ propertySource = new AppConfigurationSnapshotPropertySource(
+ selectedKeys.getSnapshotName() + "/" + resource.getEndpoint(), client, keyVaultClientFactory,
+ selectedKeys.getSnapshotName(), featureFlagClient);
+ } else {
+ propertySource = new AppConfigurationApplicationSettingPropertySource(
+ selectedKeys.getKeyFilter() + resource.getEndpoint() + "/", client, keyVaultClientFactory,
+ selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles));
+ }
+ propertySource.initProperties(resource.getTrimKeyPrefix(), requestContext);
+ sourceList.add(propertySource);
+ }
+ return sourceList;
+ }
+
+ /**
+ * Creates a list of feature flags from Azure App Configuration.
+ *
+ * @param client client for connecting to App Configuration
+ * @return a list of FeatureFlags
+ * @throws Exception creating feature flags failed
+ */
+ private List createFeatureFlags(AppConfigurationReplicaClient client)
+ throws Exception {
+ List featureFlagWatchKeys = new ArrayList<>();
+ List profiles = resource.getProfiles().getActive();
+
+ for (FeatureFlagKeyValueSelector selectedKeys : resource.getFeatureFlagSelects()) {
+ List storesFeatureFlags = featureFlagClient.loadFeatureFlags(client,
+ selectedKeys.getKeyFilter(), selectedKeys.getLabelFilter(profiles), requestContext);
+ featureFlagWatchKeys.addAll(storesFeatureFlags);
+ }
+
+ return featureFlagWatchKeys;
+ }
+
+ /**
+ * Logs a replica failure with contextual information about the failure scenario and available replicas.
+ *
+ * @param client the replica client that failed
+ * @param exceptionType a brief description of the exception type (e.g., "status exception", "exception")
+ * @param hasMoreReplicas whether there are additional replicas available to try
+ * @param exception the exception that caused the failure
+ */
+ private void logReplicaFailure(AppConfigurationReplicaClient client, String exceptionType,
+ boolean hasMoreReplicas, Exception exception) {
+ String scenario = resource.isRefresh() ? "refresh" : "startup";
+ String nextAction = hasMoreReplicas ? "Trying next replica." : "No more replicas available.";
+
+ logger.warn("Azure App Configuration replica " + client.getEndpoint()
+ + " failed during " + scenario + " with " + exceptionType + ". "
+ + nextAction + " Store: " + resource.getEndpoint(), exception);
+ }
+
+ /**
+ * Introduces a delay before throwing exceptions during startup to prevent fast crash loops.
+ */
+ private void delayException() {
+ Instant currentDate = Instant.now();
+ Instant preKillTime = START_DATE.plusSeconds(PREKILL_TIME);
+ if (currentDate.isBefore(preKillTime)) {
+ long diffInMillies = Math.abs(preKillTime.toEpochMilli() - currentDate.toEpochMilli());
+ try {
+ Thread.sleep(diffInMillies);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // Restore interrupted status
+ }
+ }
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLocationResolver.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLocationResolver.java
new file mode 100644
index 000000000000..41acde2f8765
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataLocationResolver.java
@@ -0,0 +1,159 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.azure.spring.cloud.appconfiguration.config.implementation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.naming.NamingException;
+
+import org.apache.commons.logging.Log;
+import org.springframework.boot.BootstrapRegistry.InstanceSupplier;
+import org.springframework.boot.context.config.ConfigDataLocation;
+import org.springframework.boot.context.config.ConfigDataLocationNotFoundException;
+import org.springframework.boot.context.config.ConfigDataLocationResolver;
+import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
+import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
+import org.springframework.boot.context.config.Profiles;
+import org.springframework.boot.context.properties.bind.BindHandler;
+import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.boot.logging.DeferredLog;
+import org.springframework.util.StringUtils;
+
+import com.azure.spring.cloud.appconfiguration.config.implementation.autofailover.ReplicaLookUp;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationProperties;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore;
+
+/**
+ * Resolves Azure App Configuration data locations for Spring Boot's ConfigData API.
+ *
+ * @since 6.0.0
+ */
+
+public class AzureAppConfigDataLocationResolver
+ implements ConfigDataLocationResolver {
+
+ private static final Log LOGGER = new DeferredLog();
+
+ /** Prefix used to identify Azure App Configuration locations */
+ public static final String PREFIX = "azureAppConfiguration";
+
+ /** Flag to track startup phase for proper resource initialization */
+ private static final AtomicBoolean START_UP = new AtomicBoolean(true);
+
+ /**
+ * Determines if the given location can be resolved by this resolver.
+ *
+ * @param context the resolver context containing binder and bootstrap information
+ * @param location the configuration data location to check
+ * @return true if this resolver can handle the location, false otherwise
+ */
+ @Override
+ public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
+ if (!location.hasPrefix(PREFIX)) {
+ return false;
+ }
+
+ // Check if the configuration properties for Azure App Configuration are present
+ return hasValidStoreConfiguration(context.getBinder());
+ }
+
+ /**
+ * Checks if the required configuration properties for Azure App Configuration are present.
+ *
+ * @param binder the binder to check for properties
+ * @return true if at least one of the required properties is present, false otherwise
+ */
+ private boolean hasValidStoreConfiguration(Binder binder) {
+ // Check if any of the required properties for Azure App Configuration stores are present
+ String configPrefix = AppConfigurationProperties.CONFIG_PREFIX + ".stores[0].";
+
+ return hasNonEmptyProperty(binder, configPrefix + "endpoint")
+ || hasNonEmptyProperty(binder, configPrefix + "connection-string")
+ || hasNonEmptyProperty(binder, configPrefix + "endpoints")
+ || hasNonEmptyProperty(binder, configPrefix + "connection-strings");
+ }
+
+ private boolean hasNonEmptyProperty(Binder binder, String propertyPath) {
+ return StringUtils.hasText(binder.bind(propertyPath, String.class).orElse(""));
+ }
+
+ /**
+ * Resolves configuration data resources for the given location.
+ *
+ * @param context the resolver context
+ * @param location the configuration data location
+ * @return empty list of resources
+ */
+ @Override
+ public List resolve(ConfigDataLocationResolverContext context,
+ ConfigDataLocation location)
+ throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Resolves profile-specific configuration data resources.
+ *
+ * @param resolverContext the resolver context
+ * @param location the configuration data location
+ * @param profiles the active Spring profiles
+ * @return list of Azure App Configuration data resources
+ * @throws ConfigDataLocationNotFoundException if location cannot be found
+ */
+ @Override
+ public List resolveProfileSpecific(
+ ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles)
+ throws ConfigDataLocationNotFoundException {
+
+ AppConfigurationProperties properties = loadProperties(resolverContext);
+ List locations = new ArrayList<>();
+
+ if (properties.getStores() == null || properties.getStores().isEmpty()) {
+ throw new ConfigDataLocationNotFoundException(location,
+ "No Azure App Configuration stores are configured. Please check your application properties.",
+ new IllegalStateException("No stores configured"));
+ }
+
+ for (ConfigStore store : properties.getStores()) {
+ locations.add(
+ new AzureAppConfigDataResource(store, profiles, START_UP.get(), properties.getRefreshInterval()));
+ }
+ START_UP.set(false);
+ return locations;
+ }
+
+ /**
+ * Loads and validates Azure App Configuration properties from the configuration context.
+ *
+ * @param context the configuration data location resolver context
+ * @return validated Azure App Configuration properties
+ */
+ protected AppConfigurationProperties loadProperties(ConfigDataLocationResolverContext context) {
+ Binder binder = context.getBinder();
+ BindHandler bindHandler = getBindHandler(context);
+ AppConfigurationProperties properties = binder.bind(AppConfigurationProperties.CONFIG_PREFIX,
+ Bindable.of(AppConfigurationProperties.class), bindHandler).get();
+
+ properties.validateAndInit();
+ ReplicaLookUp replicaLookup = null;
+ try {
+ replicaLookup = new ReplicaLookUp(properties);
+ context.getBootstrapContext().registerIfAbsent(ReplicaLookUp.class, InstanceSupplier.of(replicaLookup));
+ } catch (NamingException e) {
+ LOGGER.info("Failed to find DNS Entry for config store while looking for replicas.");
+ }
+
+ AzureAppConfigurationBootstrapRegistrar.register(context, binder, properties, replicaLookup);
+
+ return properties;
+ }
+
+ private BindHandler getBindHandler(ConfigDataLocationResolverContext context) {
+ return context.getBootstrapContext().getOrElse(BindHandler.class, null);
+ }
+
+}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataResource.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataResource.java
new file mode 100644
index 000000000000..0d828552c0e3
--- /dev/null
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/AzureAppConfigDataResource.java
@@ -0,0 +1,151 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package com.azure.spring.cloud.appconfiguration.config.implementation;
+
+import java.time.Duration;
+import java.util.List;
+
+import org.springframework.boot.context.config.ConfigDataResource;
+import org.springframework.boot.context.config.Profiles;
+
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationKeyValueSelector;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.AppConfigurationStoreMonitoring;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.ConfigStore;
+import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagKeyValueSelector;
+
+/**
+ * Represents an Azure App Configuration data resource that extends Spring Boot's ConfigDataResource.
+ *
+ * @since 6.0.0
+ */
+public class AzureAppConfigDataResource extends ConfigDataResource {
+
+ /** Indicates whether the configuration store is enabled for loading configuration data. */
+ private final boolean configStoreEnabled;
+
+ /** The endpoint URL of the Azure App Configuration store. */
+ private final String endpoint;
+
+ /** List of key prefixes to trim from configuration keys when loading. */
+ private final List trimKeyPrefix;
+
+ /** Spring Boot profiles configuration for conditional property loading. */
+ private final Profiles profiles;
+
+ /** List of selectors for filtering key-value pairs from the configuration store. */
+ private final List selects;
+
+ /** List of selectors for filtering feature flag key-value pairs from the configuration store. */
+ private final List featureFlagSelects;
+
+ /** Monitoring configuration for the configuration store including refresh triggers. */
+ private final AppConfigurationStoreMonitoring monitoring;
+
+ /** Indicates whether this resource supports configuration refresh at runtime. */
+ private final boolean isRefresh;
+
+ /** The interval at which configuration should be refreshed from the store. */
+ private final Duration refreshInterval;
+
+ /**
+ * Constructs a new AzureAppConfigDataResource with the specified configuration store settings.
+ *
+ * @param configStore the configuration store settings containing endpoint, selectors, and other options
+ * @param profiles the Spring Boot profiles for conditional configuration loading
+ * @param isRefresh whether this resource supports runtime configuration refresh
+ * @param refreshInterval the interval at which configuration should be refreshed
+ */
+ AzureAppConfigDataResource(ConfigStore configStore, Profiles profiles, boolean isRefresh,
+ Duration refreshInterval) {
+ this.configStoreEnabled = configStore.isEnabled();
+ this.endpoint = configStore.getEndpoint();
+ this.selects = configStore.getSelects();
+ this.featureFlagSelects = configStore.getFeatureFlags().getSelects();
+ this.trimKeyPrefix = configStore.getTrimKeyPrefix();
+ this.monitoring = configStore.getMonitoring();
+ this.profiles = profiles;
+ this.isRefresh = isRefresh;
+ this.refreshInterval = refreshInterval;
+ }
+
+ /**
+ * Gets the list of key-value selectors used to filter configuration data from the store.
+ *
+ * @return the list of configuration key-value selectors, may be null or empty
+ */
+ public List getSelects() {
+ return selects;
+ }
+
+ /**
+ * Gets the list of feature flag selectors used to filter feature flag data from the store.
+ *
+ * @return the list of feature flag selectors, may be null or empty
+ */
+ public List getFeatureFlagSelects() {
+ return featureFlagSelects;
+ }
+
+ /**
+ * Checks whether the configuration store is enabled for loading configuration data.
+ *
+ * @return true if the configuration store is enabled, false otherwise
+ */
+ public boolean isConfigStoreEnabled() {
+ return configStoreEnabled;
+ }
+
+ /**
+ * Gets the endpoint URL of the Azure App Configuration store.
+ *
+ * @return the endpoint URL as a string, may be null if not configured
+ */
+ public String getEndpoint() {
+ return endpoint;
+ }
+
+ /**
+ * Gets the monitoring configuration for this configuration store.
+ *
+ * @return the monitoring configuration, may be null if not configured
+ */
+ public AppConfigurationStoreMonitoring getMonitoring() {
+ return monitoring;
+ }
+
+ /**
+ * Gets the list of key prefixes to trim from configuration keys when loading.
+ *
+ * @return the list of key prefixes to trim, may be null or empty if no trimming is configured
+ */
+ public List getTrimKeyPrefix() {
+ return trimKeyPrefix;
+ }
+
+ /**
+ * Gets the Spring Boot profiles configuration for conditional property loading.
+ *
+ * @return the profiles configuration, never null
+ */
+ public Profiles getProfiles() {
+ return profiles;
+ }
+
+ /**
+ * Returns if true if this resource is being refreshed. False if the resource is being loaded at startup.
+ *
+ * @return true if this is a refresh operation, false if it is a startup load
+ */
+ public boolean isRefresh() {
+ return isRefresh;
+ }
+
+ /**
+ * Gets the interval at which configuration should be refreshed from the store.
+ *
+ * @return the refresh interval, may be null if not configured
+ */
+ public Duration getRefreshInterval() {
+ return refreshInterval;
+ }
+}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java
index afc647f1b31e..39f378fe2eff 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/BackoffTimeCalculator.java
@@ -5,19 +5,39 @@
import java.util.Random;
/**
- * Calculates the amount of time to the next refresh, if a refresh fails.
+ * Utility class for calculating exponential backoff times for Azure App Configuration retry operations.
*/
final class BackoffTimeCalculator {
- private static final Long MAX_ATTEMPTS = (long) 63;
+ /**
+ * Maximum number of attempts to consider for exponential backoff calculation. This prevents integer overflow when
+ * calculating 2^attempts. Value of 63 ensures that 2^63 is the largest power of 2 that fits in a long.
+ */
+ private static final long MAX_ATTEMPTS = 63;
- private static final Long SECONDS_TO_NANO_SECONDS = (long) 1000000000;
+ /**
+ * Conversion factor from seconds to nanoseconds. Used to convert backoff times from seconds to nanoseconds for
+ * precise timing.
+ */
+ private static final long SECONDS_TO_NANOSECONDS = 1_000_000_000L;
+ /**
+ * Generator for introducing jitter in backoff calculations. Jitter helps prevent multiple clients from retrying
+ * simultaneously (thundering herd).
+ */
private static final Random RANDOM = new Random();
- private static Long maxBackoff = (long) 600;
+ /**
+ * Maximum backoff time in seconds. Default: 600 seconds (10 minutes) - reasonable maximum to prevent excessively
+ * long waits.
+ */
+ private static long maxBackoffSeconds = 600;
- private static Long minBackoff = (long) 30;
+ /**
+ * Minimum backoff time in seconds. Default: 30 seconds - ensures proper rate limiting and prevents rapid retry
+ * loops.
+ */
+ private static long minBackoffSeconds = 30;
/**
*
@@ -25,23 +45,23 @@ final class BackoffTimeCalculator {
* @param minBackoff minimum amount of time between requests
*/
static void setDefaults(Long maxBackoff, Long minBackoff) {
- BackoffTimeCalculator.maxBackoff = maxBackoff;
- BackoffTimeCalculator.minBackoff = minBackoff;
+ BackoffTimeCalculator.maxBackoffSeconds = maxBackoff != null ? maxBackoff : 600L;
+ BackoffTimeCalculator.minBackoffSeconds = minBackoff != null ? minBackoff : 30L;
}
/**
- * Calculates the new Backoff time for requests.
- * @param attempts Number of attempts so far
- * @return Nano Seconds to the next request
- * @throws IllegalArgumentException when back off time or attempt number is invalid
+ * Calculates the exponential backoff time with jitter for retry operations.
+ *
+ * @param attempts the number of retry attempts made so far; must be non-negative
+ * @return the calculated backoff time in nanoseconds; never negative
*/
- static Long calculateBackoff(Integer attempts) {
+ static long calculateBackoff(Integer attempts) {
- if (minBackoff < 0) {
+ if (minBackoffSeconds < 0) {
throw new IllegalArgumentException("Minimum Backoff time needs to be greater than or equal to 0.");
}
- if (maxBackoff < 0) {
+ if (maxBackoffSeconds < 0) {
throw new IllegalArgumentException("Maximum Backoff time needs to be greater than or equal to 0.");
}
@@ -49,10 +69,11 @@ static Long calculateBackoff(Integer attempts) {
throw new IllegalArgumentException("Number of previous attempts needs to be a positive number.");
}
- long minBackoffNano = minBackoff * SECONDS_TO_NANO_SECONDS;
- long maxBackoffNano = maxBackoff * SECONDS_TO_NANO_SECONDS;
+ final long minBackoffNano = minBackoffSeconds * SECONDS_TO_NANOSECONDS;
+ final long maxBackoffNano = maxBackoffSeconds * SECONDS_TO_NANOSECONDS;
- if (attempts <= 1 || maxBackoff <= minBackoff) {
+ // For first attempts or when min equals max, return minimum backoff
+ if (attempts <= 1 || maxBackoffNano <= minBackoffNano) {
return minBackoffNano;
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java
index 16c6d2d05fdc..f160649abe06 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/ConnectionManager.java
@@ -7,7 +7,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,33 +18,43 @@
import com.azure.spring.cloud.appconfiguration.config.implementation.properties.FeatureFlagStore;
/**
- * Holds a set of connections to an app configuration store with zero to many geo-replications.
+ * Manages connection pools and client lifecycle for Azure App Configuration stores with support for geo-replication,
+ * auto-failover, and intelligent client routing.
*/
-public class ConnectionManager {
+class ConnectionManager {
private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionManager.class);
+ /** The primary endpoint URL for the App Configuration store. */
private final String originEndpoint;
- // Used if multiple connection method is given.
+ /** List of configured replica clients for the primary App Configuration store. */
private List clients;
- private Map autoFailoverClients;
+ /** Map of auto-discovered failover clients, keyed by endpoint URL. */
+ private final Map autoFailoverClients;
+ /** Currently active replica endpoint being used for requests. */
private String currentReplica;
+ /** Current health status of the App Configuration store connection. */
private AppConfigurationStoreHealth health;
+ /** Builder for creating App Configuration replica clients. */
private final AppConfigurationReplicaClientsBuilder clientBuilder;
+ /** Configuration store settings and connection parameters. */
private final ConfigStore configStore;
+ /** Service for discovering auto-failover replica endpoints. */
private final ReplicaLookUp replicaLookUp;
/**
- * Creates a set of connections to an app configuration store.
- * @param clientBuilder Builder for App Configuration Clients
- * @param configStore Connection info for the store
+ * Creates a connection manager for the specified App Configuration store.
+ *
+ * @param clientBuilder the builder for creating App Configuration replica clients; must not be null
+ * @param configStore the configuration store settings and connection parameters; must not be null
+ * @param replicaLookUp the service for discovering auto-failover endpoints; must not be null
*/
ConnectionManager(AppConfigurationReplicaClientsBuilder clientBuilder, ConfigStore configStore,
ReplicaLookUp replicaLookUp) {
@@ -59,41 +68,53 @@ public class ConnectionManager {
}
/**
- * Gets the current health information on the Connection to the Config Store
- * @return AppConfigurationConfigStoreHealth
+ * Retrieves the current health status of the App Configuration store connection.
+ *
+ * @return the current health status; never null
*/
AppConfigurationStoreHealth getHealth() {
return this.health;
}
+ /**
+ * Sets the current active replica endpoint for client routing.
+ *
+ * @param replicaEndpoint the endpoint URL to set as current; may be null to reset to primary endpoint
+ */
void setCurrentClient(String replicaEndpoint) {
this.currentReplica = replicaEndpoint;
}
/**
- * @return the originEndpoint
+ * Retrieves the primary (origin) endpoint URL for the App Configuration store.
+ *
+ * @return the primary endpoint URL; never null
*/
String getMainEndpoint() {
return originEndpoint;
}
/**
- * Returns a client.
- * @return ConfigurationClient
+ * Retrieves all available App Configuration clients that are ready for use.
+ *
+ * @return a list of available clients; may be empty if all clients are currently unavailable
*/
List getAvailableClients() {
return getAvailableClients(false);
}
/**
- * Returns a client.
- * @return ConfigurationClient
+ * Retrieves available App Configuration clients with optional current replica preference.
+ *
+ * @param useCurrent if true, prioritizes returning clients starting from the current replica; if false, returns all
+ * available clients
+ * @return a list of available clients ordered by preference; may be empty if all clients are currently unavailable
*/
List getAvailableClients(Boolean useCurrent) {
if (clients == null) {
clients = clientBuilder.buildClients(configStore);
- if (clients.size() == 0) {
+ if (clients.isEmpty()) {
this.health = AppConfigurationStoreHealth.NOT_LOADED;
}
}
@@ -143,16 +164,10 @@ List getAvailableClients(Boolean useCurrent) {
return availableClients;
}
- List getAllEndpoints() {
- List endpoints = clients.stream().map(AppConfigurationReplicaClient::getEndpoint)
- .collect(Collectors.toList());
- endpoints.addAll(replicaLookUp.getAutoFailoverEndpoints(configStore.getEndpoint()));
- return endpoints;
- }
-
/**
- * Call when the current client failed
- * @param endpoint replica endpoint
+ * Applies exponential backoff to a failed client endpoint.
+ *
+ * @param endpoint the endpoint URL of the failed client; must not be null or empty
*/
void backoffClient(String endpoint) {
for (AppConfigurationReplicaClient client : clients) {
@@ -170,9 +185,10 @@ void backoffClient(String endpoint) {
}
/**
- * Updates the sync token of the client. Only works if no replicas are being used.
- *
- * @param syncToken App Configuration sync token
+ * Updates the synchronization token for the specified client endpoint.
+ *
+ * @param endpoint the endpoint URL of the client to update; may be null (method will have no effect if null)
+ * @param syncToken the new synchronization token; may be null to clear the existing token
*/
void updateSyncToken(String endpoint, String syncToken) {
clients.stream().filter(client -> client.getEndpoint().equals(endpoint)).findFirst()
@@ -181,10 +197,20 @@ void updateSyncToken(String endpoint, String syncToken) {
.ifPresent(client -> client.updateSyncToken(syncToken));
}
+ /**
+ * Retrieves the monitoring configuration for the App Configuration store.
+ *
+ * @return the monitoring configuration; may be null if not configured
+ */
AppConfigurationStoreMonitoring getMonitoring() {
return configStore.getMonitoring();
}
+ /**
+ * Retrieves the feature flag store configuration.
+ *
+ * @return the feature flag store configuration; may be null if not configured
+ */
FeatureFlagStore getFeatureFlagStore() {
return configStore.getFeatureFlags();
}
diff --git a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java
index 956454d616ca..9c4f5433e02c 100644
--- a/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java
+++ b/sdk/spring/spring-cloud-azure-appconfiguration-config/src/main/java/com/azure/spring/cloud/appconfiguration/config/implementation/FeatureFlagClient.java
@@ -6,7 +6,6 @@
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.DEFAULT_REQUIREMENT_TYPE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.E_TAG;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_CONTENT_TYPE;
-import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_ID;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_PREFIX;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.FEATURE_FLAG_REFERENCE;
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.REQUIREMENT_TYPE_SERVICE;
@@ -14,41 +13,56 @@
import static com.azure.spring.cloud.appconfiguration.config.implementation.AppConfigurationConstants.TELEMETRY;
import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Base64;
import java.util.Collections;
+import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
-import org.bouncycastle.jcajce.provider.digest.SHA256;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
+import com.azure.core.util.Context;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
+import com.azure.data.appconfiguration.models.FeatureFlagFilter;
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.FeatureFlags;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.Feature;
import com.azure.spring.cloud.appconfiguration.config.implementation.feature.entity.FeatureTelemetry;
+import com.azure.spring.cloud.appconfiguration.config.implementation.http.policy.FeatureFlagTracing;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
-import com.nimbusds.jose.util.Base64URL;
/**
* Loads sets of feature flags, and de-duplicates the results with previously loaded feature flags. Newer Feature Flags
* take priority.
*/
@Component
-public class FeatureFlagClient {
+class FeatureFlagClient {
- protected final Map properties = new LinkedHashMap<>();
+ private static final Logger LOGGER = LoggerFactory
+ .getLogger(FeatureFlagClient.class);
+
+ private final Map properties = new LinkedHashMap<>();
private static final ObjectMapper CASE_INSENSITIVE_MAPPER = JsonMapper.builder()
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();
+
+ private FeatureFlagTracing tracing = new FeatureFlagTracing();
/**
*
@@ -62,8 +76,8 @@ public class FeatureFlagClient {
*
*
*/
- public List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, String customKeyFilter,
- String[] labelFilter, boolean isRefresh) {
+ List loadFeatureFlags(AppConfigurationReplicaClient replicaClient, String customKeyFilter,
+ String[] labelFilter, Context context) {
List loadedFeatureFlags = new ArrayList<>();
String keyFilter = SELECT_ALL_FEATURE_FLAGS;
@@ -77,14 +91,15 @@ public List loadFeatureFlags(AppConfigurationReplicaClient replica
for (String label : labels) {
SettingSelector settingSelector = new SettingSelector().setKeyFilter(keyFilter).setLabelFilter(label);
+ context.addData("FeatureFlagTracing", tracing);
- FeatureFlags features = replicaClient.listFeatureFlags(settingSelector, isRefresh);
- loadedFeatureFlags.addAll(proccessFeatureFlags(features, keyFilter));
+ FeatureFlags features = replicaClient.listFeatureFlags(settingSelector, context);
+ loadedFeatureFlags.addAll(proccessFeatureFlags(features, replicaClient.getOriginClient()));
}
return loadedFeatureFlags;
}
- public List proccessFeatureFlags(FeatureFlags features, String endpoint) {
+ List proccessFeatureFlags(FeatureFlags features, String endpoint) {
List loadedFeatureFlags = new ArrayList<>();
loadedFeatureFlags.add(features);
@@ -93,6 +108,7 @@ public List proccessFeatureFlags(FeatureFlags features, String end
if (setting instanceof FeatureFlagConfigurationSetting
&& FEATURE_FLAG_CONTENT_TYPE.equals(setting.getContentType())) {
FeatureFlagConfigurationSetting featureFlag = (FeatureFlagConfigurationSetting) setting;
+ updateTelemetry(featureFlag);
properties.put(featureFlag.getKey(), createFeature(featureFlag, endpoint));
}
}
@@ -107,7 +123,8 @@ public List proccessFeatureFlags(FeatureFlags features, String end
*/
protected static Feature createFeature(FeatureFlagConfigurationSetting item, String originEndpoint) {
String requirementType = DEFAULT_REQUIREMENT_TYPE;
- FeatureTelemetry featureTelemetry = new FeatureTelemetry();
+ FeatureTelemetry featureTelemetry = null;
+ Feature feature = null;
try {
JsonNode node = CASE_INSENSITIVE_MAPPER.readTree(item.getValue());
JsonNode conditions = node.get(CONDITIONS);
@@ -115,54 +132,154 @@ protected static Feature createFeature(FeatureFlagConfigurationSetting item, Str
requirementType = conditions.get(REQUIREMENT_TYPE_SERVICE).asText();
}
JsonNode telemetryNode = node.get(TELEMETRY);
- if (telemetryNode != null) {
+ if (telemetryNode != null && !telemetryNode.isEmpty()) {
ObjectMapper objectMapper = JsonMapper.builder()
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();
featureTelemetry = objectMapper.convertValue(telemetryNode, FeatureTelemetry.class);
}
- } catch (JsonProcessingException e) {
- }
+ feature = new Feature(item, requirementType, featureTelemetry);
- Feature feature = new Feature(item, requirementType, featureTelemetry);
-
- if (feature.getTelemetry() != null) {
- final FeatureTelemetry telemetry = feature.getTelemetry();
- if (telemetry.isEnabled()) {
- final Map originMetadata = telemetry.getMetadata();
- originMetadata.put(FEATURE_FLAG_ID, calculateFeatureFlagId(item.getKey(), item.getLabel()));
- originMetadata.put(E_TAG, item.getETag());
- if (originEndpoint != null && !originEndpoint.isEmpty()) {
- final String labelPart = item.getLabel().isEmpty() ? ""
- : String.format("?label=%s", item.getLabel());
- originMetadata.put(FEATURE_FLAG_REFERENCE,
- String.format("%s/kv/%s%s", originEndpoint, item.getKey(), labelPart));
+ if (feature.getTelemetry() != null) {
+ final FeatureTelemetry telemetry = feature.getTelemetry();
+ if (telemetry.isEnabled()) {
+ final Map originMetadata = telemetry.getMetadata();
+ originMetadata.put(E_TAG, item.getETag());
+ if (originEndpoint != null && !originEndpoint.isEmpty()) {
+ final String labelPart = item.getLabel().isEmpty() ? ""
+ : String.format("?label=%s", item.getLabel());
+ originMetadata.put(FEATURE_FLAG_REFERENCE,
+ String.format("%s/kv/%s%s", originEndpoint, item.getKey(), labelPart));
+ }
+ originMetadata.put("AllocationId", generateAllocationId(node));
}
}
+ } catch (JsonProcessingException e) {
+ LOGGER.error("Error parsing feature flag value for key: {}", item.getKey(), e);
}
return feature;
}
/**
- * @param key the key of feature flag
- * @param label the label of feature flag. If label is whitespace, treat as null
- * @return base64_url(SHA256(utf8_bytes("${key}\n${label}"))).replace('+', '-').replace('/', '_').trimEnd('=')
- * trimEnd() means trims everything after the first occurrence of the '='
+ * @return the properties
*/
- private static String calculateFeatureFlagId(String key, String label) {
- final String data = String.format("%s\n%s", key, label.isEmpty() ? null : label);
- final SHA256.Digest digest = new SHA256.Digest();
- final String beforeTrim = Base64URL.encode(digest.digest(data.getBytes(StandardCharsets.UTF_8)))
- .toString().replace('+', '-').replace('/', '_');
- final int index = beforeTrim.indexOf('=');
- return beforeTrim.substring(0, index > -1 ? index : beforeTrim.length());
+ public List getFeatureFlags() {
+ return properties.values().stream().toList();
}
-
+
+ public void resetTelemetry() {
+ tracing.resetFeatureFilterTelemetry();
+ }
+
/**
- * @return the properties
+ * Looks at each filter used in a Feature Flag to check what types it is using.
+ *
+ * @param featureFlag FeatureFlagConfigurationSetting
+ * @param tracing The TracingInfo for this store.
*/
- public Map getProperties() {
- return properties;
+ private void updateTelemetry(FeatureFlagConfigurationSetting featureFlag) {
+ for (FeatureFlagFilter filter : featureFlag.getClientFilters()) {
+ tracing.updateFeatureFilterTelemetry(filter.getName());
+ }
}
+ /**
+ * Generates a unique allocation ID for the feature flag based on its configuration.
+ *
+ * @param featureFlagValue The feature flag value as a map.
+ * @return A unique allocation ID or null if the allocation is not valid.
+ */
+ static String generateAllocationId(JsonNode featureFlagValue) {
+ StringBuilder allocationId = new StringBuilder();
+ List allocatedVariants = new ArrayList<>();
+
+ // Retrieve allocation object
+ JsonNode allocation = featureFlagValue.get("allocation");
+ if (allocation == null) {
+ return null;
+ }
+
+ // Seed
+ allocationId.append("seed=").append(allocation.has("seed") ? allocation.get("seed").asText() : "");
+
+ // DefaultWhenEnabled
+ if (allocation.has("default_when_enabled")) {
+ allocatedVariants.add(allocation.get("default_when_enabled").asText());
+ }
+ allocationId.append("\ndefault_when_enabled=").append(allocation.has("default_when_enabled") ? allocation.get("default_when_enabled").asText() : "");
+
+ // Percentile
+ allocationId.append("\npercentiles=");
+ JsonNode percentile = allocation.get("percentile");
+ List percentileAllocations = new ArrayList<>();
+ if (percentile != null && percentile.isArray()) {
+ percentile.forEach(p -> {
+ if (!Objects.equals(p.get("from").asText(), p.get("to").asText())) {
+ percentileAllocations.add(p);
+ }
+ });
+ percentileAllocations.sort(Comparator.comparing(p -> p.get("from").asInt()));
+ }
+
+ for (JsonNode percentileAllocation : percentileAllocations) {
+ if (percentileAllocation.has("variant")) {
+ allocatedVariants.add(percentileAllocation.get("variant").asText());
+ }
+ }
+
+ allocationId.append(percentileAllocations.stream()
+ .map(pa -> pa.get("from") + ","
+ + Base64.getEncoder().encodeToString(pa.get("variant").asText().getBytes(StandardCharsets.UTF_8)) + ","
+ + pa.get("to"))
+ .collect(Collectors.joining(";")));
+
+ if (allocatedVariants.isEmpty() && (allocation.get("seed") == null)) {
+ return null;
+ }
+
+ // Variants
+ allocationId.append("\nvariants=");
+ List