Verwenden von ConfigurationProperties zum generischen Füllen von Maps

Lesezeit: 11 Minuten

Ich frage mich, ob es eine allgemeine Möglichkeit gibt, eine Karte mit Eigenschaften zu füllen, bei der Sie nur das Präfix kennen.

Angenommen, es gibt eine Reihe von Eigenschaften wie

namespace.prop1=value1
namespace.prop2=value2
namespace.iDontKnowThisNameAtCompileTime=anothervalue

Ich hätte gerne eine generische Möglichkeit, diese Eigenschaft in einer Karte zu füllen, so etwas wie

@Component
@ConfigurationProperties("namespace")
public class MyGenericProps {
    private Map<String, String> propmap = new HashMap<String, String>();

    // setter and getter for propmap omitted

    public Set<String> returnAllKeys() {
        return propmap.keySet();
    }
}

Oder gibt es eine andere bequeme Möglichkeit, alle Eigenschaften mit einem bestimmten Präfix zu sammeln, anstatt alle PropertySources in der Umgebung zu durchlaufen?

Danke Hansjörg

Benutzer-Avatar
Andy Wilkinson

Solange Sie damit zufrieden sind, dass jede Immobilie der Karte hinzugefügt wurde, und nicht nur die, die Sie nicht im Voraus kennen, können Sie dies tun @ConfigurationProperties. Wenn Sie alles greifen wollen, was darunter ist namespace dann müssen Sie ein leeres Präfix verwenden und einen Getter für eine benannte Karte bereitstellen namespace:

@ConfigurationProperties("")
public class CustomProperties {

    private final Map<String, String> namespace = new HashMap<>();

    public Map<String, String> getNamespace() {
        return namespace;
    }

}

Spring Boot verwendet die getNamespace -Methode, um die Karte abzurufen, damit sie ihr die Eigenschaften hinzufügen kann. Mit diesen Eigenschaften:

namespace.a=alpha
namespace.b=bravo
namespace.c=charlie

Das namespace map enthält drei Einträge:

{a=alpha, b=bravo, c=charlie}

Wenn die Eigenschaften tiefer verschachtelt wurden, zum Beispiel:

namespace.foo.bar.a=alpha
namespace.foo.bar.b=bravo
namespace.foo.bar.c=charlie

Dann würden Sie verwenden namespace.foo als Präfix und umbenennen namespace und getNamespace an CustomProperties zu bar und getBar beziehungsweise.

Beachten Sie, dass Sie sich bewerben sollten @EnableConfigurationProperties zu Ihrer Konfiguration, um die Unterstützung für zu aktivieren @ConfigurationProperties. Sie können dann auf alle Beans verweisen, die Sie mit dieser Annotation verarbeiten möchten, anstatt eine anzugeben @Bean Methode für sie, oder mit @Component um sie durch Komponenten-Scanning entdecken zu lassen:

@SpringBootApplication
@EnableConfigurationProperties(CustomProperties.class)
public class YourApplication {
    // …
}

  • Können Sie erläutern, warum wir @ConfigurationProperties nicht mit @Component annotieren sollten? Es ist in den Boot-Dokumenten getan. Wollen Sie damit sagen, dass die Verwendung von @EnableConfiguratinoProperties bevorzugt wird?

    – jst

    25. Juni 2015 um 15:25 Uhr

  • Ich war in Eile und hätte mich etwas besser erklären sollen. Was ich geschrieben habe, war zu stark formuliert. Du solltest benutzen @EnableConfigurationProperties um die Unterstützung für einzuschalten @ConfigurationProperties-beschriftete Bohnen. An diesem Punkt können Sie auf Ihre verweisen @ConfigurationProperties-annotierte Klasse, um Dinge zu verkürzen und zu vermeiden, dass sie als Bohne deklariert werden müssen. Dies ist, was Boot selbst in seinem eigenen Code tut. Ich werde diesen Teil meiner Antwort umformulieren.

    – Andy Wilkinson

    25. Juni 2015 um 16:35 Uhr


  • Sieht nicht so aus, als ob es für verschachtelte Eigenschaften funktioniert. metrics.tagsAsSuffix.jvm.memory=area,id mit Präfix metrics.tagsAsSuffix wird nicht eingelesen Map<String, List<String>> wie jvm.memory=[area,id] aber wird jvm=[]

    – Abhijit Sarkar

    9. Juni 2018 um 23:16 Uhr

  • snakeyaml verwendet intern LinkedHashMap (es könnte besser für Namensräume geeignet sein)

    – vmartin

    23. Juli 2018 um 11:39 Uhr

  • Vielen Dank, ich wusste nicht, dass der Name der Getter-Methode wichtig ist, ich dachte, nur der Name der Member-Variablen wäre wichtig. Aber es macht Sinn, da es eine Instanz des Map-Objekts erhält und dann etwas hinzufügt.

    – Matt Weiler

    15. Februar 2019 um 20:52 Uhr

