130

I have a Spring Boot application with the following application.yml - taken basically from here:

info:
   build:
      artifact: ${project.artifactId}
      name: ${project.name}
      description: ${project.description}
      version: ${project.version}

I can inject particular values, e.g.

@Value("${info.build.artifact}") String value

I would like, however, to inject the whole map, i.e. something like this:

@Value("${info}") Map<String, Object> info

Is that (or something similar) possible? Obviously, I can load yaml directly, but was wondering if there's something already supported by Spring.

levant pied
  • 3,886
  • 5
  • 37
  • 56

8 Answers8

123

Below solution is a shorthand for @Andy Wilkinson's solution, except that it doesn't have to use a separate class or on a @Bean annotated method.

application.yml:

input:
  name: raja
  age: 12
  somedata:
    abcd: 1 
    bcbd: 2
    cdbd: 3

SomeComponent.java:

@Component
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "input")
class SomeComponent {

    @Value("${input.name}")
    private String name;

    @Value("${input.age}")
    private Integer age;

    private HashMap<String, Integer> somedata;

    public HashMap<String, Integer> getSomedata() {
        return somedata;
    }

    public void setSomedata(HashMap<String, Integer> somedata) {
        this.somedata = somedata;
    }

}

We can club both @Value annotation and @ConfigurationProperties, no issues. But getters and setters are important and @EnableConfigurationProperties is must to have the @ConfigurationProperties to work.

I tried this idea from groovy solution provided by @Szymon Stepniak, thought it will be useful for someone.

kryger
  • 12,906
  • 8
  • 44
  • 65
raksja
  • 3,969
  • 5
  • 38
  • 44
76

You can have a map injected using @ConfigurationProperties:

import java.util.HashMap;
import java.util.Map;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoConfiguration
@EnableConfigurationProperties
public class MapBindingSample {

    public static void main(String[] args) throws Exception {
        System.out.println(SpringApplication.run(MapBindingSample.class, args)
                .getBean(Test.class).getInfo());
    }

    @Bean
    @ConfigurationProperties
    public Test test() {
        return new Test();
    }

    public static class Test {

        private Map<String, Object> info = new HashMap<String, Object>();

        public Map<String, Object> getInfo() {
            return this.info;
        }
    }
}

Running this with the yaml in the question produces:

{build={artifact=${project.artifactId}, version=${project.version}, name=${project.name}, description=${project.description}}}

There are various options for setting a prefix, controlling how missing properties are handled, etc. See the javadoc for more information.

Andy Wilkinson
  • 108,729
  • 24
  • 257
  • 242
  • Thanks Andy - this works as expected. Interesting that it doesn't work without an extra class - i.e. you cannot put the `info` map within `MapBindingSample` for some reason (maybe because it's being used to run the app in `SpringApplication.run` call). – levant pied Jul 24 '14 at 14:40
  • 1
    Is there a way to inject a sub-map? E.g. inject `info.build` instead of `info` from the above map? – levant pied Jul 24 '14 at 21:01
  • 1
    Yes. Set the prefix on @ConfigurationProperties to info and then update Test replacing getInfo() with a method named getBuild() – Andy Wilkinson Jul 24 '14 at 21:06
  • Nice, thanks Andy, worked like a charm! One more thing - when setting `locations` (to get the properties from another `yml` file instead of the default `application.yml`) on `@ConfigurationProperties`, it worked, except it did not result in placeholders being replaced. E.g. if you had a system property `project.version=123` set, the example you gave in the answer would return `version=123`, while after setting `locations` it would return `project.version=${project.version}`. Do you know if there's a limitation of some sorts here? – levant pied Jul 25 '14 at 12:26
  • That's a limitation. I've opened an issue (https://github.com/spring-projects/spring-boot/issues/1301) to perform placeholder replacement when you use a custom location – Andy Wilkinson Jul 28 '14 at 15:09
  • @AndyWilkinson How can I inject a map with type `Map`, because I want to have the map to record an Operation Type and the Handler(a class), how can I specify the type? Thank you! – NingLee Oct 27 '14 at 06:44
  • I have a related problem, but slightly different. My applications.yml file contains something like this: foo: bar: x sysprops: webdriver.chrome.driver: chromedriver Unfortunately the YAML processor treats the dot separated key as nested. I've even tried 'webdriver.chrome.driver' but it doesn't make a difference. The result looks like this: webdriver: {chrome={driver=chromedriver}} Is there an easy way to deal with this? – user2337270 May 08 '17 at 21:29
  • @user2337270 A new question would be a better place to ask – Andy Wilkinson May 09 '17 at 08:00
  • If you're trying to set the key for one of your values to a string with spaces, dots, or other special characters, try surrounding it with square brackets surrounded by quotes. E.g. to "[key value.with.dots]" to avoid having Spring strip those characters. – KC Baltz Aug 07 '18 at 23:28
  • can we create a separate configuration class to create the bean, instead of writing it in main class? – ajith george Jul 08 '19 at 06:57
  • @ajithgeorge Yes, you can. – Andy Wilkinson Jul 08 '19 at 11:22
34

To retrieve map from configuration you will need configuration class. @Value annotation won't do the trick, unfortunately.

Application.yml

entries:
  map:
     key1: value1
     key2: value2

Configuration class:

@Configuration
@ConfigurationProperties("entries")
@Getter
@Setter
 public static class MyConfig {
     private Map<String, String> map;
 }
Ahmed Nabil
  • 17,392
  • 11
  • 61
  • 88
Orbite
  • 505
  • 4
  • 8
18

Solution for pulling Map using @Value from application.yml property coded as multiline

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 

Here the value for our map property "my-map-property-name" is stored in JSON format inside a string and we have achived multiline using \ at end of line

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

}

