如何使用Spring的@Value从Java属性文件中填充HashMap

70

是否可以使用Spring @Value来将属性文件中的值映射到HashMap中。

目前我有这样的代码,将一个值映射是没有问题的。 但我需要将自定义的值映射到HashMap的过期时间中。 这种情况是否可能?

@Service
@PropertySource(value = "classpath:my_service.properties")
public class SomeServiceImpl implements SomeService {


    @Value("#{conf['service.cache']}")
    private final boolean useCache = false;

    @Value("#{conf['service.expiration.[<custom name>]']}")
    private final HashMap<String, String> expirations = new HashMap<String, String>();

属性文件:'my_service.properties'

service.cache=true
service.expiration.name1=100
service.expiration.name2=20

能否像这样映射键值对:

  • name1 = 100

  • name2 = 20


1
新建和Spring Bean工厂是正交的。新建意味着“没有Spring”。 - duffymo
@duffymo 不能这样笼统地概括。新实体、新值对象不属于此类情况。 - madhairsilence
8个回答

68
您可以使用SPEL类json语法在属性文件中编写简单的映射或列表映射。
simple.map={'KEY1': 'value1', 'KEY2': 'value3', 'KEY3': 'value5'}

map.of.list={\
  'KEY1': {'value1','value2'}, \
  'KEY2': {'value3','value4'}, \
  'KEY3': {'value5'} \
 }

我使用\作为多行属性的分隔符以提高可读性。

然后,在Java中,你可以像这样使用@Value来自动访问和解析它。

@Value("#{${simple.map}}")
Map<String, String> simpleMap;

@Value("#{${map.of.list}}")
Map<String, List<String>> mapOfList;

使用${simple.map}@Value从属性文件中获取以下字符串:

"{'KEY1': 'value1', 'KEY2': 'value3', 'KEY3': 'value5'}"

然后,它被评估为如果它是内联的

@Value("#{{'KEY1': 'value1', 'KEY2': 'value3', 'KEY3': 'value5'}}")

您可以在官方文档中了解更多信息。


请问您能否在这里给我指导一下:https://stackoverflow.com/questions/60899860/spring-spel-expression-language-to-create-map-of-string-and-custom-object? - PAA
在属性文件中提供空映射的语法是什么?我可以使用simple.map=#null提供一个空映射,但是simple.map={}会生成异常。 - Pino
也许你可以在代码中提供一个默认值,并在属性文件中不给出任何值。 - Marc Bouvier
这个解决方案很好,但不适用于我的情况。在我的属性中,我需要一个冒号: configurationFields: {"DEPARTMENT:": DEPARTMENT-ID, "AGENT:": AGENT-ID}。我尝试了双引号,单引号,转义字符("DEPARTMENT: "),但没有效果。有人有什么建议吗? - salerokada
@Value("#{${simple.map: {'defaultKey' : 'defaultValue'}}}") - Jonas_Hess

22

能否使用Spring的@Value将属性文件中的值映射到HashMap中?

可以。只需一点代码和Spel的帮助。

首先,请考虑这个单例Spring-bean(您应该扫描它):

@Component("PropertySplitter")
public class PropertySplitter {

    /**
     * Example: one.example.property = KEY1:VALUE1,KEY2:VALUE2
     */
    public Map<String, String> map(String property) {
        return this.map(property, ",");
    }

    /**
     * Example: one.example.property = KEY1:VALUE1.1,VALUE1.2;KEY2:VALUE2.1,VALUE2.2
     */
    public Map<String, List<String>> mapOfList(String property) {
        Map<String, String> map = this.map(property, ";");

        Map<String, List<String>> mapOfList = new HashMap<>();
        for (Entry<String, String> entry : map.entrySet()) {
            mapOfList.put(entry.getKey(), this.list(entry.getValue()));
        }

        return mapOfList;
    }

    /**
     * Example: one.example.property = VALUE1,VALUE2,VALUE3,VALUE4
     */
    public List<String> list(String property) {
        return this.list(property, ",");
    }

    /**
     * Example: one.example.property = VALUE1.1,VALUE1.2;VALUE2.1,VALUE2.2
     */
    public List<List<String>> groupedList(String property) {
        List<String> unGroupedList = this.list(property, ";");

        List<List<String>> groupedList = new ArrayList<>();
        for (String group : unGroupedList) {
            groupedList.add(this.list(group));
        }

        return groupedList;

    }

