jiaowojiangge 发表于 2024-1-28 11:20

java动态刷新@Value

> 前言:在工作中需要实现动态刷新@Value字段值,但是项目使用的是Spring boot,又不想接入Nacos/Apollo等配置中心组件(额外增加成本),并且项目中只是简单的使用@Value读取一些配置信息进行使用(或者判断状态),所以写了简单的一个刷新机制,通过接口刷新@Value值,有需要的可以直接使用

##### 一、自定义注解,用来需要修饰需要支持动态刷新的场景
import java.lang.annotation.*;
```
import java.lang.annotation.*;

/**
* @annotationName RefreshValue
* @description自定义注解,用来需要修饰需要支持动态刷新的场景
**/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshValue {
}
```
##### 二、定义一个工具类实现配置转换
```
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
* @className RefreshEnvironment
* @description定义一个工具类实现配置转换
**/
@Component
public class RefreshEnvironmentimplements EnvironmentAware {

    private static ConfigurableEnvironment environment;

    public static void updateValue(String key, Object newValue){
      //自定的义配置文件名称
      MutablePropertySources propertySources1 = environment.getPropertySources();
      propertySources1.stream().forEach(x -> {
            if (x instanceof MapPropertySource){
                MapPropertySource propertySource = (MapPropertySource) x;
                if (propertySource.containsProperty(key)){
                  String proname = propertySource.getName();
                  Map<String, Object> source = propertySource.getSource();
                  Map<String, Object> map = new HashMap<>(source.size());
                  map.putAll(source);
                  map.put(key, newValue);
                  environment.getPropertySources().replace(proname, new MapPropertySource(proname, map));
                }
            }
      });

    }

    @Override
    public void setEnvironment(Environment environment) {
      RefreshEnvironment.environment = (ConfigurableEnvironment) environment;
    }
}
```
##### 三、找到需要刷新的配置变量
```
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.PropertyPlaceholderHelper;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* @className AnoValueRefreshPostProcessor
* @description找到需要刷新的配置变量
**/
@Component
public class AnoValueRefreshPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements EnvironmentAware {
    private Map<String, List<FieldPair>> mapper = new HashMap<>();
    private Environment environment;

    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
      processMetaValue(bean);
      return super.postProcessAfterInstantiation(bean, beanName);
    }

    /**
      * description 主要目的就是获取支持动态刷新的配置属性,然后缓存起来
   **/
    private void processMetaValue(Object bean){
      Class<?> aClass = bean.getClass();
      if (!aClass.isAnnotationPresent(RefreshValue.class)){
            return;
      }
      try {
            for (Field field: aClass.getDeclaredFields()) {
                if (field.isAnnotationPresent(Value.class)){
                  Value val = field.getAnnotation(Value.class);
                  List<String> keyList = pickPropertyKey(val.value(), 0);
                  for (String key : keyList) {
                        mapper.computeIfAbsent(key, (k) -> new ArrayList<>())
                              .add(new FieldPair(bean, field, val.value()));
                  }
                }
            }
      } catch (SecurityException e) {
            e.printStackTrace();
            System.exit(-1);
      }

    }

    /**
      * description 实现一个基础的配置文件参数动态刷新支持
   **/
    private List<String> pickPropertyKey(String value, int begin){
      int start = value.indexOf("${", begin) + 2;
      if (start < 2){
            return new ArrayList<>();
      }
      int middle = value.indexOf(":", start);
      int end = value.indexOf("}", start);

      String key;
      if (middle > 0 && middle < end){
            key = value.substring(start, middle);
      } else {
            key = value.substring(start, end);
      }

      List<String> keys = pickPropertyKey(value, end);
      keys.add(key);
      return keys;
    }


    @Override
    public void setEnvironment(Environment environment) {
      this.environment = environment;
    }

    /**
      * description 消费事件,接收到变更事件通过key从缓存中找到需要变更的Field,然后依次刷新
   **/
    @EventListener
    public void updateConfig(ConfigUpdateEvent configUpdateEvent){
      List<FieldPair> list = mapper.get(configUpdateEvent.key);
      if (!CollectionUtils.isEmpty(list)){
            list.forEach(f -> f.updateValue(environment));
      }
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class FieldPair{
      private static PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}",":", true);
      Object bean;
      Field field;
      String value;

      public void updateValue(Environment environment){
            boolean accessible = field.isAccessible();
            if (!accessible){
                field.setAccessible(true);
            }

            String updateVal = propertyPlaceholderHelper.replacePlaceholders(value, environment::getProperty);
            try {
                if (field.getType() == String.class){
                  field.set(bean, updateVal);
                }else {
                  field.set(bean, JSONObject.parseObject(updateVal, field.getType()));
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            field.setAccessible(accessible);
      }
    }

    /**
      * description 选择事件机制来实现同步,借助spring Event来完成(自定义事件类)
   **/
    public static class ConfigUpdateEvent extends ApplicationEvent {
      String key;

      public ConfigUpdateEvent(Object source, String key) {
            super(source);
            System.out.println("---" + source);
            this.key = key;
      }
    }

}
```
##### 四、使用接口修改配置
```
import com.tl.iedb.config.testTwo.AnoValueRefreshPostProcessor;
import com.tl.iedb.config.testTwo.RefreshEnvironment;
import com.tl.iedb.util.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
* @className TestOne
* @description 修改配置接口
**/
@Slf4j
@RestController
@RequestMapping(value = "/test")
@Api(value = "测试接口")
public class TestOne {
    @Autowired
    ApplicationContext applicationContext;

    @Autowired
    ConfigurableEnvironment environment;

    @ApiOperation(value = "自定义修改配置")
    @PostMapping("/update")
    public ResponseResult calcHistoryUseEle(String name, String value){
      System.out.println("参数名 name = " + name);
      System.out.println("参数 value = " + value);

      // 配置转换
      RefreshEnvironment.updateValue(name, value);

      applicationContext.publishEvent(new AnoValueRefreshPostProcessor.ConfigUpdateEvent(this, name));

      return ResponseResult.ok();
    }
}
```
##### 五、 接口验证
```
import com.tl.iedb.config.testTwo.RefreshValue;
import com.tl.iedb.util.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
* @className ProxyMetaDefinitionController
* @description 验证@Value值是否修改
**/
@Slf4j
@RestController
@RequestMapping(value = "/tem")
@Api(value = "临时开放接口")
@RefreshValue
public class ProxyMetaDefinitionController {


    @Value("${source.flag}")
    private boolean enable;
    @Value("${db.token}")
    private String token;
    @Value("${isopen.open}")
    private String open;


    @ApiOperation(value = "自定义测试")
    @PostMapping("/calc/test")
    public ResponseResult calcHistoryUseEle(){

      System.out.println("---token---" + token);
      System.out.println("---enable---" + enable);
      System.out.println("---open---" + open);

      return ResponseResult.ok();

    }
}
```
以上实现了简单的临时修改@Value值的功能,适合在项目中有一些配置变化,又不想重启项目的时候使用