Außerdem war mein Problem, dass ich nicht mehrere einfache Schlüssel/Wert-Eigenschaften hatte, sondern ganze Objekte:

zuul:
  routes:
    query1:
      path: /api/apps/test1/query/**
      stripPrefix: false
      url: "https://test.url.com/query1"
    query2:
       path: /api/apps/test2/query/**
       stripPrefix: false
       url: "https://test.url.com/query2"
    index1:
       path: /api/apps/*/index/**
       stripPrefix: false
       url: "https://test.url.com/index"

Nach Jakes Rat habe ich versucht, eine Karte mit einem Pojo wie folgt zu verwenden:

@ConfigurationProperties("zuul")
public class RouteConfig {
    private Map<String, Route> routes = new HashMap<>();

    public Map<String, Route> getRoutes() {
        return routes;
    }

    public static class Route {
        private String path;
        private boolean stripPrefix;
        String url;

        // [getters + setters]
    }
}

Funktioniert wie ein Zauber, danke!

  • Hinweis: Es ist absolut entscheidend, Getter und Setter für die Felder zu haben, die Sie aus den Eigenschaften füllen möchten. Selbst wenn die Felder öffentlich sind, füllt Spring sie nicht, wenn sie keine Setter haben (oder einen Getter, im Fall einer Karte, nehme ich an). Eine Sache, die ich immer noch nicht verstehe, ist, wie Spring ableiten kann, welche Klasse die Kartenwerte haben sollten (in diesem Fall: Route), da ich immer verstanden habe, dass generische Typinformationen zur Laufzeit nicht verfügbar sind, aber irgendwie funktioniert es. Magie, schätze ich.

    – Rest

    25. Februar 2021 um 13:31 Uhr


  • @rem – Einige generische Informationen sind zur Laufzeit für die Deklaration von Klassen, Feldern und Methodeninformationen (Methodenargumente und Rückgabewert) verfügbar. siehe Beispiel tutorials.jenkov.com/java-reflection/generics.html

    – YanivJ

    1. März 2021 um 17:38 Uhr

  • Wichtig! Wenn Sie keinen Getter für Map haben (in diesem Fall getRoutes), werden die Werte nicht festgelegt.

    – cdalxndr

    15. April um 10:28 Uhr


Ich wurde verrückt, als ich versuchte zu verstehen, warum @ Andys Antwort bei mir nicht funktionierte (wie in, the Map blieb leer), nur um festzustellen, dass ich Lomboks hatte @Builder Anmerkung, die im Weg stand, wodurch ein nicht leerer Konstruktor hinzugefügt wurde. Ich füge diese Antwort hinzu, um dies zu betonen @ConfigurationProperties an etwas arbeiten Map, muss der Werttyp über einen No-Arguments-Konstruktor verfügen. Dies wird auch in Spring’s erwähnt Dokumentation:

Eine solche Anordnung basiert auf einem leeren Standardkonstruktor, und Getter und Setter sind normalerweise obligatorisch …

Ich hoffe, das wird jemand anderem Zeit ersparen.

Benutzer-Avatar
Alter Geizhals

Ich schrieb mir a MapFilter Klasse, damit effizient umzugehen. Im Wesentlichen erstellen Sie eine Map und filtern Sie es dann, indem Sie ein Präfix für den Schlüssel angeben. Es gibt auch einen Konstruktor, der a akzeptiert Properties zur Bequemlichkeit.

Beachten Sie, dass dies nur die Hauptkarte filtert. Alle Änderungen, die auf die gefilterte Karte angewendet werden, werden auch auf die Basiskarte angewendet, einschließlich Löschungen usw., aber offensichtlich werden Änderungen an der Hauptkarte nicht in der gefilterten Karte widergespiegelt, bis etwas einen Neuaufbau verursacht.

Es ist auch sehr einfach (und effizient), bereits gefilterte Karten zu filtern.

public class MapFilter<T> implements Map<String, T> {

    // The enclosed map -- could also be a MapFilter.
    final private Map<String, T> map;
    // Use a TreeMap for predictable iteration order.
    // Store Map.Entry to reflect changes down into the underlying map.
    // The Key is the shortened string. The entry.key is the full string.
    final private Map<String, Map.Entry<String, T>> entries = new TreeMap<>();
    // The prefix they are looking for in this map.
    final private String prefix;

    public MapFilter(Map<String, T> map, String prefix) {
        // Store my backing map.
        this.map = map;
        // Record my prefix.
        this.prefix = prefix;
        // Build my entries.
        rebuildEntries();
    }

    public MapFilter(Map<String, T> map) {
        this(map, "");
    }

    private synchronized void rebuildEntries() {
        // Start empty.
        entries.clear();
        // Build my entry set.
        for (Map.Entry<String, T> e : map.entrySet()) {
            String key = e.getKey();
            // Retain each one that starts with the specified prefix.
            if (key.startsWith(prefix)) {
                // Key it on the remainder.
                String k = key.substring(prefix.length());
                // Entries k always contains the LAST occurrence if there are multiples.
                entries.put(k, e);
            }
        }

    }

    @Override
    public String toString() {
        return "MapFilter (" + prefix + ") of " + map + " containing " + entrySet();
    }

    // Constructor from a properties file.
    public MapFilter(Properties p, String prefix) {
        // Properties extends HashTable<Object,Object> so it implements Map.
        // I need Map<String,T> so I wrap it in a HashMap for simplicity.
        // Java-8 breaks if we use diamond inference.
        this(new HashMap<String, T>((Map) p), prefix);
    }

    // Helper to fast filter the map.
    public MapFilter<T> filter(String prefix) {
        // Wrap me in a new filter.
        return new MapFilter<>(this, prefix);
    }

    // Count my entries.
    @Override
    public int size() {
        return entries.size();
    }

    // Are we empty.
    @Override
    public boolean isEmpty() {
        return entries.isEmpty();
    }

    // Is this key in me?
    @Override
    public boolean containsKey(Object key) {
        return entries.containsKey(key);
    }

    // Is this value in me.
    @Override
    public boolean containsValue(Object value) {
        // Walk the values.
        for (Map.Entry<String, T> e : entries.values()) {
            if (value.equals(e.getValue())) {
                // Its there!
                return true;
            }
        }
        return false;
    }

    // Get the referenced value - if present.
    @Override
    public T get(Object key) {
        return get(key, null);
    }

    // Get the referenced value - if present.
    public T get(Object key, T dflt) {
        Map.Entry<String, T> e = entries.get((String) key);
        return e != null ? e.getValue() : dflt;
    }

    // Add to the underlying map.
    @Override
    public T put(String key, T value) {
        T old = null;
        // Do I have an entry for it already?
        Map.Entry<String, T> entry = entries.get(key);
        // Was it already there?
        if (entry != null) {
            // Yes. Just update it.
            old = entry.setValue(value);
        } else {
            // Add it to the map.
            map.put(prefix + key, value);
            // Rebuild.
            rebuildEntries();
        }
        return old;
    }

    // Get rid of that one.
    @Override
    public T remove(Object key) {
        // Do I have an entry for it?
        Map.Entry<String, T> entry = entries.get((String) key);
        if (entry != null) {
            entries.remove(key);
            // Change the underlying map.
            return map.remove(prefix + key);
        }
        return null;
    }

    // Add all of them.
    @Override
    public void putAll(Map<? extends String, ? extends T> m) {
        for (Map.Entry<? extends String, ? extends T> e : m.entrySet()) {
            put(e.getKey(), e.getValue());
        }
    }

    // Clear everything out.
    @Override
    public void clear() {
        // Just remove mine.
        // This does not clear the underlying map - perhaps it should remove the filtered entries.
        for (String key : entries.keySet()) {
            map.remove(prefix + key);
        }
        entries.clear();
    }

    @Override
    public Set<String> keySet() {
        return entries.keySet();
    }

    @Override
    public Collection<T> values() {
        // Roll them all out into a new ArrayList.
        List<T> values = new ArrayList<>();
        for (Map.Entry<String, T> v : entries.values()) {
            values.add(v.getValue());
        }
        return values;
    }

    @Override
    public Set<Map.Entry<String, T>> entrySet() {
        // Roll them all out into a new TreeSet.
        Set<Map.Entry<String, T>> entrySet = new TreeSet<>();
        for (Map.Entry<String, Map.Entry<String, T>> v : entries.entrySet()) {
            entrySet.add(new Entry<>(v));
        }
        return entrySet;
    }

    /**
     * An entry.
     *
     * @param <T>
     *
     * The type of the value.
     */
    private static class Entry<T> implements Map.Entry<String, T>, Comparable<Entry<T>> {

        // Note that entry in the entry is an entry in the underlying map.
        private final Map.Entry<String, Map.Entry<String, T>> entry;

        Entry(Map.Entry<String, Map.Entry<String, T>> entry) {
            this.entry = entry;
        }

        @Override
        public String getKey() {
            return entry.getKey();
        }

        @Override
        public T getValue() {
            // Remember that the value is the entry in the underlying map.
            return entry.getValue().getValue();
        }

        @Override
        public T setValue(T newValue) {
            // Remember that the value is the entry in the underlying map.
            return entry.getValue().setValue(newValue);
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof Entry)) {
                return false;
            }
            Entry e = (Entry) o;
            return getKey().equals(e.getKey()) && getValue().equals(e.getValue());
        }

        @Override
        public int hashCode() {
            return getKey().hashCode() ^ getValue().hashCode();
        }

        @Override
        public String toString() {
            return getKey() + "=" + getValue();
        }

        @Override
        public int compareTo(Entry<T> o) {
            return getKey().compareTo(o.getKey());
        }
    }

    // Simple tests.
    public static void main(String[] args) {
        String[] samples = {
            "Some.For.Me",
            "Some.For.You",
            "Some.More",
            "Yet.More"};
        Map map = new HashMap();
        for (String s : samples) {
            map.put(s, s);
        }
        Map all = new MapFilter(map);
        Map some = new MapFilter(map, "Some.");
        Map someFor = new MapFilter(some, "For.");
        System.out.println("All: " + all);
        System.out.println("Some: " + some);
        System.out.println("Some.For: " + someFor);
    }
}

