2

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:

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

Michael D.
  • 71
  • 2
  • 6

3 Answers3

3

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

Vijay Chauhan
  • 159
  • 1
  • 7
0

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.

Ryan Dawson
  • 11,832
  • 5
  • 38
  • 61
  • Hello, there is a bug, so the `@ConfigurationProperties` are only updated once and no second or third time. So we can't use `@ConfigurationProperties`. Hence we are forced to use `@Configuration` and `@Value`. So we wonder, how we get `@Value` working in combination with `Map`. Caused by the usage of kubernetes, we can't use the workaround described in https://stackoverflow.com/questions/28369458/how-to-fill-hashmap-from-java-property-file-with-spring-value and https://stackoverflow.com/questions/30691949/how-to-inject-a-map-using-the-value-spring-annotation . – Michael D. May 16 '18 at 06:54
  • Is that bug about properties being updated only once raised in github? Are you referring to a spring boot bug? I don't see what specifically it is that Kubernetes is preventing you from doing. For example, how does it prevent you from using the PropertySplitter workaround (https://stackoverflow.com/questions/28369458/how-to-fill-hashmap-from-java-property-file-with-spring-value)? – Ryan Dawson May 16 '18 at 09:27
  • No we didn't raised this bug yet, unfortunately. The splitter workaround does not work with the YAML structure above. – Michael D. May 16 '18 at 13:04
  • Ok, I see. You could equally use .properties (inc for kub) but I can understand that you might want to use yaml. I've not tried but I'd think you could use @Resource like in https://stackoverflow.com/a/30692388/9705485 with yaml. That example doesn't seem to be boot but I'd think similar could be done with boot (e.g. https://github.com/Activiti/activiti-cloud-service-common/blob/c1bb2172a30400df385f5fcf85308806705cb589/activiti-cloud-services-identity-basic/src/main/java/org/activiti/cloud/services/identity/basic/SecurityConfig.java#L39) – Ryan Dawson May 16 '18 at 13:20
  • As we get the information provided by kubernetes (hence it's just a `PropertySource` in `Environment`), we are not able to use `@Resource`, I think. But we found out about the new `Binder` of Spring Boot, that solved our issue: https://stackoverflow.com/a/50371700/7919291 – Michael D. May 17 '18 at 08:16
0

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());
    }
}
Michael D.
  • 71
  • 2
  • 6