    private List<String> list(String property, String splitter) {
        return Splitter.on(splitter).omitEmptyStrings().trimResults().splitToList(property);
    }

    private Map<String, String> map(String property, String splitter) {
        return Splitter.on(splitter).omitEmptyStrings().trimResults().withKeyValueSeparator(":").split(property);
    }

}

注意:`PropertySplitter` 类使用 Guava 的 `Splitter` 工具。有关详细信息,请参阅 其文档
然后,在您的某个 bean 中:
@Component
public class MyBean {

    @Value("#{PropertySplitter.map('${service.expiration}')}")
    Map<String, String> propertyAsMap;

}

最后,这个属性:
service.expiration = name1:100,name2:20

这并不完全符合您的要求,因为这个PropertySplitter只处理一个单一属性,将其转换Map,但我认为您可以采用这种指定属性的方式,或者修改PropertySplitter代码以匹配更具层次性的方式。

EL1008E:在类型为'org.springframework.beans.factory.config.BeanExpressionContext'的对象上找不到属性或字段'PropertySplitter' - 可能不是公共的?出现这个错误?我有什么遗漏吗? - Niraj Sonawane
@NirajSonawane PropertySplitter 应该在类路径中,并且 Spring 需要扫描它并找到它,这是另一个 bean。 - fps
感谢您的回复,我已经成功创建了PropertySplitter的实例/Bean。但是在将值注入到映射中时,我遇到了异常。 - Niraj Sonawane
请问您能否在这里给我指导一下:https://stackoverflow.com/questions/60899860/spring-spel-expression-language-to-create-map-of-string-and-custom-object? - PAA

22

从Spring 4.1.x(我无法记住具体的版本)开始,您可以像这样做:

从Spring 4.1.x(我不太确定具体的版本)开始,您可以像这样进行操作

@Value("#{${your.properties.key.name}}")
private Map<String, String> myMap;

您的属性文件中,your.properties.key.name 应该是类似于以下内容:

your.properties.key.name={\
    name1 : 100, \
    name2 : 200 \
}

确保您创建PropertySourcesPlaceholderConfigurer bean,以便在应用程序中使用,如果您编写任何单元测试代码来测试您的代码,则能够正常工作,否则属性值的${...}占位符将无法按预期工作,并且您将看到一些奇怪的SpringEL错误。


3
简单易懂的最新解决问题方式加一。 - dasrohith
请问您能否在这里给我指导一下:https://stackoverflow.com/questions/60899860/spring-spel-expression-language-to-create-map-of-string-and-custom-object? - PAA

20

在Spring Boot中最快的解决方案如下。在我的特定示例中,我正在将数据从一个系统迁移到另一个系统。这就是为什么我需要一个名为priority的字段的映射。

首先,我创建了属性文件(priority-migration.properties),内容如下:

my.prefix.priority.0:0
my.prefix.priority.10:1
my.prefix.priority.15:2
my.prefix.priority.20:2
another.prefix.foo:bar

并将其放在类路径上。

假设您想在Spring管理的bean /组件中使用地图,请在您的类上注释:

@Component
@PropertySource("classpath:/priority-migration.properties")

在您的地图中实际需要的只有以my.prefix为前缀的键/值对,即这部分内容:

您实际上想在地图中获取的是仅带有my.prefix前缀的键/值对,也就是这一部分:

{
    0:0
    10:1
    15:2
    20:2
}

为了实现这一点,您需要使用注释对组件进行标注:

@ConfigurationProperties("my.prefix")

并为优先级中缀创建一个getter。在我的情况下,后者被证明是强制性的(尽管Sring文档中说只需具有一个优先级属性并将其初始化为可变值即可)

private final Map<Integer, Integer> priorityMap = new HashMap<>();

public Map<Integer, Integer> getPriority() {
    return priorityMap;
}

最终结果

它看起来像这样:

@Component
@ConfigurationProperties("my.prefix")
@PropertySource("classpath:/priority-migration.properties")
class PriorityProcessor {

    private final Map<Integer, Integer> priorityMap = new HashMap<>();

    public Map<Integer, Integer> getPriority() {
        return priorityMap;
    }