Benutzer-Avatar
Tsvetoslav Tsvetkov

Die meisten Orte, die ich durchquere, sind nicht vollständig beschreibend oder etwas veraltet, also sind hier die vollständigen Schritte, die ich unternommen habe, um dies zu tun. Hinweis: Die von mir verwendete Spring Boot-Version ist 2.4.0:

  1. Fügen Sie pom.xml – spring-boot-configuration-processor hinzu

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    
  2. Erstellen Sie eine Eigenschaftendatei und legen Sie darin Daten fest – shared-messages.properties Notiz: “BIBLIOTHEK_10001” ist der Kartenschlüssel, “Buch kann nicht gefunden werden” ist der Kartenwert

    shared-messages.messages.LIBRARY_10001=Unable to find the book
    shared-messages.messages.LIBRARY_10002=Book already exists
    
  3. Erstellen Sie eine Properties-Klasse, die die Properties-Datei verwendet

    @ConfigurationProperties("shared-messages")
    @Getter
    public class LibraryProperties {
       private final Map<String, String> messages = new HashMap<>();
    }
    
  4. Definieren Sie auf Anwendungsebene die Eigenschaftsquelle und Konfigurationseigenschaft aktivieren

    @EnableConfigurationProperties(LibraryProperties.class)
    @PropertySource("shared-messages.properties")
    public class LibraryApplication {
      ....
    }
    
  5. Inject auf der Serviceebene “Bibliothekseigenschaften” Klasse und greifen Sie auf die Eigenschaft zu, die Sie benötigen

    @Autowired
    private LibraryProperties libraryProperties;
    
    
    libraryProperties.getMessages().get("LIBRARY_10001")
    

Es ist vielleicht nicht die perfekte Lösung, aber ich teilte mit, wie ich das geschafft habe, weil ich verschiedene Kombinationen ausprobiert habe, die für mich nicht funktionierten

1145310cookie-checkVerwenden von ConfigurationProperties zum generischen Füllen von Maps

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy