How to inject Maps using Spring #Value - java

We are using Spring Boot 2.
We want to use the refresh mechanism of Spring Boot, but due to a bug we can't use #Configuration, hence we are forced to replace all theses by #Value in combination with #RefreshScope.
So we used that:
#Configuration
#RefreshScope
public class MyConfig {
#Value("${myMap}")
private Map<String, String> myMap;
}
With that YAML file for example:
myMaps:
key1: Value
key2: Another Value
But we get an error out of it:
Error creating bean with name 'scopedTarget.myMap': Unsatisfied dependency expressed through field 'mapMap'; nested exception is org.springframework.beans.ConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'java.util.Map'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.util.Map': no matching editors or conversion strategy found
We found already other thread with a similar topic:
How to fill HashMap from java property file with Spring #Value
How to inject a Map using the #Value Spring Annotation?
But we can't use other kind of property bindings, as we are deploying this app into kubernetes and use the config maps of kubernetes.
So we wonder, if there is any other chance to get #Value working together with Map

Instead of: #Value("${myMap}"), you need to write like this #Value("#{${myMap}}").
'#' treat whats in the curly bracket after it as Spring Expression Language(SpEL).

I don't think Kubernetes should prevent you from using #ConfigurationProperties (example here) with #RefreshScope. (Or is something else blocking you from using this?)
You could set up a configmap containing your properties file and mount that as a volume.
How you trigger a refresh is another question (if you need hot loading instead of changing your configmap together with your app). One option is to use spring-cloud-kubernetes which uses a similar approach and has multiple reload-triggering options. I'd expect you could use #ConfigurationProperties just like in the example for that project. Alternatively, it's also possible to watch for changes but that would require introducing more code.

I found something, that might solve my problem, but perhaps there is a better solution than this:
Based on org.springframework.cloud.logging.LoggingRebinder.setLogLevels(LoggingSystem, Environment)
private static final Bindable<Map<String, String>> STRING_STRING_MAP = Bindable
.mapOf(String.class, String.class);
protected void setLogLevels(LoggingSystem system, Environment environment) {
Map<String, String> levels = Binder.get(environment)
.bind("logging.level", STRING_STRING_MAP).orElseGet(Collections::emptyMap);
for (Entry<String, String> entry : levels.entrySet()) {
setLogLevel(system, environment, entry.getKey(), entry.getValue().toString());
}
}

Related

Spring Boot 2.0 - Map configuration loading another defined configuration into the map

I am using Spring Boot 2.0.
Below are the two defined configurations:
environment: dev
map:
keyA : valA
keyB : valB
If I have this code by itself
#Configuration
public class MapConfig {
#Bean
#ConfigurationProperties("map")
Map<String, String> map() { return new HashMap<>(); }
}
The map is generated perfectly fine. When enumerating over the map, I get {keyA:valA} and {keyB:valB}.
Now, if I include this piece of code
#Configuration
#EnableAspectJAutoProxy
#ComponentScan("{aspect.package}")
public class AppConfig {
#Value("${environment:dev}")
String appEnvironment;
#Bean
String appEnvironment() { return appEnvironment; }
}
Then, suddenly, my map is now populated with only {appEnvironment:dev}.
Originally, the Bean in MapConfig was part of AppConfig. I separated it out since I thought that might have been the cause, but issue remains. The issue is also with the appEnvironment bean itself. If I comment out those four lines of code, everything would work fine again.
EDIT: As far as I can figure out, it looks to be some weird interaction with Bean that returns String. If I add more Bean that returns String, the map continues to be populated with those Beans. Beans that return other Object types don't contribute to this issue.
For now, I have moved away from these String Bean, since I can obtain them directly with #Value anyway. If anyone have an explanation for this, I would like to hear about it.

Springboot - injection from application.yml depending method name

