Spring: доступ ко всем свойствам среды как к объекту Map или Properties

86

Я использую аннотации для настройки моей среды Spring следующим образом:

@Configuration
...
@PropertySource("classpath:/config/default.properties")
...
public class GeneralApplicationConfiguration implements WebApplicationInitializer 
{
    @Autowired
    Environment env;
}

Это приводит к тому, что мои свойства default.propertiesявляются частью Environment. Я хочу использовать этот @PropertySourceмеханизм здесь, потому что он уже предоставляет возможность перегрузки свойств через несколько резервных уровней и различные динамические местоположения, в зависимости от настроек среды (например, расположение config_dir). Я просто убрал запасной вариант, чтобы упростить пример.

Однако сейчас моя проблема в том, что я хочу настроить, например, свои свойства источника данных в default.properties. Вы можете передать настройки источнику данных, не зная подробно, какие настройки ожидает источник данных, используя

Properties p = ...
datasource.setProperties(p);

Однако проблема в том, что Environmentобъект не является ни Propertiesобъектом, Mapни чем-либо сопоставимым. С моей точки зрения, просто невозможно получить доступ ко всем значениям среды, потому что нет keySetни iteratorметода, ни чего-либо сопоставимого.

Properties p <=== Environment env?

Я что-то упускаю? Можно ли Environmentкаким-то образом получить доступ ко всем записям объекта? Если да, я мог бы сопоставить записи с объектом Mapили Properties, я мог бы даже отфильтровать или сопоставить их по префиксу - создать подмножества как стандартную java Map... Это то, что я хотел бы сделать. Какие-либо предложения?

РК
источник

Ответы:

75

Вам нужно что-то подобное, может, можно улучшить. Это первая попытка:

...
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
...

@Configuration
...
@org.springframework.context.annotation.PropertySource("classpath:/config/default.properties")
...
public class GeneralApplicationConfiguration implements WebApplicationInitializer 
{
    @Autowired
    Environment env;

