You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中基于@ConfigurationProperties实现租户属性默认值继承问题

问题解决:Spring Boot租户配置继承默认值时属性覆盖问题

问题核心

你希望让tenant1、tenant2继承default租户的配置,但自定义EnvironmentPostProcessor生成的合并属性,在测试环境中被ConfigDataEnvironmentPostProcessor(由ConfigDataApplicationContextInitializer触发)覆盖,导致租户自有配置被默认值替换,断言assertEquals(5, tenant.getProperty1())失败。

解决方案

1. 调整自定义处理器的执行顺序与属性源优先级

ConfigDataEnvironmentPostProcessor的默认执行顺序为0,需让自定义处理器在它之后执行,确保能拿到完整的配置文件内容;同时将合并后的属性源添加到属性源列表最前面,保证合并后的配置优先级最高。

修正后的DefaultTenantPostProcessor代码:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.ConfigDataEnvironmentPostProcessor;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

// 设置顺序在ConfigData处理器之后
@Order(ConfigDataEnvironmentPostProcessor.ORDER + 1)
public class DefaultTenantPostProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        Map<String, Object> defaultProperties = getTenantProperties("app.tenants.default", environment);
        Set<String> tenantNames = getNonDefaultTenantNames(environment);

        Map<String, Object> mergedProperties = mergeTenantConfigs(tenantNames, defaultProperties, environment);
        // 将合并后的属性源添加到最前面,确保优先级最高
        environment.getPropertySources().addFirst(new MapPropertySource("mergedTenantConfigs", mergedProperties));
    }

    // 获取指定租户的所有配置属性
    private Map<String, Object> getTenantProperties(String tenantPrefix, ConfigurableEnvironment environment) {
        Map<String, Object> properties = new HashMap<>();
        for (org.springframework.core.env.PropertySource<?> source : environment.getPropertySources()) {
            if (source instanceof org.springframework.core.env.MapPropertySource mapSource) {
                mapSource.getSource().forEach((key, value) -> {
                    if (key.startsWith(tenantPrefix + ".")) {
                        properties.put(key, value);
                    }
                });
            }
        }
        return properties;
    }

    // 获取所有非default的租户名称
    private Set<String> getNonDefaultTenantNames(ConfigurableEnvironment environment) {
        Set<String> tenantNames = new java.util.HashSet<>();
        for (org.springframework.core.env.PropertySource<?> source : environment.getPropertySources()) {
            if (source instanceof org.springframework.core.env.MapPropertySource mapSource) {
                mapSource.getSource().keySet().forEach(key -> {
                    if (key.startsWith("app.tenants.") && !key.startsWith("app.tenants.default.")) {
                        String tenantName = key.split("\\.")[2];
                        tenantNames.add(tenantName);
                    }
                });
            }
        }
        return tenantNames;
    }

    // 合并逻辑:保留租户自有配置,仅补全缺失的默认配置
    private Map<String, Object> mergeTenantConfigs(Set<String> tenantNames, Map<String, Object> defaultProps, ConfigurableEnvironment environment) {
        Map<String, Object> merged = new HashMap<>();

        for (String tenant : tenantNames) {
            String tenantPrefix = "app.tenants." + tenant;
            Map<String, Object> tenantProps = getTenantProperties(tenantPrefix, environment);

            // 先添加租户自有配置,确保不会被默认值覆盖
            merged.putAll(tenantProps);

            // 补全租户缺失的默认配置
            defaultProps.forEach((defaultKey, defaultValue) -> {
                String targetKey = defaultKey.replace("app.tenants.default", tenantPrefix);
                if (!merged.containsKey(targetKey)) {
                    merged.put(targetKey, defaultValue);
                }
            });
        }
        return merged;
    }
}

2. 正确注册处理器

在src/main/resources/META-INF/spring.factories中添加处理器注册:

org.springframework.boot.env.EnvironmentPostProcessor=com.yourpackage.DefaultTenantPostProcessor

3. 修正测试用例拼写错误

将测试类名TenantsProeprtiesTest改为TenantsPropertiesTest,确保Autowired能正确注入:

@SpringBootTest
public class TenantsPropertiesTest {
    @Autowired
    TenantsProperties tenants;

    @Test
    void test_default_properties() {
        Tenant tenant1 = tenants.getTenants().get("tenant1");
        assertEquals(2, tenant1.getProperty2());
        assertEquals(5, tenant1.getProperty1());
        assertTrue(tenant1.getFeatures().get("feature2"));
        assertTrue(tenant1.getFeatures().get("feature1"));
    }
}

关键注意事项

  • 执行顺序:必须让自定义处理器在ConfigDataEnvironmentPostProcessor之后执行,否则无法获取完整的配置文件内容。
  • 属性源优先级:合并后的属性源要添加到最前面,保证Spring读取配置时优先使用合并后的结果,避免租户自有配置被默认值覆盖。
  • 合并逻辑:先添加租户自有配置,再补全缺失的默认配置,确保租户自定义的属性不会被覆盖。

内容的提问来源于stack exchange,提问作者Peter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 19:40:55