Dynamically update the @value annotated fields in spring

Tags: , ,



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.

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


Source: stackoverflow