使用 ConfigurationProperties 以通用方式填充 Map

我想知道,是否有一个通用的方法来填充地图的属性你只知道前缀。

假设有一大堆像

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?

谢谢 汉斯约格

92474 次浏览

我为自己编写了一个 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);
}
}

只要你乐意将每个属性添加到地图中,而不是只是那些你事先不知道的属性,你就可以使用 @ConfigurationProperties做到这一点。如果你想抓取 namespace下面的所有内容,那么你需要使用一个空前缀,并为一个名为 namespace的映射提供一个 getter:

@ConfigurationProperties("")
public class CustomProperties {


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


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


}

SpringBoot 使用 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 {
// …
}

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

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

非常有效, 谢谢!

我疯狂地试图理解为什么@Andy 的 回答对我不起作用(因为它仍然是空的) ,只是为了意识到龙目岛的 @Builder注释挡住了我的去路,它添加了一个非空的构造函数。我添加这个答案是为了强调,为了让 @ConfigurationPropertiesMap上工作,值类型必须有一个 No-Arguments 构造函数。这在春季的 文件节目中也有提及:

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

我希望这能帮别人节省点时间。

我经过的大多数地方都没有完整的描述,或者有点过时,所以下面是我为此采取的完整步骤。注意: 我使用的 Spring 引导版本是 < strong > 2.4.0 :

  1. 添加 pom.xml-spring-boot-configuration-Processing

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
    </dependency>
    
  2. 创建一个属性文件并在其中设置数据-共享信息属性注意: “ LIBRARY _ 10001”是映射键,“找不到书”是映射值

    shared-messages.messages.LIBRARY_10001=Unable to find the book
    shared-messages.messages.LIBRARY_10002=Book already exists
    
  3. 创建使用属性文件的 Properties 类

    @ConfigurationProperties("shared-messages")
    @Getter
    public class LibraryProperties {
    private final Map<String, String> messages = new HashMap<>();
    }
    
  4. 在应用程序级别上,定义属性源并启用配置属性

    @EnableConfigurationProperties(LibraryProperties.class)
    @PropertySource("shared-messages.properties")
    public class LibraryApplication {
    ....
    }
    
  5. 在 Service 级别上,注入 “ LibraryProperties”类并访问所需的属性

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

这可能不是一个完美的解决方案,但我分享了我设法做到这一点的方式,因为我尝试了不同的组合,对我不起作用