I refered Spring Boot - inject map from application.yml for injecting map from application.yml file
My application.yml snippet is below
easy.app.pairMap:
test1: 'value1'
test2: 'value2'
Properties file is like below
#Component
#Configuration
#ConfigurationProperties("easy.app")
#EnableConfigurationProperties
public class TestProperties {
private Map<String, String> pairMap= new HashMap<String, String>();
public void setPairMap(Map<String, String> pairMap) {
this.pairMap= pairMap;
}
}
But , I found that the value injection happens only when setters and getters are in proper format.ie getPairMap and setPairMap. Not when using getPairs or SetPairs. What is the reason for this behaviour
Spring takes your property full name easy.app.pairMap find ConfigurationProperties by prefix easy.app and then it try to find setter with name setPairMap, it takes property name pairMap and "converts" it to setPairMap.
If you create method setPairs property name should be like easy.app.pairs.
To bind to properties by using Spring Boot’s Binder utilities (which is what #ConfigurationProperties does), you need to have a property in the target bean and you either need to provide a setter or initialize it with a mutable value.
How does Spring can understand that it needs to use SetPairs method to set your pairMap property? There is convention for naming of getters and setters and you should follow this convention if you want everything to work.

Does Spring's Environment Abstraction use PropertyEditors?

My google-fu is failing me on this one.
I'm using Spring 4.2.4.RELEASE with java configuration. What I'm trying to do is register a custom property editor to convert from a String to a Map.
So I have a Java Configuration class that registers the appropriate BeanFactoryPostProcessor
#Bean
public static CustomEditorConfigurer customEditorConfigurer(){
CustomEditorConfigurer configurer = new CustomEditorConfigurer();
Map<Class<?>, Class<? extends PropertyEditor>> customEditors = new HashMap<>();
customEditors.put(Map.class, DelimitedStringToMapPropertyEditor.class);
configurer.setCustomEditors(customEditors);
return configurer;
}
In the same configuration class I am also injecting the Environment
#Resource
private Environment environment;
However, when I try to get the String property (which is also unfortunately named environment) that I want converted to a map, I get an exception.
environment.getProperty("environment", Map.class, Collections.EMPTY_MAP)
Exception:
Caused by: java.lang.IllegalArgumentException: Cannot convert value [VAR=hello] from source type [String] to target type [Map]
at org.springframework.core.env.PropertySourcesPropertyResolver.getProperty(PropertySourcesPropertyResolver.java:94)
at org.springframework.core.env.PropertySourcesPropertyResolver.getProperty(PropertySourcesPropertyResolver.java:65)
at org.springframework.core.env.AbstractPropertyResolver.getProperty(AbstractPropertyResolver.java:143)
at org.springframework.core.env.AbstractEnvironment.getProperty(AbstractEnvironment.java:546)
BUT if I inject the property directly using the #Value annotation, it works just fine.
#Value("${environment}")
private Map<String, String> shellEnvironment;
So, what gives? Does the Environment object not take into account registered property editors? Do I need to create a custom Converter? Isn't the Environment abstraction the latest and greatest way to resolve properties that come from anywhere?
No; the Environment abstraction does not use registered Property Editors. It only resolves properties (and does basic Type Conversion).
Thanks to the comments from #M. Deinum I was able to come to a decent compromise.
Correct it is a way to resolve properties it isn't a way to convert properties. When using the #Value 2 things happen, first the property is resolved, second conversion is attempted (only basic conversion String to numbers/booleans is done through plain java not converters/editors.). You only do the resolution part not the conversion part. – M. Deinum
You have to kind of read between the lines in the reference manual to come to this understanding.
The Environment Abstraction chapter states:
The role of the Environment object with relation to properties is to provide the user with a convenient service interface for configuring property sources and resolving properties from them.
Notice it does not say anything about conversion/editors. (Although I did notice that it does actually handle simple Type Conversion because Environment inherits PropertyResolver.getPropertyAsClass)
So I decided to combine the two methods and settled on this method of injecting my properties into my configuration class:
#Resource
private Environment environment;
#Value("#{environment['environment']?:{:}}")
private Map<String, String> shellEnvironment;

Spring Boot YAML config and list

I have to integrate a list in a YAML config file in Spring Boot, and don't see how to proceed.
I already saw other questions related : Spring Boot yaml configuration for a list of strings
And have the same issue.
I applied the solution and worked around, and found the solution a little tricky.
Is there a way to make lists work with the #Value ?
And if not now, is it expected in future ?
Thanks a lot.
According to this documentation you can do a list in yaml.
http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-yaml
YAML lists are represented as property keys with [index] dereferencers, for example this YAML:
my:
servers:
- dev.bar.com
- foo.bar.com
Would be transformed into these properties:
my.servers[0]=dev.bar.com
my.servers[1]=foo.bar.com
To bind to properties like that using the Spring DataBinder utilities (which is what #ConfigurationProperties does) you need to have a property in the target bean of type java.util.List (or Set) and you either need to provide a setter, or initialize it with a mutable value, e.g. this will bind to the properties above
#ConfigurationProperties(prefix="my")
public class Config {
private List<String> servers = new ArrayList<String>();
public List<String> getServers() {
return this.servers;
}
}
https://www.youtube.com/watch?v=d6Scea1JdMg&t=9s
Please refer to the above link. Maybe it helps, shown how to read different data types in application.yml in the Spring Boot.
There is a related GitHub thread: #Value annotation should be able to inject List from YAML properties. The issue has been closed, and according to a comment in a duplicate issue, they're not considering implementing the support now. It will be reopened once they decide to work on it.
Until then, you can go the way described in #mark's answer, using #ConfigurationProperties, also mentioned on GitHub.

Can a Spring FactoryBean get access to all of the properties in the context?

I'm trying to write a Spring FactoryBean that will produce a list of Request objects. The number of requests and the values that go into them are configurable at runtime, so I want to use properties for these.
Each request comprises of a pair of ID values, so I need some way of providing the Factory Bean with a configurable list of these ID pairs (Call them A and B for now).
What I've got so far is to use a property that looks something like:
requests=1/2,3/4,5/6
which then defines three requests, one with A=1 and B=2, one with A=3 and B=4, and one with A=5 and B=6.
This is obviously a bit nasty to configure, and rather prone to errors. What would be much nicer would be to do something with the values split out over many properties, so the above could be something like:
requests.1.A=1
requests.1.B=2
requests.2.A=3
requests.2.B=4
requests.3.A=5
requests.3.B=6
Which just makes it a bit more obvious what is going on.
However, I can't find any way of having my FactoryBean configured to access all of the available properties, instead of just the specifically named property that is passed in from the context.
Am I missing something here? Or - even better - is there a better way of doing this kind of config that is easier supported and maintained?
You can inject an Environment bean into your FactoryBean instance, it is provided by the context and you do not have to configure it. I am not sure how you are configuring your beans, but I always favor Java config. So this example will use Java config.
#Configuration
class FactoryBeanConfig {
#Bean
public FactoryBean(final Environment env) {
return new MyFactoryBean(env);
}
}
The Environment instance will give you access to all the properties, because it is a PropertyResolver You can programmatically loop over the properties
int x = 1;
while(true) {
env.getRequiredProperty("requests." + x + ".A")
env.getRequiredProperty("requests." + x + ".B")
}
If you want to use that to create instances of a specific bean I would suggest using a PropertiesBeanDefinitionReader. Specify 2 property files to load (one well known to configure the defaults) and one to add beans.
core-request.properties
request.(class)=com.company.pkg.Request
requests.properties
requests1.A=1
requests1.B=2
requests2.A=3
requests2.B=4
requests3.A=5
requests3.B=6
Now you can use your FactoryBean to construct a BeanFactory and obtain all the beans and expose them as a list.
public class YourFactoryBean implements FactoryBean<List<Request>> {
#Autowired
private ResourceLoader rl;
public Object getObject() {
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory;
PropertiesBeanDefinitionLoader loader = new PropertiesBeanDefinitionLoader(beanRegistry);
Resource core = rl.getResource(core-location);
Resource requests = rl.getResource(requests-location);
loader.loadBeanDefinitions(core);
loader.setDefaultParentBean("request");
loader.loadBeanDefinitions(requests);
return beanRegistry.getBeansOfType(Request.class).values();
}
}
Something like that.

Categories