    public void process() {

        Integer myPriority = priorityMap.get(10)
        // use it here
    }
}

3
@ConfigurationProperties 是 Spring Boot 的注解,而不是 Spring 的注解。 - R.Litto
但是有没有办法在运行时使其可重新加载?如果在应用程序运行时更改属性文件,更改是否会自动反映? - Mayank Madhav
1
嗨Mayank。这个例子不会重新加载属性。但是你应该研究一下@RefreshScope,并将其放在@ConfigurationProperties("my.prefix")下面。请参考这篇文章(见4.1):https://www.baeldung.com/spring-reloading-properties。理论上应该可以工作,但我自己没有测试过。祝好运。 - Viktor Stoitschev
1
感谢 Viktor 的建议,我们最终修改了所有服务,使用 Spring Cloud Config 加载配置。通过使用执行器刷新端点,我能够在运行时重新加载属性。此外,这使我可以拥有一个集中的 GIT 位置来存储所有属性。 - Mayank Madhav
1
感谢您为使用Spring Boot的人提供这种替代方案。这是推荐的方式,因为它允许将属性注入为对象,而且还有其他原因。我发现将相关属性分组更容易。这样我们就可以创建模块化配置并保证类型安全。https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config - Marc Bouvier

15

我受前一篇文章的启发,制作了一种解决方案。

在Spring配置中注册属性文件:

<util:properties id="myProp" location="classpath:my.properties"/>

我创建了一个组件:

@Component("PropertyMapper")
public class PropertyMapper {

    @Autowired
    ApplicationContext applicationContext;

    public HashMap<String, Object> startWith(String qualifier, String startWith) {
        return startWith(qualifier, startWith, false);
    }

    public HashMap<String, Object> startWith(String qualifier, String startWith, boolean removeStartWith) {
        HashMap<String, Object> result = new HashMap<String, Object>();

        Object obj = applicationContext.getBean(qualifier);
        if (obj instanceof Properties) {
            Properties mobileProperties = (Properties)obj;

            if (mobileProperties != null) {
                for (Entry<Object, Object> e : mobileProperties.entrySet()) {
                    Object oKey = e.getKey();
                    if (oKey instanceof String) {
                        String key = (String)oKey;
                        if (((String) oKey).startsWith(startWith)) {
                            if (removeStartWith) 
                                key = key.substring(startWith.length());
                            result.put(key, e.getValue());
                        }
                    }
                }
            }
        }

        return result;
    }
}

当我想要将所有以特定值开头的属性映射到HashMap中时,使用@Value注释:

@Service
public class MyServiceImpl implements MyService {

    @Value("#{PropertyMapper.startWith('myProp', 'service.expiration.', true)}")
    private HashMap<String, Object> portalExpirations;

3
使用@Value从编码为多行application.yml属性中提取Map的解决方案

application.yml

other-prop: just for demo 

my-map-property-name: "{\
         key1: \"ANY String Value here\", \  
         key2: \"any number of items\" , \ 
         key3: \"Note the Last item does not have comma\" \
         }"

other-prop2: just for demo 2 

在这里,我们的地图属性"my-map-property-name"的值以JSON格式存储在一个字符串中,并且我们使用\实现了多行。

myJavaClass.java

import org.springframework.beans.factory.annotation.Value;

public class myJavaClass {

@Value("#{${my-map-property-name}}") 
private Map<String,String> myMap;

public void someRandomMethod (){
    if(myMap.containsKey("key1")) {
            //todo...
    } }

}

更多解释

  • \ 在yaml中用于将字符串分成多行

  • \" 是yaml字符串中"(引号)的转义字符

  • {key:value} 在yaml中是JSON格式,@Value会将其转换为Map

  • #{ } 它是SpEL表达式,可在@Value中使用,将json转换为Map或Array / list Reference

在Spring Boot项目中测试过


0

或者在属性文件中类似于这样的东西

org.code=0009,0008,0010
org.code.0009.channel=30,40
org.code.0008.channel=30,40
org.code.0010.channel=30,40

在Java中,读取org.code,然后循环遍历每个org.code并构建org.code..channel,并将其放入映射中...

0

使用与Yaml名称相同的变量名

例如:

private final HashMap<String, String> expiration 

替代

private final HashMap<String, String> expirations 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接