Spring Boot下多个配置文件,对相同配置项(List)合并方案

1,773 阅读2分钟

需求点

在Spring Boot下是支持多个配置文件的,只需import即可

spring:
  config:
    import:
      - classpath:/a.yml
      - classpath:/b.yml

但是Spring并不会把这些文件里面相同配置项的List合并,而是替换,源码参阅:org.springframework.core.env.MutablePropertySources#addFirst

举个栗子:

a.yml

spring:
  cloud:
    gateway:
      routes:
        - id: id1
          uri: http://127.0.0.1:8724/admin/categ/test1
          predicates:
            - Path=/admin/categ/test1
        - id: id2
          uri: http://127.0.0.1:8724/admin/categ/test2
          predicates:
            - Path=/admin/categ/test2

b.yml

spring:
  cloud:
    gateway:
      routes:
        - id: id3
          uri: http://127.0.0.1:8724/admin/categ/test3
          predicates:
            - Path=/admin/categ/test3

routes里面的配置项默认a.yml 会被 b.yml替换掉,而不是合并

思路

  1. 不使用自带的import的方式导入配置文件,而是继承 EnvironmentPostProcessor 动态添加配置
  2. 手动将多个文件的配置项合并成一个PropertySource

在下面的源码打断点就可以知道,一个文件就是一个PropertySource,而里面就是这样的内容:

"spring.cloud.gateway.routes[0].id" -> {OriginTrackedValue$OriginTrackedCharSequence@2910} "id1"
"spring.cloud.gateway.routes[0].uri" -> {OriginTrackedValue$OriginTrackedCharSequence@2912} "http://127.0.0.1:8724/admin/categ/test1"
"spring.cloud.gateway.routes[0].predicates[0]" -> {OriginTrackedValue$OriginTrackedCharSequence@2914} "Path=/admin/categ/test1"
"spring.cloud.gateway.routes[1].id" -> {OriginTrackedValue$OriginTrackedCharSequence@2916} "id2"
"spring.cloud.gateway.routes[1].uri" -> {OriginTrackedValue$OriginTrackedCharSequence@2918} "http://127.0.0.1:8724/admin/categ/test2"
"spring.cloud.gateway.routes[1].predicates[0]" -> {OriginTrackedValue$OriginTrackedCharSequence@2920} "Path=/admin/categ/test2"

也就是说,每一个配置文件中的配置项都是从0开始, 第一个文件:

spring.cloud.gateway.routes[0].id
spring.cloud.gateway.routes[1].id

第二个文件又是从0开始

spring.cloud.gateway.routes[0].id

合并后应该得这样,才不会被替换掉:

spring.cloud.gateway.routes[0].id
spring.cloud.gateway.routes[1].id
spring.cloud.gateway.routes[2].id

实现

/**
 * @author heys1
 * @date 2022/5/25
 */
public class CustomEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
    /**
     * 根据正则,获取配置项序号
     *
     * @see CustomEnvironmentPostProcessor#getPropNum(java.lang.String)
     */
    private String REGULAR = "spring.cloud.gateway.routes\[(.+)\]\.(.+)$";
    /**
     * 存放yaml文件名
     */
    private String[] profiles = {
            "config/a.yml",
            "config/b.yml",
    };


    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        //这里可以取运行环境,自行拓展配置文件的文件名
        String[] activeProfiles = environment.getActiveProfiles();
        //以配置文件进行分组
        List<Map<String, List<Map.Entry<String, OriginTrackedValue>>>> propGroupList = new ArrayList<>();
        //遍历profiles
        for (String profile : profiles) {
            //加载配置文件,
            Resource resource = new ClassPathResource(profile);
            PropertySource<?> propertySource = loadProfiles(resource);
            //记录所有配置项
            Map<String, OriginTrackedValue> sourceMap = (Map<String, OriginTrackedValue>) propertySource.getSource();
            //根据序号分组
            Map<String, List<Map.Entry<String, OriginTrackedValue>>> group = sourceMap
                    .entrySet()
                    .stream()
                    .filter(e -> this.isMatch(e.getKey()))
                    .collect(Collectors.groupingBy(s -> String.valueOf(getPropNum(s.getKey()))));
            propGroupList.add(group);
        }
        //添加到Spring 配置上下文
        this.merge(environment, propGroupList);
    }


    /**
     * 将所有配置文件的配置项合并为一个 PropertySource,并添加到Spring 配置上下文
     *
     * @param groupList PropertySource 分组
     */
    public void merge(ConfigurableEnvironment environment, List<Map<String, List<Map.Entry<String, OriginTrackedValue>>>> groupList) {
        Map<String, OriginTrackedValue> routesMap = new TreeMap<>();
        int i = 0;
        for (Map<String, List<Map.Entry<String, OriginTrackedValue>>> groupItem : groupList) {
            for (Map.Entry<String, List<Map.Entry<String, OriginTrackedValue>>> routeItem : groupItem.entrySet()) {
                for (Map.Entry<String, OriginTrackedValue> routePropItem : routeItem.getValue()) {
                    int itemSort = getPropNum(routePropItem.getKey());
                    String newKey = routePropItem.getKey().replaceFirst(Integer.toString(itemSort), Integer.toString(i));
                    routesMap.put(newKey, routePropItem.getValue());
                }
                i++;
            }
        }

        PropertySource<Map<String, OriginTrackedValue>> propertySource =
                new PropertySource<Map<String, OriginTrackedValue>>(this.getClass().getName(), routesMap) {
                    @Override
                    public OriginTrackedValue getProperty(String s) {
                        return routesMap.get(s);
                    }
                };
        environment.getPropertySources().addFirst(propertySource);
    }

    /**
     * 通过正则获取当前route配置字符串的序号
     *
     * @param key spring.cloud.gateway.routes[n].xxx
     * @return n
     */
    private int getPropNum(String key) {
        Pattern r = Pattern.compile(REGULAR);
        Matcher m = r.matcher(key);
        if (m.find()) {
            return Integer.parseInt(m.group(1));
        }
        throw new RuntimeException("配置格式有问题");
    }


    /**
     * 目标配置项是否符合匹配要求
     */
    private boolean isMatch(String key) {
        try {
            getPropNum(key);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 获取配置项
     *
     * @param resource
     * @return
     */
    @SneakyThrows
    private PropertySource<?> loadProfiles(Resource resource) {
        if (Objects.requireNonNull(resource.getFilename()).contains("yml")) {
            YamlPropertySourceLoader sourceLoader = new YamlPropertySourceLoader();
            List<PropertySource<?>> propertySources = sourceLoader.load(resource.getFilename(), resource);
            return propertySources.get(0);
        }
        Properties properties = new Properties();
        properties.load(resource.getInputStream());
        return new PropertiesPropertySource(resource.getFilename(), properties);
    }

    @Override
    public int getOrder() {
        return 0;
    }

}

src/main/resources/META-INF/spring.factories 添加配置:

org.springframework.boot.env.EnvironmentPostProcessor={你的项目路径}.CustomEnvironmentPostProcessor