199556 发表于 2024-1-29 10:43

invadeo 发表于 2024-1-29 08:29
RefreshValue注解写错了吧

平台识别错误,他复制进来的时候是正确的,但是平台给改啦,需要把

NineSu 发表于 2024-11-21 16:57

好方法!
但是有个场景如何更新。
比如线程池配置,提供配置文件读取配置线程池大小,队列大小,拒绝策略等信息。然后启动的时候根据配置创建线程池。
这种情况下,如果动态更新了配置,已经创建的线程池不会修改对应的属性的吧。这种问题有方案吗。

urdarling 发表于 2024-1-28 16:53

可以 比我写得好 收藏了

yuxuan1311 发表于 2024-1-28 18:34

还能这样操作啊

reinmj 发表于 2024-1-28 19:25

这个在一些课程看过,挺实用的

TheKingOfKiller 发表于 2024-1-28 20:31

很棒,支持,有学到了

abc814360602 发表于 2024-1-28 22:06

写个@Aspect切面{:1_918:}

moruye 发表于 2024-1-28 22:08

turmasi1234 发表于 2024-1-29 07:35

感谢分享,这个是用起来很方便

invadeo 发表于 2024-1-29 08:29

RefreshValue注解写错了吧

TearApart 发表于 2024-1-29 08:55

abc814360602 发表于 2024-1-28 22:06
写个@Aspect切面

大佬用切面可以怎么实现
页: [1] 2 3
查看完整版本: java动态刷新@Value