I really like the #ConfigurationProperties-functionality of Spring to load my configuration properties via YAML into a Java class.
Normally I would use it in a way, that I have the following yaml-file:
lst:
typ:
A: "FLT"
B: "123"
C: "345"
D: "TTS"
The type-attribute would be mapped to a Java-Map. Now i would like to have a solution to reference yaml-fragments in the yaml-file itself, so that I could reuse the reference to the fragment:
lst: ${typs}
typs:
A: "FLT"
B: "123"
C: "345"
D: "TTS"
Is that possible with Spring and #ConfigurationProperties?
I believe it is only possible to use placeholder with string properties.
This leaves you with 2 options:
repeat the values;
OR define the map as a String of properties (source: https://stackoverflow.com/a/28370899/641627).
Solution - Define the map as a comma-separated String of key-values
The whole explanation is provided if you click on the link above. I'll walk you through it.
(1) application.yml:
prop1: A:FLT, B:123, C...
prop2: ${prop1}
(2) Define a String to Map converter/splitter (from #FedericoPeraltaSchaffner's answer)
#Component("PropertySplitter")
public class PropertySplitter {
public Map<String, String> map(String property) {
return this.map(property, ",");
}
private Map<String, String> map(String property, String splitter) {
return Splitter.on(splitter).omitEmptyStrings().trimResults().withKeyValueSeparator(":").split(property);
}
}
(3) Inject the map using #Value and the splitter:
#Value("#{PropertySplitter.map('${prop1}')}")
Map<String, String> prop1;
#Value("#{PropertySplitter.map('${prop2}')}")
Map<String, String> prop2;
Related
I have a following yml config:
foo:
bar.com:
a: b
baz.com:
a: c
With a following class Spring tries to inject map with keys 'bar' and 'baz', treating dots as separator:
public class JavaBean {
private Map<String, AnotherBean> foo;
(...)
}
I have tried quoting the key (i.e. 'bar.com' or "bar.com") but to no avail - still the same problem. Is there a way around this?
A slight revision of #fivetenwill's answer, which works for me on Spring Boot 1.4.3.RELEASE:
foo:
"[bar.com]":
a: b
"[baz.com]":
a: c
You need the brackets to be inside quotes, otherwise the YAML parser basically discards them before they get to Spring, and they don't make it into the property name.
This should work:
foo:
"[bar.com]":
a: b
"[baz.com]":
a: c
Inspired from Spring Boot Configuration Binding Wiki
This is not possible if you want automatic mapping of yaml keys to Java bean attributes. Reason being, Spring first convert YAML into properties format.
See section 24.6.1 of link below:
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html
So, your YAML is converted to:
foo.bar.com.a=b
foo.baz.com.a=c
Above keys are parsed as standard properties.
As a work around, you can use Spring's YamlMapFactoryBean to create a Yaml Map as it is. Then, you can use that map to create Java beans by your own.
#Configuration
public class Config {
private Map<String, Object> foo;
#Bean
public Map<String, Object> setup() {
foo = yamlFactory().getObject();
System.out.println(foo); //Prints {foo={bar.com={a=b}, baz.com={a=c}}}
return foo;
}
#Bean
public YamlMapFactoryBean yamlFactory() {
YamlMapFactoryBean factory = new YamlMapFactoryBean();
factory.setResources(resource());
return factory;
}
public Resource resource() {
return new ClassPathResource("a.yaml"); //a.yaml contains your yaml config in question
}
}
I am trying to load come properties from config using the Spring #Configuration & #ConfigurationProperties combination. I have a POJO that extends HashMap<Integer, String> and has a single variable inside. (See MyConfigPojo below). From the .yaml file, I have the same shape. However, when booting up the app, I get an error when trying to parse the string for the defaultValue into an Integer.
#Configuration
#ConfigurationPropeties(prefix = "my-config")
public class MyConfigPojo extends HashMap<Integer, String> {
private String defaultValue;
private String getValueForIdOrDefault(int id); // this.get(id) OR ELSE defaultValue
}
In config I have this:
myConfig:
1: "my first value"
23: "my 23rd value"
defaultValue: "cheese"
Which results in a
APPLICATION FAILED TO START
Description:
Failed to bind properties under 'myPackage' to MyConfigPojo:
Property: myConfig[1]
Value: cheese
I thought that was weird, so I turned on TRACE logs and found this:
99 common frames omittedCaused by: java.lang.NumberFormatException: For input string: "defaultValue" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
Is there a way to intercept reading this file or to tell Spring how to set the values properly?
You are trying to read a Map from .yml file, for that, you need #ConfigurationProperties annotation enough, don't need #Configuration annotation(if using spring boot). If you are using spring-framework, you need #Configuration also.
Use this example:
myConfig:
map:
1: "my first value"
23: "my 23rd value"
defaultValue: "cheese"
MyConfigPojo.java
package com.org;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.HashMap;
#Configuration
#ConfigurationProperties(prefix = "my-config")
public class MyConfigPojo {
private HashMap<String, String> map;
public HashMap<String, String> getMap() {
return map;
}
public void setMap(HashMap<String, String> map) {
this.map = map;
}
}
Now, you can Autowire this MyConfigPojo class anywhere(for instance, in controller class) and read those map keys and values.
#Autowired
MyConfigPojo pojo;
Now, you have considered keys & Values as String datatype for that Map, you will not get NumberFormatException.
I have a properties file that contains a map with keys that each have multiple values, like below
//properties.yml
myMap:
key1: value1, value2
key2: value1, value2, value3
It is fairly easy to read myMap using a Spring properties class as follows:
#Configuration
#ConfigurationProperties
public class MyConfiguration {
private Map<String, List<String>> myMap;
public Map<String, List<String>> getMyMap() {
return myMap;
}
public void setMyMap(Map<String, List<String>> myMap) {
this.myMap = myMap;
}
}
However this feels like a lot of code for a simple task. I was wondering if there is a way to achieve the same thing using Spring's #Value annotation? I've tried to get it to work without success, trying things like
#Value("${myMap}")
private Map<String, List<String>> myMap;
I think maybe it requires SPEL but I'm not sure how
To Inject Map using #Value you can do (but maybe you need to modify your YAML):
#Value("#{${myMap}}")
private Map<String, List<String>> myMap;
However, It is encouraged to use #ConfigurationProperties instead of #Value (especially if you use YAML format, Spring boot uses SnakeYAML to parse YAML files)
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties
You don't need a setter when loading to the map:
#ConfigurationProperties
public class MapProperties {
private Map<String, List<String>> myMap = new HashMap<>();
public Map<String, List<String>> getMyMap() {
return this.myMap;
}
}
#value annotation does not support the Relaxed binding docs and it only supports SpEL evaluation
If you define a set of configuration keys for your own components, we recommend you group them in a POJO annotated with #ConfigurationProperties. You should also be aware that, since #Value does not support relaxed binding, it is not a good candidate if you need to provide the value by using environment variables.
Finally, while you can write a SpEL expression in #Value, such expressions are not processed from application property files.
I will recommende to use #ConfigurationProperties doc1 doc2
Example
For Map properties, you can bind with property values drawn from multiple sources. However, for the same property in multiple sources, the one with the highest priority is used. The following example exposes a Map from AcmeProperties:
#ConfigurationProperties("acme")
public class AcmeProperties {
private final Map<String, MyPojo> map = new HashMap<>();
public Map<String, MyPojo> getMap() {
return this.map;
}
}
yml
acme:
map:
key1:
name: my name 1
description: my description 1
You could do the following:
#Value("#{${myMap}}")
private Map<String, List<String>> myMap;
Then in application.properties file write it like this:
myMap={'key1':{'value1', 'value2'}, 'key2':{'value1', 'value2', 'value3'}}
I have a following yml config:
foo:
bar.com:
a: b
baz.com:
a: c
With a following class Spring tries to inject map with keys 'bar' and 'baz', treating dots as separator:
public class JavaBean {
private Map<String, AnotherBean> foo;
(...)
}
I have tried quoting the key (i.e. 'bar.com' or "bar.com") but to no avail - still the same problem. Is there a way around this?
A slight revision of #fivetenwill's answer, which works for me on Spring Boot 1.4.3.RELEASE:
foo:
"[bar.com]":
a: b
"[baz.com]":
a: c
You need the brackets to be inside quotes, otherwise the YAML parser basically discards them before they get to Spring, and they don't make it into the property name.
This should work:
foo:
"[bar.com]":
a: b
"[baz.com]":
a: c
Inspired from Spring Boot Configuration Binding Wiki
This is not possible if you want automatic mapping of yaml keys to Java bean attributes. Reason being, Spring first convert YAML into properties format.
See section 24.6.1 of link below:
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html
So, your YAML is converted to:
foo.bar.com.a=b
foo.baz.com.a=c
Above keys are parsed as standard properties.
As a work around, you can use Spring's YamlMapFactoryBean to create a Yaml Map as it is. Then, you can use that map to create Java beans by your own.
#Configuration
public class Config {
private Map<String, Object> foo;
#Bean
public Map<String, Object> setup() {
foo = yamlFactory().getObject();
System.out.println(foo); //Prints {foo={bar.com={a=b}, baz.com={a=c}}}
return foo;
}
#Bean
public YamlMapFactoryBean yamlFactory() {
YamlMapFactoryBean factory = new YamlMapFactoryBean();
factory.setResources(resource());
return factory;
}
public Resource resource() {
return new ClassPathResource("a.yaml"); //a.yaml contains your yaml config in question
}
}
I have a following yml config:
foo:
bar.com:
a: b
baz.com:
a: c
With a following class Spring tries to inject map with keys 'bar' and 'baz', treating dots as separator:
public class JavaBean {
private Map<String, AnotherBean> foo;
(...)
}
I have tried quoting the key (i.e. 'bar.com' or "bar.com") but to no avail - still the same problem. Is there a way around this?
A slight revision of #fivetenwill's answer, which works for me on Spring Boot 1.4.3.RELEASE:
foo:
"[bar.com]":
a: b
"[baz.com]":
a: c
You need the brackets to be inside quotes, otherwise the YAML parser basically discards them before they get to Spring, and they don't make it into the property name.
This should work:
foo:
"[bar.com]":
a: b
"[baz.com]":
a: c
Inspired from Spring Boot Configuration Binding Wiki
This is not possible if you want automatic mapping of yaml keys to Java bean attributes. Reason being, Spring first convert YAML into properties format.
See section 24.6.1 of link below:
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html
So, your YAML is converted to:
foo.bar.com.a=b
foo.baz.com.a=c
Above keys are parsed as standard properties.
As a work around, you can use Spring's YamlMapFactoryBean to create a Yaml Map as it is. Then, you can use that map to create Java beans by your own.
#Configuration
public class Config {
private Map<String, Object> foo;
#Bean
public Map<String, Object> setup() {
foo = yamlFactory().getObject();
System.out.println(foo); //Prints {foo={bar.com={a=b}, baz.com={a=c}}}
return foo;
}
#Bean
public YamlMapFactoryBean yamlFactory() {
YamlMapFactoryBean factory = new YamlMapFactoryBean();
factory.setResources(resource());
return factory;
}
public Resource resource() {
return new ClassPathResource("a.yaml"); //a.yaml contains your yaml config in question
}
}