【问题标题】:Using ConfigurationProperties to fill Map in generic way使用 ConfigurationProperties 以通用方式填充 Map
【发布时间】:2015-09-11 19:12:58
【问题描述】:

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

假设有一堆类似的属性

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

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

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

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

谢谢 汉斯约尔格

【问题讨论】:

    标签: java spring spring-boot


    【解决方案1】:

    只要您乐于将每个属性添加到地图中,而不仅仅是那些您事先不知道的属性,您就可以使用@ConfigurationProperties 来完成此操作。如果您想获取namespace 下的所有内容,则需要使用空前缀并为名为@9​​87654323@ 的地图提供getter:

    @ConfigurationProperties("")
    public class CustomProperties {
    
        private final Map<String, String> namespace = new HashMap<>();
    
        public Map<String, String> getNamespace() {
            return namespace;
        }
    
    }
    

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

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

    namespace 映射将包含三个条目:

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

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

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

    然后您将使用namespace.foo 作为前缀并将CustomProperties 上的namespacegetNamespace 分别重命名为bargetBar

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

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

    【讨论】:

    • 您能否详细说明为什么我们不应该用@Component 注释@ConfigurationProperties?它在引导文档中完成。你是说偏好是使用@EnableConfiguratinoProperties?
    • 我很着急,应该更好地解释一下自己。我写的措辞太强了。您应该使用@EnableConfigurationProperties 打开对@ConfigurationProperties-annotated beans 的支持。那时,您可以引用您的 @ConfigurationProperties-annotated 类来简化操作,避免将其声明为 bean。这就是 Boot 本身在自己的代码中所做的事情。我会改写我的答案。
    • 似乎不适用于嵌套属性。带有前缀metrics.tagsAsSuffixmetrics.tagsAsSuffix.jvm.memory=area,id 不会以jvm.memory=[area,id] 的形式读入Map&lt;String, List&lt;String&gt;&gt;,而是变成jvm=[]
    • snakeyaml 内部使用 LinkedHashMap(它可能更适合命名空间)
    • 非常感谢,我没有意识到 getter 方法名称很重要,我认为只有成员变量名称很重要。但这是有道理的,因为它获取了 Map 对象的一个​​实例,然后添加到它。
    【解决方案2】:

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

    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"
    

    按照 Jake 的建议,我尝试像这样使用带有 Pojo 的 Map:

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

    像魅力一样工作, 谢谢!

    【讨论】:

    • 注意:为要从属性中填充的字段设置 getter 和 setter 绝对至关重要。即使这些字段是公开的,如果它们没有设置器(或者我想在地图的情况下是吸气剂),Spring 也不会填充它们。我仍然不明白的一件事是 Spring 如何能够推断出映射值应该具有的类(在本例中:Route),因为我一直理解泛型类型信息在运行时不可用,但不知何故它作品。我猜是魔法。
    • @rem - 一些泛型信息在运行时可用于声明类、字段和方法信息(方法参数和返回值)。见例子tutorials.jenkov.com/java-reflection/generics.html
    【解决方案3】:

    我疯了,试图理解为什么 @Andy 的 answer 对我不起作用(例如,Map 仍然是空的)只是为了意识到我有 Lombok 的 @Builder 注释妨碍了我,它添加了一个非空构造函数。我添加这个答案是为了强调为了让@ConfigurationPropertiesMap 上工作,值类型必须有一个无参数构造函数。 Spring的documentation中也提到了这一点:

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

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

    【讨论】:

      【解决方案4】:

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

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-04-06
        • 1970-01-01
        • 1970-01-01
        • 2017-02-04
        • 2012-02-23
        • 1970-01-01
        • 2019-10-09
        • 1970-01-01
        相关资源
        最近更新 更多