Post-根据前缀处理 Spring 中的 YAML 属性以从 REST 服务检索 属性

Post-processing YAML properties in Spring based on prefix to retrieve property from REST service

我有一个 Spring 启动配置 YAML,类似

spring:
  application:
    name: my-app
a: a literal
b: <<external due to special first and last chars>>

我想做的是添加某种解析器,它将检测 b 的值是否为 <<X>> 的形式,并将触发从外部 rest 检索该值api 在 YAML 传递给运行时保存配置的 bean 之前在内存中覆盖 YAML 中的值

我尝试使用 EnvironmentPostProcessor 但失败了,因为我无法获得实际的 属性 值,只有 属性 sources,所以我无法 post- 处理这些值。

目前对我有用的是 @Configuration bean,它包含字段 ab,在设置器中实现一些东西来检测 spring 正在尝试设置以 << 开始并以 >> 结束,如果是这样,用我从其余 api 检索到的版本覆盖加载到 pojo 中的内容。这并不理想,因为我最终得到了很多重复

在 Spring 5 中实现类似功能的正确方法是什么?我知道 spring 属性支持使用语法 ${a} 对其他属性的引用,因此必须有一些机制已经允许添加自定义占位符解析器

不知道正确的方法,但是从 REST 调用获取属性的一种方法是实现您自己的 PropertySource,它获取(和缓存?)具体命名的属性的值。

这是我使用 Spring Boot 2.1.5 想出的一个 hacky 解决方案。 可能最好使用自定义 PropertyResolver

基本上它是这样的:

  1. 抓住我关心的PropertySource。对于这种情况,它是 application.properties。应用程序可以有 N 个来源,因此如果还有其他地方可能会出现 << >>,那么您也需要检查它们。
  2. 循环遍历 << >>
  3. 的源值
  4. 如果匹配则动态替换该值。

我的属性是:

a=hello from a
b=<<I need special attention>>

我被黑的 ApplicationListener 是:

import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.web.client.RestTemplate;

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

public class EnvironmentPrepareListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {

    private final RestTemplate restTemplate = new RestTemplate();

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        // Only focused main application.properties (or yml) configuration
        // Loop through sources to figure out name
        final String propertySourceName = "applicationConfig: [classpath:/application.properties]";
        PropertySource<?> propertySource = event.getEnvironment().getPropertySources()
                .get(propertySourceName);

        Map<String, Object> source = ((OriginTrackedMapPropertySource) propertySource).getSource();
        Map<String, Object> myUpdatedProps = new HashMap<>();
        final String url = "https://jsonplaceholder.typicode.com/todos/1";

        for (Map.Entry<String, Object> entry : source.entrySet()) {
            if (isDynamic(entry.getValue())) {
                String updatedValue = restTemplate.getForEntity(url, String.class).getBody();
                myUpdatedProps.put(entry.getKey(), updatedValue);
            }
        }

        if (!myUpdatedProps.isEmpty()) {
            event.getEnvironment().getPropertySources()
                    .addBefore(
                            propertySourceName,
                            new MapPropertySource("myUpdatedProps", myUpdatedProps)
                    );
        }
    }

    private boolean isDynamic(Object value) {
        return StringUtils.startsWith(value.toString(), "<<")
                && StringUtils.endsWith(value.toString(), ">>");
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

点击 /test 会得到我:

{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }

我最后做了一些改动以标记特殊属性。然后我创建了自己的 PropertySource ,就像@Andreas 建议的那样。全部灵感来自 org.springframework.boot.env.RandomValuePropertySource

诀窍是将特殊字符 <<>> 更改为 spring 已经使用的语法:${},但就像使用 ${random.int} 我做了类似 ${rest.XXX} 的事情。我之前不知道的是,通过这样做,Spring 将使用来自占位符值的新 属性 名称第二次调用所有 属性 源(rest.XXX 在我之前的例子中)。如果 属性 的名称以我的前缀 rest.

开头,那么在 属性 源中我可以处理该值

这是我的解决方案的简化版本

public class MyPropertySource extends PropertySource<RestTemplate> {
  private static final String PREFIX = "rest.";

  public MyPropertySource() {
    super(MyPropertySource.class.getSimpleName());
  }

  @Override
  public Object getProperty(@Nonnull String name) {
    String result = null;
    if (name.startsWith(PREFIX)) {
        result = getValueFromRest(name.substring(PREFIX.length()));
    }

    return result;
  }
}

最后,为了注册 属性 来源,我使用 EnvironmentPostProcessor 作为 described here。我找不到不需要维护新文件的更简单方法 META-INF/spring.factories