Wiosna: dostęp do wszystkich właściwości środowiska jako obiekt mapy lub właściwości

86

Używam adnotacji, aby skonfigurować moje środowisko wiosenne w następujący sposób:

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

Prowadzi to do tego, że moje właściwości default.propertiessą częścią Environment. Chcę @PropertySourcetutaj użyć mechanizmu, ponieważ zapewnia on już możliwość przeciążania właściwości przez kilka warstw rezerwowych i różne dynamiczne lokalizacje, w oparciu o ustawienia środowiska (np. Lokalizacja config_dir). Po prostu usunąłem rezerwę, aby ułatwić przykład.

Jednak moim problemem jest teraz to, że chcę skonfigurować na przykład właściwości mojego źródła danych w default.properties. Możesz przekazać ustawienia do źródła danych, nie wiedząc szczegółowo, jakich ustawień oczekuje źródło danych

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

Problem polega jednak na tym, że Environmentprzedmiot nie jest ani Propertiesprzedmiotem, Mapani niczym porównywalnym. Z mojego punktu widzenia jest to po prostu niemożliwe, aby uzyskać dostęp do wszystkich walorów środowiska, ponieważ nie ma keySetani iteratormetoda lub coś porównywalne.

Properties p <=== Environment env?

Czy coś mi brakuje? Czy można w Environmentjakiś sposób uzyskać dostęp do wszystkich wpisów obiektu? Jeśli tak, mógłbym zmapować wpisy do obiektu Maplub Propertiesobiektu, mógłbym nawet filtrować lub mapować je według prefiksu - tworzyć podzbiory jako standardowe java Map... To właśnie chciałbym zrobić. Jakieś sugestie?

RoK
źródło

Odpowiedzi:

75

Potrzebujesz czegoś takiego, może można to poprawić. To jest pierwsza próba:

...
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());
            }
        }
        ...
    }
...

Zasadniczo do wszystkiego ze środowiska, które jest a MapPropertySource(a jest sporo implementacji), można uzyskać dostęp jako Mapwłaściwości.

Andrei Stefan
źródło
Dzięki za podzielenie się tym podejściem. Uważam to za trochę "brudne", ale to chyba jedyny sposób, aby tu dotrzeć. Innym podejściem, które pokazał mi kolega, byłoby umieszczenie właściwości w konfiguracji za pomocą stałego klucza, który zawiera listę wszystkich kluczy właściwości. Następnie możesz wczytać właściwości do obiektu Map / Properties na podstawie listy kluczy. To przynajmniej uchroniłoby obsadę ...
RoK
20
Uwaga dotycząca rozruchu sprężynowego ... to getPropertySources () zwraca PropertySource w kolejności pierwszeństwa, więc musisz skutecznie odwrócić to w przypadkach, gdy wartości właściwości są nadpisywane
Rob Bygrave
2
Jak wspomniał @RobBygrave, kolejność może być inna, ale zamiast odwracać kolejność (ponieważ możesz wdrożyć sprężynowy rozruch do kontenera, gdy wojna lub to zachowanie może się zmienić w przyszłości), po prostu zebrałbym wszystkie klucze, a następnie applicationContext.getEnvironment().getProperty(key)użyłbym ich do rozwiązania
ziemniak
@potato To dobry pomysł i próbowałem tego. Jedynym potencjalnym problemem jest to, że napotkasz problemy z oceną ze
znakami
1
Dziękuję! .. Szukałem sprężynowej alternatywy do użycia zamiast org.apache.ibatis.io.Resources.getResourceAsProperties („Filepath”) To rozwiązanie bardzo mi się sprawdziło.
so-random-dude
69

To stare pytanie, ale przyjęta odpowiedź ma poważną wadę. Jeśli Environmentobiekt Spring zawiera jakiekolwiek wartości nadpisujące (zgodnie z opisem w konfiguracji zewnętrznej ), nie ma gwarancji, że mapa wartości właściwości, które generuje, będzie pasować do zwracanych z Environmentobiektu. Zauważyłem, że zwykłe iterowanie przez PropertySources z w Environmentrzeczywistości nie daje żadnych nadrzędnych wartości. Zamiast tego wytworzył pierwotną wartość, taką, która powinna była zostać nadpisana.