    public void someMethod() {
        ...
        Map<String, Object> map = new HashMap();
        for(Iterator it = ((AbstractEnvironment) env).getPropertySources().iterator(); it.hasNext(); ) {
            PropertySource propertySource = (PropertySource) it.next();
            if (propertySource instanceof MapPropertySource) {
                map.putAll(((MapPropertySource) propertySource).getSource());
            }
        }
        ...
    }
...

По сути, все, что есть в Environment MapPropertySource(а существует довольно много реализаций), может быть доступно как Mapиз свойств.

Андрей Стефан
источник
Спасибо, что поделились этим подходом. Я считаю это немного "грязным", но, вероятно, это единственный способ пойти сюда. Другой подход, который показал мне коллега, - это добавить свойство в конфигурацию с использованием фиксированного ключа, который содержит список со всеми ключами свойств. Затем вы можете прочитать свойства в объекте Map / Properties на основе списка ключей. Это, по крайней мере, предотвратило бы забросы ...
РК
20
Примечание для загрузки Spring ... что getPropertySources () возвращает PropertySource в порядке приоритета, поэтому вам необходимо отменить это в тех случаях, когда значения свойств перезаписываются
Роб Бигрейв,
2
Как упоминал @RobBygrave, порядок может быть другим, но вместо его отмены (поскольку вы можете развернуть весеннюю загрузку в контейнер, поскольку война, или это поведение может измениться в будущем), я бы просто собирал все ключи, а затем использовал их applicationContext.getEnvironment().getProperty(key)для их разрешения
potato
@potato Это хорошая идея, и я попробовал. Единственная потенциальная проблема заключается в том, что вы сталкиваетесь с проблемами оценки с заполнителями, как в этом вопросе здесь: stackoverflow.com/questions/34584498/…
bischoje
1
Спасибо! .. Я искал весеннюю альтернативу для использования вместо org.apache.ibatis.io.Resources.getResourceAsProperties ("Filepath"). Это решение мне очень понравилось.
so-random-dude
69

Это старый вопрос, но принятый ответ имеет серьезный недостаток. Если Environmentобъект Spring содержит какие-либо замещающие значения (как описано в разделе « Внешняя конфигурация» ), нет гарантии, что созданная им карта значений свойств будет соответствовать значениям, возвращаемым из Environmentобъекта. Я обнаружил, что просто перебираяPropertySource s изEnvironment действительности не дает никаких переопределяющих значений. Вместо этого было получено исходное значение, то есть то, которое следовало изменить.

Вот лучшее решение. Он использует EnumerablePropertySources Environmentдля перебора известных имен свойств, но затем считывает фактическое значение из реальной среды Spring. Это гарантирует, что это значение действительно разрешено Spring, включая любые замещающие значения.

Properties props = new Properties();
MutablePropertySources propSrcs = ((AbstractEnvironment) springEnv).getPropertySources();
StreamSupport.stream(propSrcs.spliterator(), false)
        .filter(ps -> ps instanceof EnumerablePropertySource)
        .map(ps -> ((EnumerablePropertySource) ps).getPropertyNames())
        .flatMap(Arrays::<String>stream)
        .forEach(propName -> props.setProperty(propName, springEnv.getProperty(propName)));
педорро
источник
1
Стоит отметить, что начиная с Spring 4.1.2 это решение (в отличие от других ответов) не нужно обновлять, чтобы явно иметь дело с CompositePropertySource, поскольку CompositePropertySource расширяет EnumerablePropertySource, и поэтому getPropertyNames вернет набор всех имен свойств в составном источник.
M. Justin
5
Можно также собрать свойства , используя встроенный collectметод на потоке вместо того , чтобы делать forEach: .distinct().collect(Collectors.toMap(Function.identity(), springEnv::getProperty)). Если вам нужно было собрать его в Properties вместо Map, вы можете использовать версию с четырьмя аргументами collect.
M. Justin
2
Что springEnv? Откуда это взялось? Отличается ли оно от envпринятого решения?
sebnukem
2
@sebnukem Хорошее замечание. springEnvявляется envпредметом исходного вопроса и принятого решения. Полагаю, мне следовало оставить имя без изменений.
pedorro
3
Вы можете использовать ConfigurableEnvironment и не должны делать гипс.
Abhijit Sarkar
19

У меня было требование получить все свойства, ключ которых начинается с отдельного префикса (например, все свойства, начинающиеся с «log4j.appender.»), И я написал следующий код (используя потоки и ламды Java 8).

public static Map<String,Object> getPropertiesStartingWith( ConfigurableEnvironment aEnv,
                                                            String aKeyPrefix )
{
    Map<String,Object> result = new HashMap<>();

    Map<String,Object> map = getAllProperties( aEnv );

    for (Entry<String, Object> entry : map.entrySet())
    {
        String key = entry.getKey();

        if ( key.startsWith( aKeyPrefix ) )
        {
            result.put( key, entry.getValue() );
        }
    }

    return result;
}

public static Map<String,Object> getAllProperties( ConfigurableEnvironment aEnv )
{
    Map<String,Object> result = new HashMap<>();
    aEnv.getPropertySources().forEach( ps -> addAll( result, getAllProperties( ps ) ) );
    return result;
}

public static Map<String,Object> getAllProperties( PropertySource<?> aPropSource )
{
    Map<String,Object> result = new HashMap<>();

    if ( aPropSource instanceof CompositePropertySource)
    {
        CompositePropertySource cps = (CompositePropertySource) aPropSource;
        cps.getPropertySources().forEach( ps -> addAll( result, getAllProperties( ps ) ) );
        return result;
    }

    if ( aPropSource instanceof EnumerablePropertySource<?> )
    {
        EnumerablePropertySource<?> ps = (EnumerablePropertySource<?>) aPropSource;
        Arrays.asList( ps.getPropertyNames() ).forEach( key -> result.put( key, ps.getProperty( key ) ) );
        return result;
    }

    // note: Most descendants of PropertySource are EnumerablePropertySource. There are some
    // few others like JndiPropertySource or StubPropertySource
    myLog.debug( "Given PropertySource is instanceof " + aPropSource.getClass().getName()
                 + " and cannot be iterated" );

    return result;

}

private static void addAll( Map<String, Object> aBase, Map<String, Object> aToBeAdded )
{
    for (Entry<String, Object> entry : aToBeAdded.entrySet())
    {
        if ( aBase.containsKey( entry.getKey() ) )
        {
            continue;
        }

        aBase.put( entry.getKey(), entry.getValue() );
    }
}

Обратите внимание, что отправной точкой является ConfigurableEnvironment, которая может возвращать встроенные PropertySources (ConfigurableEnvironment является прямым потомком Environment). Вы можете выполнить автоматическое подключение:

@Autowired
private ConfigurableEnvironment  myEnv;

Если вы не используете особые типы источников свойств (например, JndiPropertySource, который обычно не используется в автоконфигурации Spring), вы можете получить все свойства, хранящиеся в среде.

Реализация зависит от порядка итераций, который предоставляет сама пружина, и принимает первое найденное свойство, все последующие найденные свойства с тем же именем отбрасываются. Это должно гарантировать такое же поведение, как если бы окружение запрашивало свойство напрямую (возвращая первое найденное свойство).

Также обратите внимание, что возвращенные свойства еще не разрешены, если они содержат псевдонимы с оператором $ {...}. Если вы хотите разрешить определенный ключ, вам нужно снова напрямую спросить Environment:

myEnv.getProperty( key );
Хери
источник
1
Почему бы просто не обнаружить все ключи таким образом, а затем использовать environment.getProperty для принудительного разрешения соответствующего значения? Хотелось бы обеспечить соблюдение переопределений среды, например, application-dev.properties переопределяет значение по умолчанию в application.properties и, как вы упомянули, заполнитель eval.
GameSalutes
Это то, что я указал в последнем абзаце. Использование env.getProperty обеспечивает исходное поведение Spring.
Heri
Как вы это тестируете? Я всегда получаю NullPointerExceptionв своих модульных тестах, когда он пытается получить @Autowiredэкземпляр ConfigurationEnvironment.
ArtOfWarfare
Вы уверены, что запускаете свой тест как весеннее приложение?
Хери
Я делаю это так:
Heri
10

Исходный вопрос намекал, что было бы неплохо иметь возможность фильтровать все свойства на основе префикса. Я только что подтвердил, что это работает с Spring Boot 2.1.1.RELEASE для Properties или Map<String,String> . Я уверен, что пока это работает. Интересно, что это не работает без prefix =квалификации, т.е. я не знаю, как загрузить все окружение на карту. Как я уже сказал, это могло быть именно то, с чего OP хотел начать. Префикс и следующий за ним "." будет снято, что может быть, а может и не быть тем, что нужно:

@ConfigurationProperties(prefix = "abc")
@Bean
public Properties getAsProperties() {
    return new Properties();
}

@Bean
public MyService createService() {
    Properties properties = getAsProperties();
    return new MyService(properties);
}

Постскриптум: действительно возможно и позорно легко получить всю окружающую среду. Не знаю, как это от меня сбежало:

@ConfigurationProperties
@Bean
public Properties getProperties() {
    return new Properties();
}
Абу-Насар
источник
1
Кроме того, такие свойства, как abc = x, вкладываются в {b = {c = x}}
weberjn
Никакая часть этого не сработала - getAsProperties()всегда возвращается пустой Propertiesэкземпляр, а попытка его без указанного префикса даже не позволяет ему скомпилировать. Это с Spring Boot 2.1.6.RELEASE
ArtOfWarfare
1
Я не пишу Java на работе, но довольно быстро придумал: github.com/AbuCarlo/SpringPropertiesBean . Возможно, это не сработает, если вы каким-то образом обойдете последовательность запуска Spring (то есть bean-компонент «свойства» никогда не заполняется). Это для Java 8, Spring 2.2.6.
AbuNassar 01
5

Как и этот весенний билет Jira , это намеренный дизайн. Но у меня работает следующий код.

public static Map<String, Object> getAllKnownProperties(Environment env) {
    Map<String, Object> rtn = new HashMap<>();
    if (env instanceof ConfigurableEnvironment) {
        for (PropertySource<?> propertySource : ((ConfigurableEnvironment) env).getPropertySources()) {
            if (propertySource instanceof EnumerablePropertySource) {
                for (String key : ((EnumerablePropertySource) propertySource).getPropertyNames()) {
                    rtn.put(key, propertySource.getProperty(key));
                }
            }
        }
    }
    return rtn;
}
Джейсонлики
источник
2

Spring не позволяет отделиться java.util.Propertiesот среды Spring.

Но Properties.load()все еще работает в приложении загрузки Spring:

Properties p = new Properties();
try (InputStream is = getClass().getResourceAsStream("/my.properties")) {
    p.load(is);
}
Weberjn
источник
1

В других ответах указано решение для большинства связанных с этим случаев PropertySources, но ни один из них не упомянул, что определенные источники свойств не могут быть преобразованы в полезные типы.

Одним из таких примеров является источник свойств для аргументов командной строки. Используемый класс SimpleCommandLinePropertySource. Этот частный класс возвращается общедоступным методом, что затрудняет доступ к данным внутри объекта. Мне пришлось использовать отражение, чтобы прочитать данные и в конечном итоге заменить источник свойства.

Если у кого-то есть лучшее решение, я бы очень хотел его увидеть; однако это единственный хитрость, с которой мне пришлось работать.

Чад Ван Де Хей
источник
Вы нашли решение проблемы с закрытым классом?
Тобиас
1

Работая с Spring Boot 2, мне нужно было сделать нечто подобное. Большинство приведенных выше ответов работают нормально, просто помните, что на разных этапах жизненного цикла приложения результаты будут разными.

Например, после a ApplicationEnvironmentPreparedEventникаких свойств внутри application.propertiesнет. Однако после ApplicationPreparedEventсобытия они есть.

Майк
источник
1

Для Spring Boot принятый ответ перезапишет повторяющиеся свойства с более низким приоритетом . Это решение будет собирать свойства в SortedMapи принимать только повторяющиеся свойства с наивысшим приоритетом.

final SortedMap<String, String> sortedMap = new TreeMap<>();
for (final PropertySource<?> propertySource : env.getPropertySources()) {
    if (!(propertySource instanceof EnumerablePropertySource))
        continue;
    for (final String name : ((EnumerablePropertySource<?>) propertySource).getPropertyNames())
        sortedMap.computeIfAbsent(name, propertySource::getProperty);
}
Джефф Брауэр
источник
env.getPropertySources () дает свойства от самого низкого до самого высокого приоритета?
Faraz
Все наоборот. Они отсортированы от высокого до низкого приоритета.
Самуэль Татипамула
0

Я решил добавить еще один способ. В моем случае я предоставляю это, com.hazelcast.config.XmlConfigBuilderкоторому нужно только java.util.Propertiesразрешить некоторые свойства внутри файла конфигурации Hazelcast XML, т.е. он вызывает толькоgetProperty(String) метод. Итак, это позволило мне сделать то, что мне было нужно:

@RequiredArgsConstructor
public class SpringReadOnlyProperties extends Properties {

  private final org.springframework.core.env.Environment delegate;

  @Override
  public String getProperty(String key) {
    return delegate.getProperty(key);
  }

  @Override
  public String getProperty(String key, String defaultValue) {
    return delegate.getProperty(key, defaultValue);
  }

  @Override
  public synchronized String toString() {
    return getClass().getName() + "{" + delegate + "}";
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    if (!super.equals(o)) return false;
    SpringReadOnlyProperties that = (SpringReadOnlyProperties) o;
    return delegate.equals(that.delegate);
  }

  @Override
  public int hashCode() {
    return Objects.hash(super.hashCode(), delegate);
  }

  private void throwException() {
    throw new RuntimeException("This method is not supported");
  }

  //all methods below throw the exception

  * override all methods *
}

PS Я закончил тем, что не использовал это специально для Hazelcast, потому что он разрешает только свойства для файла XML, но не во время выполнения. Так как я тоже использую Spring, я решил использовать custom org.springframework.cache.interceptor.AbstractCacheResolver#getCacheNames. Это разрешает свойства для обеих ситуаций, по крайней мере, если вы используете свойства в именах кешей.

Сэм
источник