diff --git a/.gitignore b/.gitignore
index 65975da2d..7f388619f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,3 +59,4 @@ CLAUDE.md
/backup
backup
*/tls
+/.cursor/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d1a47f23..cc807386c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,3 +16,4 @@
- [feat: support using seperated cb in wildcard-api level and add cb counters expire interval config](https://github.com/Tencent/spring-cloud-tencent/pull/1809)
- [feat: adapt to polaris-java ReportClientRequestCustomizer plugin for config watch reporting](https://github.com/Tencent/spring-cloud-tencent/pull/1813)
- [feat: support audit log](https://github.com/Tencent/spring-cloud-tencent/pull/1812)
+- [feat: support config effective value ](https://github.com/Tencent/spring-cloud-tencent/pull/1815)
diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/ConfigurationModifier.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/ConfigurationModifier.java
index 72dd0bee9..8a7f5f234 100644
--- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/ConfigurationModifier.java
+++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/ConfigurationModifier.java
@@ -32,6 +32,7 @@ import com.tencent.cloud.polaris.context.config.PolarisContextProperties;
import com.tencent.polaris.api.config.consumer.OutlierDetectionConfig;
import com.tencent.polaris.api.utils.CollectionUtils;
import com.tencent.polaris.api.utils.StringUtils;
+import com.tencent.polaris.configuration.client.internal.ConfigEffectiveQueryConfig;
import com.tencent.polaris.configuration.client.internal.ConfigWatchReportRequestCustomizer;
import com.tencent.polaris.configuration.client.internal.ConfigWatchReportRequestCustomizerConfig;
import com.tencent.polaris.factory.config.ConfigurationImpl;
@@ -76,6 +77,14 @@ public class ConfigurationModifier implements PolarisConfigurationConfigModifier
ConfigWatchReportRequestCustomizer.NAME, ConfigWatchReportRequestCustomizerConfig.class);
configWatchCustomizerConfig.setEnable(polarisConfigProperties.getReport().isEnabled());
customizerConfig.setPluginConfig(ConfigWatchReportRequestCustomizer.NAME, configWatchCustomizerConfig);
+ // enabled by default like the config watch report, and cascaded under report.enabled:
+ // disabling the whole report switch also disables the effective-time query channel
+ ConfigEffectiveQueryConfig configEffectiveConfig = customizerConfig.getPluginConfig(
+ ConfigEffectiveQueryConfig.NAME, ConfigEffectiveQueryConfig.class);
+ boolean effectiveEnabled = polarisConfigProperties.getReport().isEnabled()
+ && polarisConfigProperties.getReport().getEffective().isEnabled();
+ configEffectiveConfig.setEnable(effectiveEnabled);
+ customizerConfig.setPluginConfig(ConfigEffectiveQueryConfig.NAME, configEffectiveConfig);
configuration.getConsumer().getOutlierDetection().setWhen(OutlierDetectionConfig.When.never);
configuration.getConsumer().getCircuitBreaker().setEnable(false);
diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigEffectiveValueAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigEffectiveValueAutoConfiguration.java
new file mode 100644
index 000000000..5e014c078
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigEffectiveValueAutoConfiguration.java
@@ -0,0 +1,70 @@
+/*
+ * Tencent is pleased to support the open source community by making spring-cloud-tencent available.
+ *
+ * Copyright (C) 2021 Tencent. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed
+ * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+ * CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+package com.tencent.cloud.polaris.config;
+
+import com.tencent.cloud.polaris.config.adapter.SpringConfigEffectiveValueProvider;
+import com.tencent.polaris.configuration.api.core.ConfigEffectiveValueRegistration;
+import com.tencent.polaris.configuration.api.core.ConfigFileService;
+
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+/**
+ * Registers the Spring-Environment-based effective value provider into polaris-java
+ * for config effective-time realtime query.
+ *
+ * This auto-configuration is intentionally NOT registered as a BootstrapConfiguration:
+ * in legacy bootstrap mode the bootstrap context's Environment only carries bootstrap.yml
+ * sources, so a provider registered there would resolve stale or missing effective values.
+ * Being main-context-only guarantees the provider always captures the Environment the
+ * application actually reads. In legacy bootstrap mode the {@link ConfigFileService} bean
+ * is inherited from the bootstrap (parent) context.
+ *
+ * @author evelynwei
+ */
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnPolarisConfigEnabled
+@AutoConfigureAfter(PolarisConfigBootstrapAutoConfiguration.class)
+public class PolarisConfigEffectiveValueAutoConfiguration {
+
+ /**
+ * Enabled by default (aligned with the config watch report switch); set
+ * spring.cloud.polaris.config.report.effective.enabled=false to opt out. The returned
+ * registration is closed on context shutdown so the SDK never holds a destroyed
+ * Environment.
+ *
+ * {@code @AutoConfigureAfter} on this class guarantees the {@code configFileService}
+ * bean definition of {@link PolarisConfigBootstrapAutoConfiguration} is already
+ * processed, so {@code @ConditionalOnBean} sees it in ConfigData mode; in legacy
+ * bootstrap mode the bean is visible in the parent context.
+ */
+ @Bean(destroyMethod = "close")
+ @ConditionalOnBean(ConfigFileService.class)
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = "spring.cloud.polaris.config.report.effective", name = "enabled",
+ havingValue = "true", matchIfMissing = true)
+ public ConfigEffectiveValueRegistration polarisConfigEffectiveValueRegistration(
+ ConfigurableEnvironment environment, ConfigFileService configFileService) {
+ return configFileService.registerEffectiveValueProvider(new SpringConfigEffectiveValueProvider(environment));
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/SpringConfigEffectiveValueProvider.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/SpringConfigEffectiveValueProvider.java
new file mode 100644
index 000000000..092692ede
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/adapter/SpringConfigEffectiveValueProvider.java
@@ -0,0 +1,288 @@
+/*
+ * Tencent is pleased to support the open source community by making spring-cloud-tencent available.
+ *
+ * Copyright (C) 2021 Tencent. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed
+ * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+ * CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+package com.tencent.cloud.polaris.config.adapter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import com.tencent.polaris.configuration.api.core.ConfigEffectiveValueProvider;
+import com.tencent.polaris.configuration.api.core.ConfigFileMetadata;
+import com.tencent.polaris.configuration.api.core.ConfigKVFile;
+import com.tencent.polaris.configuration.api.core.ConfigKeyConflict;
+import com.tencent.polaris.configuration.api.core.EffectiveValue;
+import com.tencent.polaris.configuration.client.internal.CompositeConfigFile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.cloud.bootstrap.config.BootstrapPropertySource;
+import org.springframework.core.env.CompositePropertySource;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.PropertySource;
+
+/**
+ * Resolves key list, file value, effective value and conflict context from Spring
+ * {@link ConfigurableEnvironment}, for polaris-java config effective-time query.
+ *
+ * Config values must never be written to any log (including DEBUG): exception messages
+ * and stack traces may embed raw values (e.g. Spring's unresolvable-placeholder error
+ * quotes the whole value), so logs only carry key names, file coordinates and exception
+ * class names. Per the interface contract, methods must not throw: single-key failures
+ * degrade that key only.
+ *
+ * @author evelynwei
+ */
+public class SpringConfigEffectiveValueProvider implements ConfigEffectiveValueProvider {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SpringConfigEffectiveValueProvider.class);
+
+ private static final String SOURCE_PREFIX = "polaris:";
+
+ /**
+ * Name of Spring Boot's configuration-properties facade source
+ * ({@code ConfigurationPropertySourcesPropertySource}). Attached at the head of the
+ * Environment, its containsProperty delegates to every underlying source, so it
+ * matches any key and would shadow the real source.
+ */
+ private static final String CONFIGURATION_PROPERTIES_SOURCE_NAME = "configurationProperties";
+
+ private final ConfigurableEnvironment environment;
+
+ public SpringConfigEffectiveValueProvider(ConfigurableEnvironment environment) {
+ this.environment = environment;
+ }
+
+ @Override
+ public List getKeys(ConfigFileMetadata configFile) {
+ try {
+ ConfigKVFile file = findConfigKVFile(configFile);
+ if (file == null) {
+ return Collections.emptyList();
+ }
+ List keys = new ArrayList<>(file.getPropertyNames());
+ // Sort for stable output, friendly to tests and console display.
+ Collections.sort(keys);
+ return keys;
+ }
+ catch (Throwable t) {
+ LOG.warn("[SCT Config] Get keys failed, file = {}, error = {}", coordinateOf(configFile),
+ t.getClass().getSimpleName());
+ return Collections.emptyList();
+ }
+ }
+
+ @Override
+ public EffectiveValue resolve(String key, ConfigFileMetadata configFile) {
+ String fileValue;
+ try {
+ ConfigKVFile file = findConfigKVFile(configFile);
+ // Raw value in the file (may contain unresolved placeholders).
+ fileValue = file == null ? null : file.getProperty(key, null);
+ }
+ catch (Throwable t) {
+ // Contract: return null instead of throwing; SDK degrades this key only.
+ LOG.warn("[SCT Config] Resolve file value failed, key = {}, file = {}, error = {}", key,
+ coordinateOf(configFile), t.getClass().getSimpleName());
+ return null;
+ }
+ String effectiveValue = null;
+ String propertySource = null;
+ try {
+ // Effective value: Environment has already converged by precedence and
+ // resolves ${} placeholders, i.e. the value the application actually reads.
+ effectiveValue = environment.getProperty(key);
+ propertySource = resolvePropertySourceName(key);
+ }
+ catch (Throwable t) {
+ // Only degrade the effective-value dimension; the file value already in hand is returned.
+ LOG.warn("[SCT Config] Resolve effective value failed, key = {}, error = {}", key,
+ t.getClass().getSimpleName());
+ }
+ return new EffectiveValue(fileValue, effectiveValue, propertySource);
+ }
+
+ @Override
+ public List resolveConflicts(String key, ConfigFileMetadata excludeFile) {
+ List conflicts = new ArrayList<>();
+ // Static call: PolarisPropertySourceManager is not a bean and cannot be injected.
+ // Used here only to enumerate the watched-file set, never for precedence.
+ for (PolarisPropertySource source : PolarisPropertySourceManager.getAllPropertySources()) {
+ for (ConfigKVFile file : expandSubs(source.getConfigKVFile())) {
+ try {
+ collectIfConflict(conflicts, key, file, excludeFile);
+ }
+ catch (Throwable t) {
+ // Per-file fallback: one broken file must not drop conflicts already collected.
+ LOG.warn("[SCT Config] Resolve conflicts failed, key = {}, error = {}", key,
+ t.getClass().getSimpleName());
+ }
+ }
+ }
+ return conflicts;
+ }
+
+ /**
+ * Walks the ordered PropertySource chain of the Environment and returns the source
+ * identity of the first one containing the key. Iteration order of
+ * MutablePropertySources is exactly Spring's precedence order.
+ */
+ private String resolvePropertySourceName(String key) {
+ for (PropertySource> source : environment.getPropertySources()) {
+ String name = matchSource(source, key);
+ if (name != null) {
+ return name;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Matches the key inside a single PropertySource, unwrapping wrappers first:
+ * in bootstrap mode polaris sources are wrapped into a CompositePropertySource
+ * ("polaris-config") and then into BootstrapPropertySource, so recursion is needed
+ * to reach the real PolarisPropertySource. Polaris sources are translated to a
+ * normalized coordinate {@code polaris:namespace/group/fileName}.
+ */
+ private String matchSource(PropertySource> source, String key) {
+ if (CONFIGURATION_PROPERTIES_SOURCE_NAME.equals(source.getName())) {
+ return null;
+ }
+ if (source instanceof CompositePropertySource) {
+ for (PropertySource> sub : ((CompositePropertySource) source).getPropertySources()) {
+ String name = matchSource(sub, key);
+ if (name != null) {
+ return name;
+ }
+ }
+ return null;
+ }
+ if (source instanceof BootstrapPropertySource) {
+ return matchSource(((BootstrapPropertySource>) source).getDelegate(), key);
+ }
+ if (!source.containsProperty(key)) {
+ return null;
+ }
+ if (source instanceof PolarisPropertySource) {
+ PolarisPropertySource polarisSource = (PolarisPropertySource) source;
+ ConfigKVFile file = polarisSource.getConfigKVFile();
+ if (file instanceof CompositeConfigFile) {
+ // Group-dimension load: merge semantics is first-file-wins, so the first sub
+ // file containing the key is the effective one.
+ for (ConfigKVFile sub : ((CompositeConfigFile) file).getConfigKVFiles()) {
+ if (sub.getProperty(key, null) != null) {
+ return formatCoordinate(sub);
+ }
+ }
+ // Fallback: the composite's sub list is frozen at startup and does not cover
+ // files added to the group at runtime; look them up by group coordinate.
+ ConfigKVFile added = findInGroup(polarisSource.getNamespace(), polarisSource.getGroup(), key);
+ if (added != null) {
+ return formatCoordinate(added);
+ }
+ }
+ else {
+ return formatCoordinate(file);
+ }
+ }
+ return source.getName();
+ }
+
+ /**
+ * Finds the watched ConfigKVFile by coordinate. Sources loaded by file dimension match
+ * directly; sources loaded by group dimension carry an empty fileName and their
+ * CompositeConfigFile must be expanded to match sub files.
+ */
+ private ConfigKVFile findConfigKVFile(ConfigFileMetadata metadata) {
+ for (PolarisPropertySource source : PolarisPropertySourceManager.getAllPropertySources()) {
+ for (ConfigKVFile file : expandSubs(source.getConfigKVFile())) {
+ if (sameFile(file, metadata)) {
+ return file;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Finds a watched file in the given namespace/group containing the key. Used as the
+ * fallback for group sources whose frozen composite misses runtime-added files.
+ */
+ private ConfigKVFile findInGroup(String namespace, String group, String key) {
+ for (PolarisPropertySource source : PolarisPropertySourceManager.getAllPropertySources()) {
+ for (ConfigKVFile file : expandSubs(source.getConfigKVFile())) {
+ if (Objects.equals(file.getNamespace(), namespace) && Objects.equals(file.getFileGroup(), group)
+ && file.getProperty(key, null) != null) {
+ return file;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Expands a CompositeConfigFile to its sub files; a plain file expands to itself.
+ */
+ private List expandSubs(ConfigKVFile file) {
+ if (file instanceof CompositeConfigFile) {
+ List subs = ((CompositeConfigFile) file).getConfigKVFiles();
+ return subs == null ? Collections.emptyList() : subs;
+ }
+ return Collections.singletonList(file);
+ }
+
+ private void collectIfConflict(List conflicts, String key, ConfigKVFile candidate,
+ ConfigFileMetadata excludeFile) {
+ if (sameFile(candidate, excludeFile)) {
+ return;
+ }
+ String value = candidate.getProperty(key, null);
+ if (value == null) {
+ return;
+ }
+ // The same file may be watched at both file dimension and group dimension; dedupe by coordinate.
+ for (ConfigKeyConflict existing : conflicts) {
+ if (Objects.equals(existing.getNamespace(), candidate.getNamespace())
+ && Objects.equals(existing.getGroup(), candidate.getFileGroup())
+ && Objects.equals(existing.getFileName(), candidate.getFileName())) {
+ return;
+ }
+ }
+ // Coordinate + value only; never the full content of the conflict file.
+ conflicts.add(new ConfigKeyConflict(candidate.getNamespace(), candidate.getFileGroup(),
+ candidate.getFileName(), value));
+ }
+
+ private boolean sameFile(ConfigKVFile candidate, ConfigFileMetadata metadata) {
+ return metadata != null
+ && Objects.equals(candidate.getNamespace(), metadata.getNamespace())
+ && Objects.equals(candidate.getFileGroup(), metadata.getFileGroup())
+ && Objects.equals(candidate.getFileName(), metadata.getFileName());
+ }
+
+ private String formatCoordinate(ConfigKVFile file) {
+ return SOURCE_PREFIX + file.getNamespace() + "/" + file.getFileGroup() + "/" + file.getFileName();
+ }
+
+ private String coordinateOf(ConfigFileMetadata metadata) {
+ if (metadata == null) {
+ return "null";
+ }
+ return metadata.getNamespace() + "/" + metadata.getFileGroup() + "/" + metadata.getFileName();
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PolarisConfigProperties.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PolarisConfigProperties.java
index 261f05643..3d2d50221 100644
--- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PolarisConfigProperties.java
+++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/config/PolarisConfigProperties.java
@@ -275,6 +275,11 @@ public class PolarisConfigProperties {
*/
private boolean enabled = true;
+ /**
+ * Config effective-time realtime query settings.
+ */
+ private Effective effective = new Effective();
+
public boolean isEnabled() {
return enabled;
}
@@ -283,11 +288,48 @@ public class PolarisConfigProperties {
this.enabled = enabled;
}
+ public Effective getEffective() {
+ return effective;
+ }
+
+ public void setEffective(Effective effective) {
+ this.effective = effective;
+ }
+
@Override
public String toString() {
return "Report{" +
"enabled=" + enabled +
+ ", effective=" + effective +
'}';
}
+
+ /**
+ * Config effective-time realtime query settings.
+ */
+ public static class Effective {
+
+ /**
+ * Whether to enable config effective-time realtime query.
+ * Default true, aligned with the config watch metadata report switch.
+ * Effective only when {@link Report#enabled} is also true (cascaded).
+ */
+ private boolean enabled = true;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @Override
+ public String toString() {
+ return "Effective{" +
+ "enabled=" + enabled +
+ '}';
+ }
+ }
}
}
diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/endpoint/PolarisConfigEndpoint.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/endpoint/PolarisConfigEndpoint.java
index 0942fb2f9..04788b6d0 100644
--- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/endpoint/PolarisConfigEndpoint.java
+++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/endpoint/PolarisConfigEndpoint.java
@@ -21,6 +21,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import com.tencent.cloud.polaris.config.PolarisConfigSDKContextManager;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySource;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceManager;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
@@ -50,6 +51,22 @@ public class PolarisConfigEndpoint {
List propertySourceList = PolarisPropertySourceManager.getAllPropertySources();
configInfo.put("PolarisPropertySource", propertySourceList);
+ configInfo.put("ClientId", getClientId());
+
return configInfo;
}
+
+ /**
+ * The config SDK context is created in the config-data phase, so its startup logs may be
+ * dropped before the polaris log appenders are ready. Exposing the client id here gives
+ * tooling a reliable source instead of grepping logs.
+ */
+ private String getClientId() {
+ try {
+ return PolarisConfigSDKContextManager.innerGetConfigSDKContext().getValueContext().getClientId();
+ }
+ catch (Throwable throwable) {
+ return null;
+ }
+ }
}
diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-starter-tencent-polaris-config/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index 8c94b1e71..874fd0497 100644
--- a/spring-cloud-starter-tencent-polaris-config/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/spring-cloud-starter-tencent-polaris-config/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -92,6 +92,13 @@
"description": "Whether to report config watch metadata through the Polaris client reporter.",
"sourceType": "com.tencent.cloud.polaris.config.config.PolarisConfigProperties"
},
+ {
+ "name": "spring.cloud.polaris.config.report.effective.enabled",
+ "type": "java.lang.Boolean",
+ "defaultValue": true,
+ "description": "Whether to enable config effective-time realtime query. Default true and cascaded under spring.cloud.polaris.config.report.enabled (disabling the whole report switch also disables this query channel).",
+ "sourceType": "com.tencent.cloud.polaris.config.config.PolarisConfigProperties"
+ },
{
"name": "spring.cloud.polaris.config.crypto.enabled",
"type": "java.lang.Boolean",
diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-starter-tencent-polaris-config/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index bad1ece40..e42410bc5 100644
--- a/spring-cloud-starter-tencent-polaris-config/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-cloud-starter-tencent-polaris-config/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1,4 +1,5 @@
com.tencent.cloud.polaris.config.PolarisConfigAutoConfiguration
com.tencent.cloud.polaris.config.endpoint.PolarisConfigEndpointAutoConfiguration
com.tencent.cloud.polaris.config.PolarisConfigBootstrapAutoConfiguration
+com.tencent.cloud.polaris.config.PolarisConfigEffectiveValueAutoConfiguration
com.tencent.cloud.polaris.config.tsf.PolarisAdaptorTsfConfigAutoConfiguration
diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/ConfigurationModifierTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/ConfigurationModifierTest.java
index 4a46887fa..84558f28a 100644
--- a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/ConfigurationModifierTest.java
+++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/ConfigurationModifierTest.java
@@ -32,6 +32,7 @@ import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
import com.tencent.cloud.polaris.config.config.PolarisCryptoConfigProperties;
import com.tencent.cloud.polaris.context.config.PolarisContextProperties;
import com.tencent.polaris.api.config.consumer.OutlierDetectionConfig;
+import com.tencent.polaris.configuration.client.internal.ConfigEffectiveQueryConfig;
import com.tencent.polaris.configuration.client.internal.ConfigWatchReportRequestCustomizer;
import com.tencent.polaris.configuration.client.internal.ConfigWatchReportRequestCustomizerConfig;
import com.tencent.polaris.factory.config.ConfigurationImpl;
@@ -81,12 +82,18 @@ class ConfigurationModifierTest {
@Mock
private PolarisConfigProperties.Report report;
+ @Mock
+ private PolarisConfigProperties.Report.Effective effective;
+
@Mock
private PolarisContextProperties polarisContextProperties;
@Mock
private ConfigWatchReportRequestCustomizerConfig configWatchCustomizer;
+ @Mock
+ private ConfigEffectiveQueryConfig configEffectiveCustomizer;
+
private ConfigurationModifier configurationModifier;
@BeforeEach
@@ -95,6 +102,8 @@ class ConfigurationModifierTest {
polarisConfigProperties, polarisCryptoConfigProperties, polarisContextProperties);
Mockito.lenient().when(polarisConfigProperties.getReport()).thenReturn(report);
Mockito.lenient().when(report.isEnabled()).thenReturn(true);
+ Mockito.lenient().when(report.getEffective()).thenReturn(effective);
+ Mockito.lenient().when(effective.isEnabled()).thenReturn(true);
}
/**
@@ -113,6 +122,8 @@ class ConfigurationModifierTest {
when(globalConfig.getReportClientRequestCustomizer()).thenReturn(requestCustomizer);
when(requestCustomizer.getPluginConfig(ConfigWatchReportRequestCustomizer.NAME,
ConfigWatchReportRequestCustomizerConfig.class)).thenReturn(configWatchCustomizer);
+ when(requestCustomizer.getPluginConfig(ConfigEffectiveQueryConfig.NAME,
+ ConfigEffectiveQueryConfig.class)).thenReturn(configEffectiveCustomizer);
Mockito.lenient().when(globalConfig.getServerConnector()).thenReturn(serverConnector);
Mockito.lenient().when(globalConfig.getAPI()).thenReturn(apiConfig);
when(configuration.getGlobal()).thenReturn(globalConfig);
@@ -220,6 +231,55 @@ class ConfigurationModifierTest {
verify(configuration.getConfigFile(), never()).getServerConnector();
}
+ @DisplayName("modify should disable config effective query when configured")
+ @Test
+ void testModify_ConfigEffectiveQueryDisabled() {
+ ConfigurationImpl configuration = buildMockConfiguration();
+ when(effective.isEnabled()).thenReturn(false);
+ when(polarisContextProperties.getEnabled()).thenReturn(false);
+
+ configurationModifier.modify(configuration);
+
+ // an explicit false always overrides the SDK-side default
+ verify(configEffectiveCustomizer).setEnable(false);
+ verify(configuration.getGlobal().getReportClientRequestCustomizer())
+ .setPluginConfig(ConfigEffectiveQueryConfig.NAME, configEffectiveCustomizer);
+ verify(configuration.getConfigFile(), never()).getServerConnector();
+ }
+
+ @DisplayName("modify should enable config effective query when configured")
+ @Test
+ void testModify_ConfigEffectiveQueryEnabled() {
+ ConfigurationImpl configuration = buildMockConfiguration();
+ when(effective.isEnabled()).thenReturn(true);
+ when(polarisContextProperties.getEnabled()).thenReturn(false);
+
+ configurationModifier.modify(configuration);
+
+ verify(configEffectiveCustomizer).setEnable(true);
+ verify(configuration.getGlobal().getReportClientRequestCustomizer())
+ .setPluginConfig(ConfigEffectiveQueryConfig.NAME, configEffectiveCustomizer);
+ verify(configuration.getConfigFile(), never()).getServerConnector();
+ }
+
+ @DisplayName("modify should cascade: report disabled also disables config effective query")
+ @Test
+ void testModify_ConfigEffectiveQueryCascadesWithReportDisabled() {
+ ConfigurationImpl configuration = buildMockConfiguration();
+ // report.enabled=false 级联关闭 effective query,即使 effective.enabled 显式为 true
+ // (级联短路后 effective.isEnabled() 不再被读取,故用 lenient)
+ when(report.isEnabled()).thenReturn(false);
+ Mockito.lenient().when(effective.isEnabled()).thenReturn(true);
+ when(polarisContextProperties.getEnabled()).thenReturn(false);
+
+ configurationModifier.modify(configuration);
+
+ verify(configEffectiveCustomizer).setEnable(false);
+ verify(configuration.getGlobal().getReportClientRequestCustomizer())
+ .setPluginConfig(ConfigEffectiveQueryConfig.NAME, configEffectiveCustomizer);
+ verify(configuration.getConfigFile(), never()).getServerConnector();
+ }
+
/**
* Test modify with local file data source.
* Scenario: dataSource is "localFile", both polaris and config are enabled.
diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/PolarisConfigEffectiveValueRegistrationTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/PolarisConfigEffectiveValueRegistrationTest.java
new file mode 100644
index 000000000..a20ee68b5
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/PolarisConfigEffectiveValueRegistrationTest.java
@@ -0,0 +1,97 @@
+/*
+ * Tencent is pleased to support the open source community by making spring-cloud-tencent available.
+ *
+ * Copyright (C) 2021 Tencent. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed
+ * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+ * CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+package com.tencent.cloud.polaris.config;
+
+import com.tencent.cloud.polaris.config.adapter.SpringConfigEffectiveValueProvider;
+import com.tencent.polaris.configuration.api.core.ConfigEffectiveValueProvider;
+import com.tencent.polaris.configuration.api.core.ConfigEffectiveValueRegistration;
+import com.tencent.polaris.configuration.api.core.ConfigFileService;
+import com.tencent.polaris.configuration.api.core.EffectiveValue;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for {@link PolarisConfigEffectiveValueAutoConfiguration}. The registration bean must
+ * only be created in the main application context: in legacy bootstrap mode the bootstrap
+ * context's Environment carries only bootstrap.yml sources, so a provider registered there
+ * would report stale effective values.
+ *
+ * @author evelynwei
+ */
+// The mocked AutoCloseable registrations are never closed; the real bean is closed via destroyMethod.
+@SuppressWarnings("try")
+class PolarisConfigEffectiveValueRegistrationTest {
+
+ private final ApplicationContextRunner runner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(PolarisConfigEffectiveValueAutoConfiguration.class));
+
+ @Test
+ void testRegistrationBeanCreatedWhenConfigFileServicePresent() {
+ ConfigFileService configFileService = mock(ConfigFileService.class);
+ ConfigEffectiveValueRegistration registration = mock(ConfigEffectiveValueRegistration.class);
+ when(configFileService.registerEffectiveValueProvider(any(ConfigEffectiveValueProvider.class)))
+ .thenReturn(registration);
+
+ runner.withBean(ConfigFileService.class, () -> configFileService)
+ .run(context -> {
+ assertThat(context).hasSingleBean(ConfigEffectiveValueRegistration.class);
+ verify(configFileService).registerEffectiveValueProvider(
+ any(SpringConfigEffectiveValueProvider.class));
+ });
+ }
+
+ @Test
+ void testRegistrationBeanSkippedWhenConfigFileServiceAbsent() {
+ // local data source etc.: no ConfigFileService bean, provider must not be registered
+ runner.run(context -> assertThat(context).doesNotHaveBean(ConfigEffectiveValueRegistration.class));
+ }
+
+ @Test
+ void testRegistrationBeanSkippedWhenExplicitlyDisabled() {
+ ConfigFileService configFileService = mock(ConfigFileService.class);
+ runner.withBean(ConfigFileService.class, () -> configFileService)
+ .withPropertyValues("spring.cloud.polaris.config.report.effective.enabled=false")
+ .run(context -> assertThat(context).doesNotHaveBean(ConfigEffectiveValueRegistration.class));
+ }
+
+ @Test
+ void testProviderResolvesAgainstRegisteringContextEnvironment() {
+ ConfigFileService configFileService = mock(ConfigFileService.class);
+ ConfigEffectiveValueRegistration registration = mock(ConfigEffectiveValueRegistration.class);
+ ArgumentCaptor providerCaptor =
+ ArgumentCaptor.forClass(ConfigEffectiveValueProvider.class);
+ when(configFileService.registerEffectiveValueProvider(providerCaptor.capture()))
+ .thenReturn(registration);
+
+ runner.withBean(ConfigFileService.class, () -> configFileService)
+ .withPropertyValues("sct.test.key=from-main-environment")
+ .run(context -> {
+ EffectiveValue value = providerCaptor.getValue().resolve("sct.test.key", null);
+ assertThat(value.getEffectiveValue()).isEqualTo("from-main-environment");
+ });
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/SpringConfigEffectiveValueProviderTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/SpringConfigEffectiveValueProviderTest.java
new file mode 100644
index 000000000..18478f8ed
--- /dev/null
+++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/adapter/SpringConfigEffectiveValueProviderTest.java
@@ -0,0 +1,313 @@
+/*
+ * Tencent is pleased to support the open source community by making spring-cloud-tencent available.
+ *
+ * Copyright (C) 2021 Tencent. All rights reserved.
+ *
+ * Licensed under the BSD 3-Clause License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed
+ * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+ * CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+package com.tencent.cloud.polaris.config.adapter;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.tencent.polaris.configuration.api.core.ConfigFileMetadata;
+import com.tencent.polaris.configuration.api.core.ConfigKVFile;
+import com.tencent.polaris.configuration.api.core.ConfigKeyConflict;
+import com.tencent.polaris.configuration.api.core.EffectiveValue;
+import com.tencent.polaris.configuration.client.internal.CompositeConfigFile;
+import com.tencent.polaris.configuration.client.internal.DefaultConfigFileMetadata;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import org.springframework.cloud.bootstrap.config.BootstrapPropertySource;
+import org.springframework.core.env.CompositePropertySource;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.core.env.StandardEnvironment;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for {@link SpringConfigEffectiveValueProvider}.
+ *
+ * @author evelynwei
+ */
+@ExtendWith(MockitoExtension.class)
+class SpringConfigEffectiveValueProviderTest {
+
+ private static final String NAMESPACE = "default";
+
+ private static final String GROUP = "order-service";
+
+ private static final String FILE_NAME = "application.yaml";
+
+ private StandardEnvironment environment;
+
+ private SpringConfigEffectiveValueProvider provider;
+
+ @BeforeEach
+ void setUp() {
+ PolarisPropertySourceManager.clearPropertySources();
+ environment = new StandardEnvironment();
+ provider = new SpringConfigEffectiveValueProvider(environment);
+ }
+
+ @AfterEach
+ void tearDown() {
+ PolarisPropertySourceManager.clearPropertySources();
+ }
+
+ /**
+ * Registers a file-dimension PolarisPropertySource into both the Environment chain
+ * (at the tail, below system properties like the real runtime) and the static manager.
+ */
+ private PolarisPropertySource addFileSource(String namespace, String group, String fileName,
+ Map props) {
+ ConfigKVFile kvFile = mockConfigKVFile(namespace, group, fileName, props);
+ PolarisPropertySource source = new PolarisPropertySource(namespace, group, fileName, kvFile,
+ new HashMap(props));
+ PolarisPropertySourceManager.addPropertySource(source);
+ environment.getPropertySources().addLast(source);
+ return source;
+ }
+
+ private ConfigKVFile mockConfigKVFile(String namespace, String group, String fileName,
+ Map props) {
+ ConfigKVFile kvFile = mock(ConfigKVFile.class);
+ lenient().when(kvFile.getNamespace()).thenReturn(namespace);
+ lenient().when(kvFile.getFileGroup()).thenReturn(group);
+ lenient().when(kvFile.getFileName()).thenReturn(fileName);
+ lenient().when(kvFile.getPropertyNames()).thenReturn(props.keySet());
+ lenient().when(kvFile.getProperty(anyString(), any())).thenAnswer(invocation -> {
+ String key = invocation.getArgument(0);
+ return props.getOrDefault(key, invocation.getArgument(1));
+ });
+ return kvFile;
+ }
+
+ private ConfigFileMetadata metadata(String namespace, String group, String fileName) {
+ return new DefaultConfigFileMetadata(namespace, group, fileName);
+ }
+
+ @Test
+ void testCommandLineArgsOverridesPolarisValue() {
+ addFileSource(NAMESPACE, GROUP, FILE_NAME, Collections.singletonMap("server.port", "8080"));
+ environment.getPropertySources().addFirst(new MapPropertySource("commandLineArgs",
+ Collections.singletonMap("server.port", "9090")));
+
+ EffectiveValue value = provider.resolve("server.port", metadata(NAMESPACE, GROUP, FILE_NAME));
+
+ assertThat(value).isNotNull();
+ assertThat(value.getFileValue()).isEqualTo("8080");
+ assertThat(value.getEffectiveValue()).isEqualTo("9090");
+ assertThat(value.getPropertySource()).isEqualTo("commandLineArgs");
+ }
+
+ @Test
+ void testEffectiveValueResolvesPlaceholder() {
+ Map props = new LinkedHashMap<>();
+ props.put("server.port", "${http.port:8080}");
+ props.put("http.port", "8081");
+ addFileSource(NAMESPACE, GROUP, FILE_NAME, props);
+
+ EffectiveValue value = provider.resolve("server.port", metadata(NAMESPACE, GROUP, FILE_NAME));
+
+ assertThat(value).isNotNull();
+ // file value keeps the raw placeholder, effective value is what the application reads
+ assertThat(value.getFileValue()).isEqualTo("${http.port:8080}");
+ assertThat(value.getEffectiveValue()).isEqualTo("8081");
+ // still sourced from the polaris file itself: normalized coordinate
+ assertThat(value.getPropertySource()).isEqualTo("polaris:default/order-service/application.yaml");
+ }
+
+ @Test
+ void testGetKeysSortedAndComplete() {
+ Map props = new LinkedHashMap<>();
+ props.put("b.key", "2");
+ props.put("a.key", "1");
+ props.put("c.key", "3");
+ addFileSource(NAMESPACE, GROUP, FILE_NAME, props);
+
+ List keys = provider.getKeys(metadata(NAMESPACE, GROUP, FILE_NAME));
+
+ assertThat(keys).containsExactly("a.key", "b.key", "c.key");
+ // unknown coordinate: empty list, and SDK will omit the whole properties field
+ assertThat(provider.getKeys(metadata(NAMESPACE, GROUP, "unknown.yaml"))).isEmpty();
+ }
+
+ @Test
+ void testConflictsAcrossMultipleWatchedFiles() {
+ addFileSource(NAMESPACE, GROUP, FILE_NAME, Collections.singletonMap("k", "1"));
+ addFileSource(NAMESPACE, "common", "common.yaml", Collections.singletonMap("k", "2"));
+
+ List conflicts = provider.resolveConflicts("k", metadata(NAMESPACE, GROUP, FILE_NAME));
+
+ assertThat(conflicts).hasSize(1);
+ ConfigKeyConflict conflict = conflicts.get(0);
+ assertThat(conflict.getNamespace()).isEqualTo(NAMESPACE);
+ assertThat(conflict.getGroup()).isEqualTo("common");
+ assertThat(conflict.getFileName()).isEqualTo("common.yaml");
+ assertThat(conflict.getValue()).isEqualTo("2");
+
+ // querying from the other side excludes itself and reports application.yaml
+ List reverse = provider.resolveConflicts("k", metadata(NAMESPACE, "common", "common.yaml"));
+ assertThat(reverse).hasSize(1);
+ assertThat(reverse.get(0).getFileName()).isEqualTo(FILE_NAME);
+ assertThat(reverse.get(0).getValue()).isEqualTo("1");
+ }
+
+ @Test
+ void testCompositeConfigFileExpanded() {
+ // group-dimension loading: sub files merged into a CompositeConfigFile, first file wins
+ ConfigKVFile subA = mockConfigKVFile(NAMESPACE, "mygroup", "a.yaml", Collections.singletonMap("k", "1"));
+ ConfigKVFile subB = mockConfigKVFile(NAMESPACE, "mygroup", "b.yaml", Collections.singletonMap("k", "2"));
+ CompositeConfigFile composite = new CompositeConfigFile(Arrays.asList(subA, subB));
+ Map merged = new HashMap<>();
+ merged.put("k", "1");
+ PolarisPropertySource groupSource = new PolarisPropertySource(NAMESPACE, "mygroup", "", composite, merged);
+ PolarisPropertySourceManager.addPropertySource(groupSource);
+ environment.getPropertySources().addLast(groupSource);
+
+ // query the losing sub file b.yaml
+ EffectiveValue value = provider.resolve("k", metadata(NAMESPACE, "mygroup", "b.yaml"));
+
+ assertThat(value).isNotNull();
+ // file value from sub file b, effective value from the merged group map (a wins)
+ assertThat(value.getFileValue()).isEqualTo("2");
+ assertThat(value.getEffectiveValue()).isEqualTo("1");
+ // property source points to the winning sub file, not the opaque group source name
+ assertThat(value.getPropertySource()).isEqualTo("polaris:default/mygroup/a.yaml");
+
+ // conflicts are sub-file grained: b is excluded, a is reported
+ List conflicts = provider.resolveConflicts("k", metadata(NAMESPACE, "mygroup", "b.yaml"));
+ assertThat(conflicts).hasSize(1);
+ assertThat(conflicts.get(0).getFileName()).isEqualTo("a.yaml");
+ assertThat(conflicts.get(0).getValue()).isEqualTo("1");
+
+ // getKeys finds the sub file inside the composite
+ assertThat(provider.getKeys(metadata(NAMESPACE, "mygroup", "a.yaml"))).containsExactly("k");
+ }
+
+ @Test
+ void testResolveKeepsFileValueWhenEffectiveResolutionFails() {
+ // environment.getProperty 抛异常(如不可解析占位符):生效值维度降级为 null,
+ // 但已到手的文件原始值照常返回
+ addFileSource(NAMESPACE, GROUP, FILE_NAME, Collections.singletonMap("k", "file-v"));
+ ConfigurableEnvironment broken = mock(ConfigurableEnvironment.class);
+ when(broken.getProperty(anyString()))
+ .thenThrow(new IllegalArgumentException("Could not resolve placeholder"));
+ SpringConfigEffectiveValueProvider failingProvider = new SpringConfigEffectiveValueProvider(broken);
+
+ EffectiveValue value = failingProvider.resolve("k", metadata(NAMESPACE, GROUP, FILE_NAME));
+
+ assertThat(value).isNotNull();
+ assertThat(value.getFileValue()).isEqualTo("file-v");
+ assertThat(value.getEffectiveValue()).isNull();
+ assertThat(value.getPropertySource()).isNull();
+ }
+
+ @Test
+ void testConflictsSkipsBrokenFile() {
+ // 一个坏文件(getProperty 抛异常)不应丢弃已从健康文件收集的冲突
+ addFileSource(NAMESPACE, "common", "common.yaml", Collections.singletonMap("k", "2"));
+ ConfigKVFile broken = mock(ConfigKVFile.class);
+ lenient().when(broken.getNamespace()).thenReturn(NAMESPACE);
+ lenient().when(broken.getFileGroup()).thenReturn("broken-group");
+ lenient().when(broken.getFileName()).thenReturn("broken.yaml");
+ when(broken.getProperty(anyString(), any())).thenThrow(new RuntimeException("boom"));
+ PolarisPropertySource brokenSource = new PolarisPropertySource(NAMESPACE, "broken-group", "broken.yaml",
+ broken, new HashMap());
+ PolarisPropertySourceManager.addPropertySource(brokenSource);
+
+ List conflicts = provider.resolveConflicts("k", metadata(NAMESPACE, GROUP, FILE_NAME));
+
+ assertThat(conflicts).hasSize(1);
+ assertThat(conflicts.get(0).getFileName()).isEqualTo("common.yaml");
+ assertThat(conflicts.get(0).getValue()).isEqualTo("2");
+ }
+
+ @Test
+ void testConflictsDedupWhenWatchedAtBothDimensions() {
+ // 同一文件同时以文件维度与 group 维度监听注册:冲突按坐标去重
+ Map props = Collections.singletonMap("k", "2");
+ addFileSource(NAMESPACE, "common", "common.yaml", props);
+ ConfigKVFile sub = mockConfigKVFile(NAMESPACE, "common", "common.yaml", props);
+ CompositeConfigFile composite = new CompositeConfigFile(Collections.singletonList(sub));
+ PolarisPropertySource groupSource = new PolarisPropertySource(NAMESPACE, "common", "", composite,
+ new HashMap(props));
+ PolarisPropertySourceManager.addPropertySource(groupSource);
+
+ List conflicts = provider.resolveConflicts("k", metadata(NAMESPACE, GROUP, FILE_NAME));
+
+ assertThat(conflicts).hasSize(1);
+ assertThat(conflicts.get(0).getFileName()).isEqualTo("common.yaml");
+ assertThat(conflicts.get(0).getValue()).isEqualTo("2");
+ }
+
+ @Test
+ void testBootstrapWrappedSourcesUnwrapped() {
+ // bootstrap 模式:PolarisPropertySource 被 CompositePropertySource("polaris-config") 聚合,
+ // 外层再被 BootstrapPropertySource 包装——来源解析需递归解包出坐标
+ PolarisPropertySource polarisSource = addFileSource(NAMESPACE, GROUP, FILE_NAME,
+ Collections.singletonMap("k", "1"));
+ environment.getPropertySources().remove(polarisSource.getName());
+ CompositePropertySource composite = new CompositePropertySource("polaris-config");
+ composite.addPropertySource(polarisSource);
+ environment.getPropertySources().addLast(new BootstrapPropertySource<>(composite));
+
+ EffectiveValue value = provider.resolve("k", metadata(NAMESPACE, GROUP, FILE_NAME));
+
+ assertThat(value).isNotNull();
+ assertThat(value.getEffectiveValue()).isEqualTo("1");
+ assertThat(value.getPropertySource()).isEqualTo("polaris:default/order-service/application.yaml");
+ }
+
+ @Test
+ void testConfigurationPropertiesFacadeSourceSkipped() {
+ // Spring Boot 的 configurationProperties facade source 挂在链头,
+ // 其 containsProperty 委托给所有底层 source(任意 key 都命中),
+ // 不跳过会遮蔽真实来源,property_source 恒为 "configurationProperties"
+ addFileSource(NAMESPACE, GROUP, FILE_NAME, Collections.singletonMap("k", "1"));
+ environment.getPropertySources().addFirst(new MapPropertySource("configurationProperties",
+ Collections.singletonMap("k", "1")));
+
+ EffectiveValue value = provider.resolve("k", metadata(NAMESPACE, GROUP, FILE_NAME));
+
+ assertThat(value).isNotNull();
+ assertThat(value.getPropertySource()).isEqualTo("polaris:default/order-service/application.yaml");
+ }
+
+ @Test
+ void testKeyNotPresentAnywhere() {
+ addFileSource(NAMESPACE, GROUP, FILE_NAME, Collections.singletonMap("other.key", "1"));
+
+ EffectiveValue value = provider.resolve("missing.key", metadata(NAMESPACE, GROUP, FILE_NAME));
+
+ assertThat(value).isNotNull();
+ assertThat(value.getFileValue()).isNull();
+ assertThat(value.getEffectiveValue()).isNull();
+ assertThat(value.getPropertySource()).isNull();
+ }
+}
diff --git a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/endpoint/PolarisConfigEndpointTest.java b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/endpoint/PolarisConfigEndpointTest.java
index f077e50ba..51bc92975 100644
--- a/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/endpoint/PolarisConfigEndpointTest.java
+++ b/spring-cloud-starter-tencent-polaris-config/src/test/java/com/tencent/cloud/polaris/config/endpoint/PolarisConfigEndpointTest.java
@@ -21,14 +21,19 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import com.tencent.cloud.polaris.config.PolarisConfigSDKContextManager;
import com.tencent.cloud.polaris.config.adapter.MockedConfigKVFile;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySource;
import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceManager;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
+import com.tencent.polaris.api.plugin.common.ValueContext;
+import com.tencent.polaris.client.api.SDKContext;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
@@ -53,6 +58,11 @@ public class PolarisConfigEndpointTest {
PolarisPropertySourceManager.clearPropertySources();
}
+ @AfterEach
+ public void tearDown() {
+ PolarisConfigSDKContextManager.innerConfigDestroy();
+ }
+
@Test
public void testPolarisConfigEndpoint() {
Map content = new HashMap<>();
@@ -69,4 +79,24 @@ public class PolarisConfigEndpointTest {
assertThat(polarisConfigProperties).isEqualTo(info.get("PolarisConfigProperties"));
assertThat(Collections.singletonList(polarisPropertySource)).isEqualTo(info.get("PolarisPropertySource"));
}
+
+ @Test
+ public void testPolarisConfigEndpointExposesClientId() {
+ SDKContext sdkContext = Mockito.mock(SDKContext.class);
+ ValueContext valueContext = Mockito.mock(ValueContext.class);
+ Mockito.when(sdkContext.getValueContext()).thenReturn(valueContext);
+ Mockito.when(valueContext.getClientId()).thenReturn("host_1234_0");
+ PolarisConfigSDKContextManager.setConfigSDKContext(sdkContext);
+
+ PolarisConfigEndpoint endpoint = new PolarisConfigEndpoint(polarisConfigProperties);
+
+ assertThat(endpoint.polarisConfig().get("ClientId")).isEqualTo("host_1234_0");
+ }
+
+ @Test
+ public void testPolarisConfigEndpointClientIdIsNullWhenContextAbsent() {
+ PolarisConfigEndpoint endpoint = new PolarisConfigEndpoint(polarisConfigProperties);
+
+ assertThat(endpoint.polarisConfig().get("ClientId")).isNull();
+ }
}