Oto lepsze rozwiązanie. Używa EnumerablePropertySources of the Environmentdo iteracji przez znane nazwy właściwości, ale następnie odczytuje rzeczywistą wartość z rzeczywistego środowiska Spring. Gwarantuje to, że wartość jest faktycznie ustalona przez Spring, w tym wszelkie wartości nadrzędne.

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)));
pedorro
źródło
1
Warto zauważyć, że od wersji Spring 4.1.2 to rozwiązanie (w przeciwieństwie do innych odpowiedzi) nie musi być aktualizowane, aby jawnie obsługiwać CompositePropertySource, ponieważ CompositePropertySource rozszerza EnumerablePropertySource, a zatem getPropertyNames zwróci zestaw wszystkich nazw właściwości w złożeniu źródło.
M. Justin
5
Można również zbierać właściwości przy użyciu wbudowanej collectmetody na potoku zamiast robi forEach: .distinct().collect(Collectors.toMap(Function.identity(), springEnv::getProperty)). Jeśli potrzebujesz zebrać je do Właściwości zamiast mapy, możesz użyć czteroargumentowej wersji collect.
M. Justin
2
Co springEnv? Skąd to pochodzi? Czy różni się od envprzyjętego rozwiązania?
sebnukem
2
@sebnukem Słuszna uwaga. springEnvjest envprzedmiotem pierwotnego pytania i przyjętego rozwiązania. Przypuszczam, że powinienem zachować tę samą nazwę.
pedorro
3
Możesz użyć ConfigurableEnvironment i nie musisz robić obsady.
Abhijit Sarkar
19

Miałem wymóg, aby pobrać wszystkie właściwości, których klucz zaczyna się od wyraźnego prefiksu (np. Wszystkie właściwości zaczynające się od „log4j.appender.”) I napisałem następujący kod (używając strumieni i lamdas 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() );
    }
}

Zauważ, że punktem początkowym jest ConfigurableEnvironment, który jest w stanie zwrócić osadzone PropertySources (ConfigurableEnvironment jest bezpośrednim potomkiem Environment). Możesz go automatycznie podłączyć:

@Autowired
private ConfigurableEnvironment  myEnv;

Jeśli nie używasz bardzo specjalnych rodzajów źródeł właściwości (takich jak JndiPropertySource, który zwykle nie jest używany w wiosennej automatycznej konfiguracji), możesz pobrać wszystkie właściwości przechowywane w środowisku.

Implementacja opiera się na kolejności iteracji, która sama zapewnia sprężynę i przyjmuje pierwszą znalezioną właściwość, wszystkie później znalezione właściwości o tej samej nazwie są odrzucane. Powinno to zapewnić takie samo zachowanie, jak gdyby środowisko zostało bezpośrednio poproszone o właściwość (zwrócenie pierwszej znalezionej).

Zauważ również, że zwrócone właściwości nie są jeszcze rozwiązane, jeśli zawierają aliasy z operatorem $ {...}. Jeśli chcesz rozwiązać konkretny klucz, musisz ponownie zapytać bezpośrednio Środowisko:

myEnv.getProperty( key );
Heri
źródło
1
Dlaczego po prostu nie odkryć w ten sposób wszystkich kluczy, a następnie użyć environment.getProperty do wymuszenia odpowiedniego rozwiązania wartości? Chciałbym mieć pewność, że nadpisania środowiska są przestrzegane, np. Application-dev.properties przesłania domyślną wartość w application.properties i, jak wspomniałeś, zastępcze eval.
GameSalutes,
To właśnie wskazałem w ostatnim akapicie. Używanie env.getProperty zapewnia oryginalne zachowanie Springa.
Heri,
Jak testujesz to jednostkowo? Zawsze dostaję NullPointerExceptionw moich testach jednostkowych, gdy próbuje uzyskać @Autowiredwystąpienie ConfigurationEnvironment.
ArtOfWarfare
Czy na pewno uruchamiasz swój test jako aplikację wiosenną?
Heri
Robię to tak:
Heri
10

Pierwotne pytanie sugerowało, że fajnie byłoby móc filtrować wszystkie właściwości na podstawie przedrostka. Właśnie potwierdziłem, że działa to od wersji Spring Boot 2.1.1.RELEASE dla Properties lub Map<String,String> . Jestem pewien, że teraz to działało. Co ciekawe, bez prefix =kwalifikacji nie działa , czyli nie wiem jak załadować całe środowisko do mapy. Jak powiedziałem, może to być coś, od czego OP chciał zacząć. Przedrostek i następujący po nim „.” zostanie pozbawiony, co może, ale nie musi być tym, czego się chce:

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

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

