Java 使用 ConfigurationProperties 以通用方式填充 Map

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/31045955/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 10:34:58  来源:igfitidea点击:

Using ConfigurationProperties to fill Map in generic way

javaspringspring-boot

提问by Hansjoerg Wingeier

I'm wondering, if there is a generic way to fill a map with properties you just know the prefix.

我想知道,是否有一种通用的方法可以用您只知道前缀的属性填充地图。

Assuming there are a bunch of properties like

假设有一堆属性,如

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

I'd like to have a generic way to fill this property inside a map, something like

我想有一个通用的方法来填充地图中的这个属性,比如

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

Or is there another convenient way to collect all properties with a certain prefix, instead of iterating over all PropertySources in the environment?

或者是否有另一种方便的方法来收集具有特定前缀的所有属性,而不是迭代环境中的所有 PropertySource?

Thanks Hansjoerg

感谢 Hansjoerg

采纳答案by Andy Wilkinson

As long as you're happy having every property added into the map, rather than just those that you don't know in advance, you can do this with @ConfigurationProperties. If you want to grab everything that's beneath namespacethen you need to use an empty prefix and provide a getter for a map named namespace:

只要您很高兴将每个属性添加到地图中,而不仅仅是那些您事先不知道的属性,您就可以使用@ConfigurationProperties. 如果您想获取下面的所有内容,namespace则需要使用空前缀并为名为 的地图提供一个 getter namespace

@ConfigurationProperties("")
public class CustomProperties {

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

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

}

Spring Boot uses the getNamespacemethod to retrieve the map so that it can add the properties to it. With these properties:

Spring Boot 使用该getNamespace方法来检索映射,以便它可以向其添加属性。具有这些属性:

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

The namespacemap will contain three entries:

namespace地图将包含三个条目:

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

If the properties were nested more deeply, for example:

如果属性嵌套更深,例如:

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

Then you'd use namespace.fooas the prefix and rename namespaceand getNamespaceon CustomPropertiesto barand getBarrespectively.

然后你会使用namespace.foo作为前缀和重命名namespace以及getNamespaceCustomPropertiesbargetBar分别。

Note that you should apply @EnableConfigurationPropertiesto your configuration to enable support for @ConfigurationProperties. You can then reference any beans that you want to be processed using that annotation, rather than providing an @Beanmethod for them, or using @Componentto have them discovered by component scanning:

请注意,您应该应用@EnableConfigurationProperties到您的配置以启用对@ConfigurationProperties. 然后,您可以使用该注释引用您想要处理的任何 bean,而不是@Bean为它们提供方法,或者使用@Component组件扫描来发现它们:

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

回答by OldCurmudgeon

I wrote myself a MapFilterclass to handle this efficiently. Essentially, you create a Mapand then filter it by specifying a prefix for the key. There is also a constructor that takes a Propertiesfor convenience.

我给自己写了一个MapFilter类来有效地处理这个问题。本质上,您创建 aMap然后通过为键指定前缀来过滤它。Properties为了方便起见,还有一个构造函数。

Be aware that this just filters the main map. Any changes applied to the filtered map are also applied to the base map, including deletions etc but obviously changes to the main map will not be reflected in the filtered map until something causes a rebuild.

请注意,这只是过滤主地图。应用于过滤地图的任何更改也会应用于基本地图,包括删除等,但显然对主地图的更改不会反映在过滤地图中,直到某些原因导致重建。

It is also very easy (and efficient) to filter already filtered maps.

过滤已过滤的地图也非常容易(且高效)。

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

回答by MMascarin

In addition to this, my problem was that I didn't had multiple simple key/value properties but whole objects:

除此之外,我的问题是我没有多个简单的键/值属性,而是整个对象:

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"

Following Jake's advice I tried to use a Map with a Pojo like this:

按照 Jake 的建议,我尝试将 Map 与这样的 Pojo 一起使用:

@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]
    }
}

Works like a charm, Thanks!

像魅力一样工作,谢谢!

回答by asherbar

I was going nuts trying to understand why @Andy's answerwasn't working for me (as in, the Mapwas remaining empty) just to realize that I had Lombok's @Builderannotation getting in the way, which added a non-empty constructor. I'm adding this answer to emphasize that in order for @ConfigurationPropertiesto work on Map, the value type must have a No-Arguments constructor. This is also mentioned in Spring's documentation:

我疯狂地试图理解为什么@Andy 的答案对我不起作用(因为Map它仍然是空的)只是为了意识到我有 Lombok 的@Builder注释妨碍了,它添加了一个非空的构造函数。我添加这个答案是为了强调为了@ConfigurationProperties工作Map,值类型必须有一个 No-Arguments 构造函数。Spring的文档中也提到了这一点:

Such arrangement relies on a default empty constructor and getters and setters are usually mandatory ...

这种安排依赖于默认的空构造函数,而 getter 和 setter 通常是强制性的......

I hope this will save someone else some time.

我希望这会为其他人节省一些时间。