Frühling: Zugriff auf alle Umgebungseigenschaften als Map- oder Eigenschaftenobjekt

86

Ich verwende Anmerkungen, um meine Federumgebung wie folgt zu konfigurieren:

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

Dies führt dazu, dass meine Eigenschaften default.propertiesnicht Teil der Environment. Ich möchte den @PropertySourceMechanismus hier verwenden, da er bereits die Möglichkeit bietet, Eigenschaften über mehrere Fallback-Ebenen und verschiedene dynamische Speicherorte hinweg zu überladen, basierend auf den Umgebungseinstellungen (z. B. config_dir-Speicherort). Ich habe gerade den Fallback entfernt, um das Beispiel zu vereinfachen.

Mein Problem ist jetzt jedoch, dass ich beispielsweise meine Datenquelleneigenschaften in konfigurieren möchte default.properties. Sie können die Einstellungen an die Datenquelle übergeben, ohne genau zu wissen, welche Einstellungen die Datenquelle erwartet

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

Das Problem ist jedoch, dass das EnvironmentObjekt weder ein PropertiesObjekt noch ein Mapoder etwas Vergleichbares ist. Aus meiner Sicht ist es einfach nicht möglich, auf alle Werte der Umgebung zuzugreifen, da es keine keySetoder iteratorMethode oder etwas Vergleichbares gibt.

Properties p <=== Environment env?

Vermisse ich etwas Ist es Environmentirgendwie möglich, auf alle Einträge des Objekts zuzugreifen ? Wenn ja, könnte ich die Einträge einem Mapoder einem PropertiesObjekt zuordnen, ich könnte sie sogar nach Präfix filtern oder zuordnen - Teilmengen als Standard-Java erstellen Map... Dies ist, was ich tun möchte. Irgendwelche Vorschläge?

RoK
quelle

Antworten:

74

Sie brauchen so etwas, vielleicht kann es verbessert werden. Dies ist ein erster Versuch:

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

Grundsätzlich kann auf alles aus der Umgebung, was eine ist MapPropertySource(und es gibt ziemlich viele Implementierungen), als eine Mapder Eigenschaften zugegriffen werden.

Andrei Stefan
quelle
Vielen Dank, dass Sie diesen Ansatz geteilt haben. Ich halte das für ein bisschen "schmutzig", aber es ist wahrscheinlich der einzige Weg, hierher zu kommen. Ein anderer Ansatz, den mir ein Kollege gezeigt hat, besteht darin, eine Eigenschaft mit einem festen Schlüssel in die Konfiguration einzufügen, der eine Liste mit allen Eigenschaftsschlüsseln enthält. Sie können die Eigenschaften dann basierend auf der Keylist in ein Map / Properties-Objekt einlesen. Das würde zumindest die Casts verhindern ...
RoK
20
Hinweis für Spring Boot ..., dass getPropertySources () die PropertySource in der Rangfolge zurückgibt, sodass Sie dies in den Fällen, in denen Eigenschaftswerte überschrieben werden, effektiv umkehren müssen
Rob Bygrave
2
Wie @RobBygrave erwähnte, könnte die Reihenfolge anders sein, aber anstatt die Reihenfolge zurückzusetzen (da Sie Spring Boot als Container für den Krieg bereitstellen können oder sich dieses Verhalten in Zukunft ändern kann), würde ich einfach alle Schlüssel sammeln und sie dann applicationContext.getEnvironment().getProperty(key)auflösen
Kartoffel
@potato Das ist eine gute Idee, und das habe ich versucht. Das einzige mögliche Problem ist, dass Sie auf Bewertungsprobleme mit Platzhaltern stoßen
bischoje
1
Vielen Dank! .. Ich habe nach einer Federalternative gesucht, die anstelle von org.apache.ibatis.io.Resources.getResourceAsProperties ("Dateipfad") verwendet werden kann. Diese Lösung hat bei mir sehr gut funktioniert.
so zufälliger Typ
67

Dies ist eine alte Frage, aber die akzeptierte Antwort weist einen schwerwiegenden Fehler auf. Wenn das Spring- EnvironmentObjekt überschreibende Werte enthält (wie unter Externalisierte Konfiguration beschrieben ), kann nicht garantiert werden, dass die von ihm erzeugte Zuordnung der Eigenschaftswerte mit den vom Objekt zurückgegebenen Werten übereinstimmt Environment. Ich fand heraus, dass das einfache Durchlaufen des PropertySources des Environmentin der Tat keine übergeordneten Werte ergab. Stattdessen wurde der ursprüngliche Wert erzeugt, der überschrieben werden sollte.

Hier ist eine bessere Lösung. Dies verwendet das EnumerablePropertySources von Environment, um die bekannten Eigenschaftsnamen zu durchlaufen, liest dann aber den tatsächlichen Wert aus der realen Spring-Umgebung. Dies garantiert, dass der Wert der tatsächlich von Spring aufgelöste Wert ist, einschließlich aller überschreibenden Werte.

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
quelle
1
Es ist erwähnenswert, dass diese Lösung ab Spring 4.1.2 (im Gegensatz zu den anderen Antworten) nicht aktualisiert werden muss, um CompositePropertySource explizit zu behandeln, da CompositePropertySource EnumerablePropertySource erweitert und getPropertyNames daher die Menge aller Eigenschaftsnamen im Verbund zurückgibt Quelle.
M. Justin
5
Sie können die Eigenschaften auch mithilfe der im collectStream integrierten Methode erfassen, anstatt Folgendes auszuführen forEach: .distinct().collect(Collectors.toMap(Function.identity(), springEnv::getProperty)). Wenn Sie es in einer Eigenschaft anstelle einer Karte sammeln müssen, können Sie die Version mit vier Argumenten von verwenden collect.
M. Justin
2
Was ist springEnv? Woher kommt das? Unterscheidet es sich von envder akzeptierten Lösung?
sebnukem
2
@sebnukem Guter Punkt. springEnvist das envObjekt der ursprünglichen Frage & akzeptierte Lösung. Ich hätte den Namen wohl gleich behalten sollen.
Pedorro
3
Sie könnten ConfigurableEnvironment die Besetzung verwenden und müssen sie nicht machen.
Abhijit Sarkar
19

Ich musste alle Eigenschaften abrufen, deren Schlüssel mit einem bestimmten Präfix beginnt (z. B. alle Eigenschaften, die mit "log4j.appender" beginnen) und schrieb folgenden Code (unter Verwendung von Streams und Lamdas von 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() );
    }
}

Beachten Sie, dass der Ausgangspunkt die ConfigurableEnvironment ist, die die eingebetteten PropertySources zurückgeben kann (die ConfigurableEnvironment ist ein direkter Nachkomme von Environment). Sie können es automatisch verdrahten, indem Sie:

@Autowired
private ConfigurableEnvironment  myEnv;

Wenn Sie keine ganz besonderen Arten von Eigenschaftsquellen verwenden (wie JndiPropertySource, die normalerweise in der Frühjahrsautokonfiguration nicht verwendet wird), können Sie alle in der Umgebung enthaltenen Eigenschaften abrufen.

Die Implementierung basiert auf der Iterationsreihenfolge, die Spring selbst bereitstellt, und übernimmt die erste gefundene Eigenschaft. Alle später gefundenen Eigenschaften mit demselben Namen werden verworfen. Dies sollte das gleiche Verhalten sicherstellen, als ob die Umgebung direkt nach einer Eigenschaft gefragt würde (Rückgabe der zuerst gefundenen).

Beachten Sie auch, dass die zurückgegebenen Eigenschaften noch nicht aufgelöst sind, wenn sie Aliase mit dem Operator $ {...} enthalten. Wenn Sie einen bestimmten Schlüssel lösen möchten, müssen Sie die Umgebung erneut direkt fragen:

myEnv.getProperty( key );
Heri
quelle
1
Warum nicht einfach alle Schlüssel auf diese Weise entdecken und dann mit environment.getProperty eine angemessene Wertauflösung erzwingen? Möchten Sie sicherstellen, dass Umgebungsüberschreibungen eingehalten werden, z. B. überschreibt application-dev.properties einen Standardwert in application.properties und, wie Sie bereits erwähnt haben, die Platzhalterbewertung.
GameSalutes
Darauf habe ich im letzten Absatz hingewiesen. Die Verwendung von env.getProperty stellt das ursprüngliche Verhalten von Spring sicher.
Heri
Wie testest du das? Ich bekomme immer einen NullPointerExceptionin meinen Unit-Tests, wenn es versucht, die @AutowiredInstanz von zu bekommen ConfigurationEnvironment.
ArtOfWarfare
Sind Sie sicher, dass Sie Ihren Test als Frühjahrsanwendung durchführen?
Heri
Ich mache es so:
Heri
10

Die ursprüngliche Frage deutete an, dass es schön wäre, alle Eigenschaften anhand eines Präfixes filtern zu können. Ich habe gerade bestätigt, dass dies ab Spring Boot 2.1.1.RELEASE für Properties oder funktioniert Map<String,String>. Ich bin sicher, es hat schon eine Weile funktioniert. Interessanterweise funktioniert es nicht ohne die prefix =Qualifikation, dh ich weiß nicht , wie ich die gesamte Umgebung in eine Karte laden kann. Wie gesagt, dies könnte tatsächlich das sein, womit OP beginnen wollte. Das Präfix und das folgende '.' wird abgestreift, was sein könnte oder nicht, was man will:

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

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

Nachtrag: Es ist in der Tat möglich und schändlich einfach, die gesamte Umgebung zu erhalten. Ich weiß nicht, wie mir das entgangen ist:

@ConfigurationProperties
@Bean
public Properties getProperties() {
    return new Properties();
}
AbuNassar
quelle
1
Eigenschaften wie abc = x werden auch in {b = {c = x}}
verschachtelt
Kein Teil davon hat funktioniert - gibt getAsProperties()immer eine leere PropertiesInstanz zurück, und wenn Sie es ohne ein angegebenes Präfix versuchen, kann es nicht einmal kompiliert werden. Dies ist mit Spring Boot 2.1.6.RELEASE
ArtOfWarfare
1
Ich schreibe kein Java bei der Arbeit, aber ich habe das ziemlich schnell erledigt : github.com/AbuCarlo/SpringPropertiesBean . Es ist möglich, dass es nicht funktioniert, wenn Sie die Startsequenz von Spring irgendwie umgehen (dh die Bean "Eigenschaften" wird nie gefüllt). Dies ist für Java 8, Spring 2.2.6.
AbuNassar
5

Als Jira-Ticket für diesen Frühling ist es ein absichtliches Design. Aber der folgende Code funktioniert für mich.

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
quelle
2

Der Frühling erlaubt keine Entkopplung java.util.Propertiesvon der Frühlingsumgebung.

Funktioniert aber Properties.load()immer noch in einer Spring-Boot-Anwendung:

Properties p = new Properties();
try (InputStream is = getClass().getResourceAsStream("/my.properties")) {
    p.load(is);
}
weberjn
quelle
1

Die anderen Antworten haben die Lösung für die meisten Fälle aufgezeigt PropertySources, aber keiner hat erwähnt, dass bestimmte Eigenschaftsquellen nicht in nützliche Typen umgewandelt werden können.

Ein solches Beispiel ist die Eigenschaftsquelle für Befehlszeilenargumente. Die verwendete Klasse ist SimpleCommandLinePropertySource. Diese private Klasse wird von einer öffentlichen Methode zurückgegeben, was den Zugriff auf die Daten im Objekt äußerst schwierig macht. Ich musste Reflektion verwenden, um die Daten zu lesen und schließlich die Eigenschaftsquelle zu ersetzen.

Wenn jemand da draußen eine bessere Lösung hat, würde ich sie wirklich gerne sehen. Dies ist jedoch der einzige Hack, den ich zur Arbeit gebracht habe.

Chad Van De Hey
quelle
Haben Sie eine Lösung für das Problem mit der nicht öffentlichen Klasse gefunden?
Tobias
1

Bei der Arbeit mit Spring Boot 2 musste ich etwas Ähnliches tun. Die meisten der oben genannten Antworten funktionieren einwandfrei. Beachten Sie jedoch, dass die Ergebnisse in verschiedenen Phasen des App-Lebenszyklus unterschiedlich sind.

Zum Beispiel, nachdem ApplicationEnvironmentPreparedEventirgendwelche Eigenschaften im Inneren application.propertiesnicht vorhanden sind. Nach einem ApplicationPreparedEventEreignis sind sie jedoch.

Mike
quelle
1

Bei Spring Boot überschreibt die akzeptierte Antwort doppelte Eigenschaften mit niedrigeren Eigenschaften . Diese Lösung sammelt die Eigenschaften in a SortedMapund verwendet nur doppelte Eigenschaften mit der höchsten Priorität.

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
quelle
env.getPropertySources () gibt Eigenschaften mit der niedrigsten bis höchsten Priorität?
Faraz
Es ist umgekehrt. Sie sind nach hoher -> niedriger Priorität sortiert.
Samuel Tatipamula
0

Ich dachte, ich würde noch einen Weg hinzufügen. In meinem Fall gebe ich dies an, für com.hazelcast.config.XmlConfigBuilderdas nur java.util.Propertieseinige Eigenschaften in der Hazelcast-XML-Konfigurationsdatei aufgelöst werden müssen, dh nur die getProperty(String)Methode aufgerufen wird. So konnte ich tun, was ich brauchte:

@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 Ich habe dies letztendlich nicht speziell für Hazelcast verwendet, da es nur Eigenschaften für XML-Dateien auflöst, jedoch nicht zur Laufzeit. Da ich auch Spring benutze, habe ich mich für einen Brauch entschieden org.springframework.cache.interceptor.AbstractCacheResolver#getCacheNames. Dadurch werden Eigenschaften für beide Situationen aufgelöst, zumindest wenn Sie Eigenschaften in Cache-Namen verwenden.

Sam
quelle