动态更新在 Spring 中用 @value 注释的字段。

huangapple go评论71阅读模式
英文:

Dynamically update the @value annotated fields in spring

问题

我正试图动态更新应用程序中带有 @value 注解的字段。

首先,该应用程序具有自定义的属性源,其源是一个 Map<Object, String>
在一个定时器被启用以在一分钟间隔后更新这些值。

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 CustomPropertySource extends EnumerablePropertySource<Map<String, Object>> {

    public CustomPropertySource(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() {
        return StringUtils.toStringArray(this.source.keySet());
    }

    @Override
    public Object getProperty(String name) {
        return this.source.get(name);
    }
}

属性源的初始值 Map<String, Object>PropertySourceLocator 提供。(这并非真实情况,我只是尝试重新创建这里使用的逻辑)

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 CustomPropertySource("custom_source", source);
    }
}

在下面的 RestController 类中,我使用 @Value 注入这些属性。
environment.getProperty("prop1"); 正在提供更新的值,但 @value 注解的字段没有更新。
我还尝试使用 environment.propertySources().addFirst 方法注入一个新的属性源 updatedMap,以便它优先于其他属性源。但这个努力也没有成功。如果有任何线索,将不胜感激。

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("*******************************************************");
            }
        });

        return propertyOne;
    }

    @GetMapping("/p2")
    private Long getProp2() {
        System.out.println("~~~~>" + environment.getPropertySources());

        return propertyTwo;
    }

    @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();
    }
}

如果您认为这不是将值注入到 RestController 的正确方式,请告诉我。我接受所有可能的替代建议和最佳实践。

英文:

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&lt;Object, String&gt;.
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&lt;Map&lt;String, Object&gt;&gt; {
public CustomPropertySorce(String name, Map&lt;String, Object&gt; source) {
super(name, source);
new java.util.Timer().schedule(new java.util.TimerTask() {
@Override
public void run() {
source.put(&quot;prop1&quot;, &quot;yoyo-modified&quot;);
source.put(&quot;prop2&quot;, new Date().getTime());
System.out.println(&quot;Updated Source :&quot; + 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&lt;String, Object&gt; 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&lt;?&gt; locate(Environment environment) {
Map&lt;String, Object&gt; source=new HashMap&lt;String,Object&gt;(){{put(&quot;prop1&quot;,&quot;yoyo&quot;);put(&quot;prop2&quot;,new Date().getTime());}};
return new CustomPropertySorce(&quot;custom_source&quot;,source);
}
}

RestController class where I inject these properties using @Value is given below.
environment.getProperty(&quot;prop1&quot;); 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(&quot;${prop1}&quot;)
private String propertyOne;
@Value(&quot;${prop2}&quot;)
private Long propertyTwo;
@Autowired
private ConfigurableEnvironment environment;
@GetMapping(&quot;/p1&quot;)
private String getProp1() {
System.out.println(&quot;~~~~&gt;&quot;+environment.getPropertySources());
environment.getPropertySources().forEach(ps -&gt; {
if(ps.containsProperty(&quot;prop1&quot;) || ps.containsProperty(&quot;prop2&quot;)) {
System.out.println(&quot;*******************************************************&quot;);
System.out.println(ps.getName());
System.out.println(ps.getProperty(&quot;prop1&quot;));
System.out.println(ps.getProperty(&quot;prop2&quot;));
System.out.println(&quot;*******************************************************&quot;);
}
});
//		env.get
return propertyOne;
//		return environment.getProperty(&quot;prop1&quot;);
}
@GetMapping(&quot;/p2&quot;)
private Long getProp2() {
System.out.println(&quot;~~~~&gt;&quot;+environment.getPropertySources());
//		env.get
return propertyTwo;
//		return environment.getProperty(&quot;prop1&quot;);
}
@GetMapping(&quot;/update&quot;)
public String updateProperty() {
Map&lt;String, Object&gt; updatedProperties = new HashMap&lt;&gt;();
updatedProperties.put(&quot;prop1&quot;, &quot;Property one modified&quot;);
MapPropertySource mapPropSource = new MapPropertySource(&quot;updatedMap&quot;, 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.

答案1

得分: 2

谢谢 @flaxel。我使用了 @RefreshScope 来解决这个问题。
如果这个解决方案对有同样问题的人有帮助,我在这里发布一下。

在这个特定的情况下,我在我的控制器上应用了 @RefreshScope,以便刷新带有新值的 bean。

在应用 @RefreshScope 到你的 bean 之前,你可以在这个链接中查看。

Spring Boot 的执行器实现了这个刷新机制。所以为了让这个工作,你必须在你的类路径中有执行器。

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: "${springboot_version}"

然后,就像之前讨论的那样,将 RefreshScope 添加到需要刷新的 bean 上。

最后,调用 actuator/refresh 端点来触发刷新。

如果你想以编程的方式执行这个操作,可以通过 AutowireRefreshEndpoint 类的实例注入到你的 bean 中,并在其中调用 refresh() 方法。
【注意:你不必严格遵循这种方法,我只是给出一个提示,说明可以进行注入操作】

@RefreshScope
@RestController
public class DataController {
  @Value("${prop1}")
  private String prop1;
  
  @Autowired
  private RefreshEndpoint refreshEndpoint;
  
  @GetMapping("/p1")
  public String getProp1(){
    return prop1;
  }
  
  @GetMapping("/refresh")
  public void refresh(){
    refreshEndpoint.refresh();
  }
}

**************** 更多(如果你正在开发一个库) ********************

如果你正在开发一个库,你需要从当前的 ApplicationContext 中获取 RefreshEndpoint 实例怎么办?

简单地将 RefreshEndpoint 进行自动注入可能会得到一个空引用。相反,你可以通过下面给出的方法获取当前的 ApplicationContext。然后使用 ApplicationContext 获取 RefreshEndpoint 实例,并在上面调用 refresh() 方法。

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();
    }
}

最后,将这个类添加到 spring.factories 中,以便被 Spring 调用。

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.x.y.z.LocalApplicationContextFetcher
英文:

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: &#39;org.springframework.boot&#39;, name: &#39;spring-boot-starter-actuator&#39;, version: &quot;${springboot_version}&quot;

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(&quot;${prop1}&quot;)
private String prop1;
@Autowired
private RefreshEndpoint refreshEndpoint;
@GetMapping(&quot;/p1&quot;)
public String getProp1(){
return prop1;
}
@getMappig(&quot;/refresh&quot;)
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&lt;ConfigurableApplicationContext&gt; {
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

huangapple
  • 本文由 发表于 2020年9月28日 18:36:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/64100539.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定