I am trying to dynamically update the @value
annotated fields in my application.
First of all, this application has a custom property source, with source being a Map<Object, String>
.
A timer is enabled to update the values after a minute interval.
package com.test.dynamic.config; import java.util.Date; import java.util.Map; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.util.StringUtils; public class CustomPropertySorce extends EnumerablePropertySource<Map<String, Object>> { public CustomPropertySorce(String name, Map<String, Object> source) { super(name, source); new java.util.Timer().schedule(new java.util.TimerTask() { @Override public void run() { source.put("prop1", "yoyo-modified"); source.put("prop2", new Date().getTime()); System.out.println("Updated Source :" + source); } }, 60000); } @Override public String[] getPropertyNames() { // TODO Auto-generated method stub return StringUtils.toStringArray(this.source.keySet()); } @Override public Object getProperty(String name) { // TODO Auto-generated method stub return this.source.get(name); } }
Initial values of source Map<String, Object>
is supplied from the PropertySourceLocator
. (This is not the real scenario, but I am trying to recreate the logic used here)
package com.test.dynamic.config; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.springframework.cloud.bootstrap.config.PropertySourceLocator; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; public class CustomPropertySourceLocator implements PropertySourceLocator { @Override public PropertySource<?> locate(Environment environment) { Map<String, Object> source=new HashMap<String,Object>(){{put("prop1","yoyo");put("prop2",new Date().getTime());}}; return new CustomPropertySorce("custom_source",source); } }
RestController
class where I inject these properties using @Value
is given below.
environment.getProperty("prop1");
is supplying updated value, but not the @value
annotated fields.
I also tried to inject a new property source updatedMap
using the addFirst
method of environment.propertySources()
assuming that it will take precedence over the others. But that effort also went futile. any clue is much appreciated.
package com.test.dynamic.config.controller; import java.util.HashMap; import java.util.Map; import javax.annotation.Resource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class DataController { @Value("${prop1}") private String propertyOne; @Value("${prop2}") private Long propertyTwo; @Autowired private ConfigurableEnvironment environment; @GetMapping("/p1") private String getProp1() { System.out.println("~~~~>"+environment.getPropertySources()); environment.getPropertySources().forEach(ps -> { if(ps.containsProperty("prop1") || ps.containsProperty("prop2")) { System.out.println("*******************************************************"); System.out.println(ps.getName()); System.out.println(ps.getProperty("prop1")); System.out.println(ps.getProperty("prop2")); System.out.println("*******************************************************"); } }); // env.get return propertyOne; // return environment.getProperty("prop1"); } @GetMapping("/p2") private Long getProp2() { System.out.println("~~~~>"+environment.getPropertySources()); // env.get return propertyTwo; // return environment.getProperty("prop1"); } @GetMapping("/update") public String updateProperty() { Map<String, Object> updatedProperties = new HashMap<>(); updatedProperties.put("prop1", "Property one modified"); MapPropertySource mapPropSource = new MapPropertySource("updatedMap", updatedProperties); environment.getPropertySources().addFirst(mapPropSource); return environment.getPropertySources().toString(); } }
If you think this is not the right way of injecting values to a RestController
, please let me know. All possible alternate suggestions/best practices are accepted.
Advertisement
Answer
Thank you @flaxel
. I used @RefreshScope
to resolve this issue.
Posting the solution here if it helps someone with the same query.
In this particular case, I applied @RefreshScope
on my Controller to refresh the bean with new values.
You can refer to this link before applying @RefreshScope
to your bean.
It is the spring boot actuator that facilitates this refresh mechanism. So in order for this to work, you must have actuator in your classpath.
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: "${springboot_version}"
Then as discussed earlier, add RefreshScope
to the bean that needs to be refreshed.
Finally, invoke the actuator/refresh
endpoint to trigger the refresh.
If you want to programmatically do it, Autowire an instance of RefreshEndpoint
class to your bean and invoke the refresh()
method in it.
[Note: You don’t have to strictly follow this approach, but I am giving a clue that it can be Autowired]
@RefreshScope @RestController public class DataController { @Value("${prop1}") private String prop1; @Autowired private RefreshEndpoint refreshEndpoint; @GetMapping("/p1") public String getProp1(){ return prop1; } @getMappig("/refresh") public void refresh(){ refreshEndpoint.refresh(); } }
**************** MORE (if you are developing a library) ********************
What if you are developing a library and you have to get the RefreshEndpoint
instance from the current ApplicationContext
?
Simply Autowiring RefreshEndpoint
may give you a null reference. Instead, you can get hold of the current ApplicationContext
by the method given below. And use the ApplicationContext
to get the RefreshEndpoint
instance to invoke the refresh()
method on it.
public class LocalApplicationContextFetcher implements ApplicationContextInitializer<ConfigurableApplicationContext> { private static ApplicationContext ctx; @Override public void initialize(ConfigurableApplicationContext applicationContext) { ctx = applicationContext; } public static ApplicationContext getCtx() { return ctx; } public static void refresh(){ ctx.getBean(RefreshEndpoint.class).refresh(); } }
Finally, add this class to the spring.factories
to get invoked by spring.
org.springframework.cloud.bootstrap.BootstrapConfiguration= com.x.y.z.LocalApplicationContextFetcher