Постобработка свойств YAML в Spring на основе префикса

У меня есть конфигурация загрузки Spring YAML с чем-то вроде

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

Что я пытаюсь сделать, так это добавить какой-то преобразователь, который обнаружит, что значение b имеет форму <<X>>, и вызовет извлечение этого значения из внешнего API-интерфейса остатка, чтобы перезаписать в памяти значение, которое было в YAML до он передается компоненту, который содержит конфигурации во время выполнения.

Я попытался использовать EnvironmentPostProcessor, но потерпел неудачу, потому что не могу получить фактические значения свойств, а только свойства источники, поэтому я не могу выполнить пост-обработку значений.

В настоящее время у меня работает компонент @Configuration, который содержит поля a и b, реализовать что-то в установщиках, чтобы определить, начинается ли значение, которое Spring пытается установить, с << и заканчивается >>, и если да, перезапишите то, что загружается в pojo с версией, которую я получаю из остальных API. Это не идеально, потому что я получаю много дублирования

Как правильно реализовать что-то подобное в Spring 5? Я знаю, что свойства spring поддерживают ссылки на другие свойства с использованием синтаксиса ${a}, поэтому должен быть какой-то механизм, который уже позволяет добавлять пользовательские преобразователи заполнителей.


person Hilikus    schedule 14.06.2019    source источник


Ответы (3)


Я закончил тем, что немного изменил вещи, чтобы отметить специальные свойства. Затем я создал свой собственный PropertySource, как предложил @Andreas. Все это было вдохновлено org.springframework.boot.env.RandomValuePropertySource

Хитрость заключалась в том, чтобы изменить специальные символы << и >> на синтаксис, который уже использовался весной: ${}, но, как и в случае со случайным преобразователем, который использует ${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 как описана здесь. Я не мог найти более простого способа, который не влечет за собой сохранение нового файла META-INF/spring.factories

person Hilikus    schedule 14.06.2019

Не знаю, как правильно, но один из способов получить свойства из вызова REST — реализовать собственный PropertySource, который получает (и кэширует?) значения специально названных свойств.

person Andreas    schedule 14.06.2019
comment
Я не думаю, что это сработает. не все свойства в файле взяты из API REST, только те, значения которых имеют специальный синтаксис. Более того, некоторые профили могут иметь одно и то же свойство с буквальным значением, в то время как другой профиль должен быть разрешен извне, все на основе синтаксиса значения. - person Hilikus; 14.06.2019
comment
не все свойства в файле взяты из REST API Итак? Существует более одного активного PropertySource, и Environment будет сканировать их до тех пор, пока свойство не будет найдено, поэтому пользовательский RestPropertySource должен давать только значения источников свойств из веб-службы REST. Вам, конечно, потребуется настроить RestPropertySource, какие свойства нужно получить и какую конечную точку REST использовать для этого. размещать ли пользовательский RestPropertySource первым, последним или где-то посередине в последовательности источников свойств, конечно, полностью зависит от вас. - person Andreas; 14.06.2019
comment
я хочу сказать, что получение его из RestPropertySource является функцией значения свойства, а не имени свойства, поэтому в RestPropertySource я не могу сказать, нужно ли его извлекать или нет . Мне понадобится обычный источник свойства yaml, чтобы сначала загрузить его, а затем я мог бы сказать, нужно ли его загружать из Rest, но похоже, что spring перестает искать источник, как только один из них находит его. я что-то упускаю? - person Hilikus; 14.06.2019
comment
Но это только в том случае, если вы застряли только на одном способе ведения дел, а не развлекаетесь идеями сделать это немного по-другому. Например. если свойство не указано явно в файле свойств, вы должны указать альтернативное свойство для получения значения. Например, вместо определения foo.bar = <<XYZ>> вы бы вместо этого определили foo.bar.rest = XYZ и разместили бы RestPropertySource последним, поэтому будет применено любое локально определенное значение foo.bar, но если запрашивается RestPropertySource, то он будет искать значение xxx.rest и получит значение из службы REST найден. - person Andreas; 15.06.2019
comment
Короче говоря, попробуйте мыслить нестандартно на секунду. - person Andreas; 15.06.2019

Вот хакерское решение, которое я придумал, используя Spring Boot 2.1.5. Вероятно, лучше использовать собственный Резолвер свойства

По сути это выглядит так:

  1. Возьмите PropertySource, который мне небезразличен. В данном случае это application.properties. Приложения могут иметь N количество источников, поэтому, если есть другие места, где может произойти << >>, вы также должны проверить их.
  2. Перебрать исходные значения для << >>
  3. Динамически заменить значение, если оно совпадает.

Мои свойства:

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 }
person Francisco Mateo    schedule 14.06.2019
comment
проблема, которую я вижу, IIUC, заключается в том, что вы запрашиваете именно свойства a и b. Это была именно моя проблема с EnvironmentPostProcessor, я не мог найти способ перебрать все разрешенные свойства в поисках тех, которые имеют форму ‹‹X››. другими словами, вы зависите от знания имени свойства, а не только значения, той же проблемы, что и решение Андреаса. - person Hilikus; 14.06.2019
comment
Я вижу, вы поняли это, но я обновил свое решение, чтобы отразить то, что вы просите. - person Francisco Mateo; 15.06.2019