More explanation

  • \ in yaml it is Used to break string into multiline

  • \" is escape charater for "(quote) in yaml string

  • {key:value} JSON in yaml which will be converted to Map by @Value

  • #{ } it is SpEL expresion and can be used in @Value to convert json int Map or Array / list Reference

Tested in a spring boot project

Milan
  • 829
  • 8
  • 12
17

I run into the same problem today, but unfortunately Andy's solution didn't work for me. In Spring Boot 1.2.1.RELEASE it's even easier, but you have to be aware of a few things.

Here is the interesting part from my application.yml:

oauth:
  providers:
    google:
     api: org.scribe.builder.api.Google2Api
     key: api_key
     secret: api_secret
     callback: http://callback.your.host/oauth/google

providers map contains only one map entry, my goal is to provide dynamic configuration for other OAuth providers. I want to inject this map into a service that will initialize services based on the configuration provided in this yaml file. My initial implementation was:

@Service
@ConfigurationProperties(prefix = 'oauth')
class OAuth2ProvidersService implements InitializingBean {

    private Map<String, Map<String, String>> providers = [:]

    @Override
    void afterPropertiesSet() throws Exception {
       initialize()
    }

    private void initialize() {
       //....
    }
}

After starting the application, providers map in OAuth2ProvidersService was not initialized. I tried the solution suggested by Andy, but it didn't work as well. I use Groovy in that application, so I decided to remove private and let Groovy generates getter and setter. So my code looked like this:

@Service
@ConfigurationProperties(prefix = 'oauth')
class OAuth2ProvidersService implements InitializingBean {

    Map<String, Map<String, String>> providers = [:]

    @Override
    void afterPropertiesSet() throws Exception {
       initialize()
    }

    private void initialize() {
       //....
    }
}

After that small change everything worked.

Although there is one thing that might be worth mentioning. After I make it working I decided to make this field private and provide setter with straight argument type in the setter method. Unfortunately it wont work that. It causes org.springframework.beans.NotWritablePropertyException with message:

Invalid property 'providers[google]' of bean class [com.zinvoice.user.service.OAuth2ProvidersService]: Cannot access indexed value in property referenced in indexed property path 'providers[google]'; nested exception is org.springframework.beans.NotReadablePropertyException: Invalid property 'providers[google]' of bean class [com.zinvoice.user.service.OAuth2ProvidersService]: Bean property 'providers[google]' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?

Keep it in mind if you're using Groovy in your Spring Boot application.

Szymon Stepniak
  • 40,216
  • 10
  • 104
  • 131
12

In case of direct @Value injection, the most elegant way is writing the key-values as an inline json (use ' and " chars to avoid cumbersome escapings) and parsing it using SPEL:

#in yaml file:
my:
  map:
      is: '{ "key1":"val1", 
              "key2":"val2" }'

in your @Component or @Bean, :

@Component
public class MyClass{
     @Value("#{${my.map.is}}")
     Map<String,String> myYamlMap;
}

for a more YAML convenient syntax, you can avoid the json curly braces altogether, directly typing the key value pairs

 my:  
   map:  
       is: '"a":"b", "foo":"bar"'

and add the missing curly braces directly to your @Value SPEL expression:

@Value("#{{${my.map.is}}}")
 Map<String,String> myYamlMap;

the value will be resolved from the yaml, the wrapping curlies will be concatenated to it, and finally the SPEL expression will resolve the string as map.

Constant
  • 377
  • 4
  • 10
4
foo.bars.one.counter=1
foo.bars.one.active=false
foo.bars[two].id=IdOfBarWithKeyTwo

public class Foo {

  private Map<String, Bar> bars = new HashMap<>();

  public Map<String, Bar> getBars() { .... }
}

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding

emerson moura
  • 345
  • 2
  • 3
  • 7
    Welcome to Stack Overflow! While this code snippet may solve the question, [including an explanation](//meta.stackexchange.com/questions/114762/explaining-entirely-code-based-answers) really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. – Scott Weldon Jul 19 '16 at 16:18
  • the link to the wiki is valuable, though. The explanation is at https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding#nested-property – dschulten Feb 27 '19 at 09:49
1

You can make it even simplier, if you want to avoid extra structures.

service:
  mappings:
    key1: value1
    key2: value2
@Configuration
@EnableConfigurationProperties
public class ServiceConfigurationProperties {

  @Bean
  @ConfigurationProperties(prefix = "service.mappings")
  public Map<String, String> serviceMappings() {
    return new HashMap<>();
  }

}

And then use it as usual, for example with a constructor:

public class Foo {

  private final Map<String, String> serviceMappings;

  public Foo(Map<String, String> serviceMappings) {
    this.serviceMappings = serviceMappings;
  }

}