Postscriptum: Rzeczywiście jest możliwe i haniebnie łatwe, aby uzyskać całe środowisko. Nie wiem, jak mi to umknęło:

@ConfigurationProperties
@Bean
public Properties getProperties() {
    return new Properties();
}
AbuNassar
źródło
1
ponadto właściwości takie jak abc = x są zagnieżdżane w {b = {c = x}}
weberjn
Żadna część tego nie zadziałała - getAsProperties()zawsze zwraca pustą Propertiesinstancję, a wypróbowanie jej bez określonego przedrostka nie pozwala nawet na kompilację. To jest z Spring Boot 2.1.6. WYDANIE
ArtOfWarfare,
1
Nie piszę Javy w pracy, ale szybko to załapałem : github.com/AbuCarlo/SpringPropertiesBean . Możliwe, że nie zadziała, jeśli w jakiś sposób ominiesz sekwencję startową Springa (tj. Fasola „properties” nigdy nie zostanie zapełniona). Dotyczy to środowiska Java 8, Spring 2.2.6.
AbuNassar
5

Jak na wiosenny bilet do Jira , jest to celowy projekt. Ale następujący kod działa dla mnie.

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;
}
jasonleakey
źródło
2

Wiosna nie pozwoli na odłączenie połączenia via java.util.Propertiesod Spring Environment.

Ale Properties.load()nadal działa w aplikacji rozruchowej Spring:

Properties p = new Properties();
try (InputStream is = getClass().getResourceAsStream("/my.properties")) {
    p.load(is);
}
weberjn
źródło
1

Inne odpowiedzi wskazywały na rozwiązanie większości spraw z udziałem PropertySources , ale żadna z nich nie wspomniała, że ​​pewnych źródeł własności nie można podzielić na użyteczne typy.

Jednym z takich przykładów jest źródło właściwości dla argumentów wiersza poleceń. Używana jest klasa SimpleCommandLinePropertySource. Ta klasa prywatna jest zwracana przez publiczną metodę , przez co dostęp do danych wewnątrz obiektu jest niezwykle trudny. Musiałem użyć odbicia, aby odczytać dane i ostatecznie zastąpić źródło właściwości.

Jeśli ktoś ma lepsze rozwiązanie, naprawdę chciałbym je zobaczyć; jest to jednak jedyny hack, jaki mam do roboty.

Chad Van De Hey
źródło
Czy znalazłeś rozwiązanie problemu z zajęciami niepublicznymi?
Tobias
1

Pracując ze Spring Boot 2, musiałem zrobić coś podobnego. Większość powyższych odpowiedzi działa dobrze, ale pamiętaj, że na różnych etapach cyklu życia aplikacji wyniki będą różne.

Na przykład po ApplicationEnvironmentPreparedEventżadnej właściwości w środku application.propertiesnie ma. Jednak po ApplicationPreparedEventwydarzeniu są.

Mikrofon
źródło
1

W przypadku Spring Boot zaakceptowana odpowiedź nadpisze zduplikowane właściwości tymi o niższym priorytecie . To rozwiązanie zbierze właściwości do SortedMapi przyjmie tylko zduplikowane właściwości o najwyższym priorytecie.

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);
}
Jeff Brower
źródło
env.getPropertySources () podaje właściwości od najniższego do najwyższego priorytetu?
Faraz,
Jest odwrotnie. Są sortowane od wysokiego -> niskiego priorytetu.
Samuel Tatipamula
0

Pomyślałem, że dodam jeszcze jeden sposób. W moim przypadku dostarczam to, do com.hazelcast.config.XmlConfigBuilderktórego wystarczy java.util.Propertiesrozwiązać niektóre właściwości w pliku konfiguracyjnym Hazelcast XML, tj. Wywołuje tylko getProperty(String)metodę. To pozwoliło mi zrobić to, czego potrzebowałem:

@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 Skończyło się na tym, że nie używałem tego specjalnie dla Hazelcast, ponieważ rozwiązuje tylko właściwości pliku XML, ale nie w czasie wykonywania. Ponieważ używam również Spring, zdecydowałem się na niestandardowy org.springframework.cache.interceptor.AbstractCacheResolver#getCacheNames. To rozwiązuje właściwości w obu sytuacjach, przynajmniej jeśli używasz właściwości w nazwach pamięci podręcznej.

Sam
źródło