• SpringBoot如何动态更新yml文件?


    前言

    在系统运行过程中,可能由于一些配置项的简单变动需要重新打包启停项目,这对于在运行中的项目会造成数据丢失,客户操作无响应等情况发生,针对这类情况对开发框架进行升级提供yml文件实时修改更新功能

    项目依赖

    项目基于的是2.0.0.RELEASE版本,所以snakeyaml需要单独引入,高版本已包含在内

    1. <dependency>
    2. <groupId>org.yamlgroupId>
    3. <artifactId>snakeyamlartifactId>
    4. <version>1.23version>
    5. dependency>

    网上大多数方法是引入spring-cloud-context配置组件调用ContextRefresher的refresh方法达到同样的效果,考虑以下两点未使用

    • 开发框架使用了logback日志,引入spring-cloud-context会造成日志配置读取错误
    • 引入spring-cloud-context会同时引入spring-boot-starter-actuator组件,会开放一些健康检查路由及端口,需要框架安全方面进行额外控制

    YML文件内容获取

    读取resource文件下的文件需要使用ClassPathResource获取InputStream

    1. public String getTotalYamlFileContent() throws Exception {
    2. String fileName = "application.yml";
    3. return getYamlFileContent(fileName);
    4. }
    5. public String getYamlFileContent(String fileName) throws Exception {
    6. ClassPathResource classPathResource = new ClassPathResource(fileName);
    7. return onvertStreamToString(classPathResource.getInputStream());
    8. }
    9. public static String convertStreamToString(InputStream inputStream) throws Exception{
    10. return IOUtils.toString(inputStream, "utf-8");
    11. }

    YML文件内容更新

    我们获取到yml文件内容后可视化显示到前台进行展示修改,将修改后的内容通过yaml.load方法转换成Map结构,再使用yaml.dumpAsMap转换为流写入到文件

    1. public void updateTotalYamlFileContent(String content) throws Exception {
    2. String fileName = "application.yml";
    3. updateYamlFileContent(fileName, content);
    4. }
    5. public void updateYamlFileContent(String fileName, String content) throws Exception {
    6. Yaml template = new Yaml();
    7. Map yamlMap = template.load(content);
    8. ClassPathResource classPathResource = new ClassPathResource(fileName);
    9. Yaml yaml = new Yaml();
    10. //字符输出
    11. FileWriter fileWriter = new FileWriter(classPathResource.getFile());
    12. //用yaml方法把map结构格式化为yaml文件结构
    13. fileWriter.write(yaml.dumpAsMap(yamlMap));
    14. //刷新
    15. fileWriter.flush();
    16. //关闭流
    17. fileWriter.close();
    18. }

    YML属性刷新

    yml属性在程序中读取使用一般有三种

    • 使用Value注解
    1. @Value("${system.systemName}")
    2. private String systemName;
    • 通过enviroment注入读取
    1. @Autowired
    2. private Environment environment;
    3. environment.getProperty("system.systemName")
    • 使用ConfigurationProperties注解读取
    1. @Component
    2. @ConfigurationProperties(prefix = "system")
    3. public class SystemConfig {
    4. private String systemName;
    5. }

    Property刷新

    我们通过environment.getProperty方法读取的配置集合实际是存储在PropertySources中的,我们只需要把键值对全部取出存储在propertyMap中,将更新后的yml文件内容转换成相同格式的ymlMap,两个Map进行合并,调用PropertySources的replace方法进行整体替换即可

    但是yaml.load后的ymlMap和PropertySources取出的propertyMap两者数据解构是不同的,需要进行手动转换

    propertyMap集合就是单纯的key,value键值对,key是properties形式的名称,例如system.systemName=>xxxxx集团管理系统

    ymlMap集合是key,LinkedHashMap的嵌套层次结构,例如system=>(systemName=>xxxxx集团管理系统)

    • 转换方法如下
    1. public HashMap convertYmlMapToPropertyMap(Map yamlMap) {
    2. HashMap propertyMap = new HashMap();
    3. for (String key : yamlMap.keySet()) {
    4. String keyName = key;
    5. Object value = yamlMap.get(key);
    6. if (value != null && value.getClass() == LinkedHashMap.class) {
    7. convertYmlMapToPropertyMapSub(keyName, ((LinkedHashMap) value), propertyMap);
    8. } else {
    9. propertyMap.put(keyName, value);
    10. }
    11. }
    12. return propertyMap;
    13. }
    14. private void convertYmlMapToPropertyMapSub(String keyName, LinkedHashMap submMap, Map propertyMap) {
    15. for (String key : submMap.keySet()) {
    16. String newKey = keyName + "." + key;
    17. Object value = submMap.get(key);
    18. if (value != null && value.getClass() == LinkedHashMap.class) {
    19. convertYmlMapToPropertyMapSub(newKey, ((LinkedHashMap) value), propertyMap);
    20. } else {
    21. propertyMap.put(newKey, value);
    22. }
    23. }
    24. }
    • 刷新方法如下
    1. String name = "applicationConfig: [classpath:/" + fileName + "]";
    2. MapPropertySource propertySource = (MapPropertySource) environment.getPropertySources().get(name);
    3. Map source = propertySource.getSource();
    4. Map map = new HashMap<>(source.size());
    5. map.putAll(source);
    6. Map propertyMap = convertYmlMapToPropertyMap(yamlMap);
    7. for (String key : propertyMap.keySet()) {
    8. Object value = propertyMap.get(key);
    9. map.put(key, value);
    10. }
    11. environment.getPropertySources().replace(name, new MapPropertySource(name, map));

    注解刷新

    不论是Value注解还是ConfigurationProperties注解,实际都是通过注入Bean对象的属性方法使用的,我们先自定注解RefreshValue来修饰属性所在Bean的class

    通过实现
    InstantiationAwareBeanPostProcessorAdapter接口在系统启动时过滤筛选对应的Bean存储下来,在更新yml文件时通过spring的event通知更新对应

    bean的属性即可

    • 注册事件使用EventListener注解
    1. @EventListener
    2. public void updateConfig(ConfigUpdateEvent configUpdateEvent) {
    3. if(mapper.containsKey(configUpdateEvent.key)){
    4. List fieldPairList = mapper.get(configUpdateEvent.key);
    5. if(fieldPairList.size()>0){
    6. for (FieldPair fieldPair:fieldPairList) {
    7. fieldPair.updateValue(environment);
    8. }
    9. }
    10. }
    11. }
    • 通知触发事件使用ApplicationContext的publishEvent方法
    1. @Autowired
    2. private ApplicationContext applicationContext;
    3. for (String key : propertyMap.keySet()) {
    4. applicationContext.publishEvent(new YamlConfigRefreshPostProcessor.ConfigUpdateEvent(this, key));
    5. }


    YamlConfigRefreshPostProcessor的完整代码如下

    1. @Component
    2. public class YamlConfigRefreshPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements EnvironmentAware {
    3. private Map> mapper = new HashMap<>();
    4. private Environment environment;
    5. @Override
    6. public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
    7. processMetaValue(bean);
    8. return super.postProcessAfterInstantiation(bean, beanName);
    9. }
    10. @Override
    11. public void setEnvironment(Environment environment) {
    12. this.environment = environment;
    13. }
    14. private void processMetaValue(Object bean) {
    15. Class clz = bean.getClass();
    16. if (!clz.isAnnotationPresent(RefreshValue.class)) {
    17. return;
    18. }
    19. if (clz.isAnnotationPresent(ConfigurationProperties.class)) {
    20. //@ConfigurationProperties注解
    21. ConfigurationProperties config = (ConfigurationProperties) clz.getAnnotation(ConfigurationProperties.class);
    22. for (Field field : clz.getDeclaredFields()) {
    23. String key = config.prefix() + "." + field.getName();
    24. if(mapper.containsKey(key)){
    25. mapper.get(key).add(new FieldPair(bean, field, key));
    26. }else{
    27. List fieldPairList = new ArrayList<>();
    28. fieldPairList.add(new FieldPair(bean, field, key));
    29. mapper.put(key, fieldPairList);
    30. }
    31. }
    32. } else {
    33. //@Valuez注解
    34. try {
    35. for (Field field : clz.getDeclaredFields()) {
    36. if (field.isAnnotationPresent(Value.class)) {
    37. Value val = field.getAnnotation(Value.class);
    38. String key = val.value().replace("${", "").replace("}", "");
    39. if(mapper.containsKey(key)){
    40. mapper.get(key).add(new FieldPair(bean, field, key));
    41. }else{
    42. List fieldPairList = new ArrayList<>();
    43. fieldPairList.add(new FieldPair(bean, field, key));
    44. mapper.put(key, fieldPairList);
    45. }
    46. }
    47. }
    48. } catch (Exception e) {
    49. e.printStackTrace();
    50. System.exit(-1);
    51. }
    52. }
    53. }
    54. public static class FieldPair {
    55. private static PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}",
    56. ":", true);
    57. private Object bean;
    58. private Field field;
    59. private String value;
    60. public FieldPair(Object bean, Field field, String value) {
    61. this.bean = bean;
    62. this.field = field;
    63. this.value = value;
    64. }
    65. public void updateValue(Environment environment) {
    66. boolean access = field.isAccessible();
    67. if (!access) {
    68. field.setAccessible(true);
    69. }
    70. try {
    71. if (field.getType() == String.class) {
    72. String updateVal = environment.getProperty(value);
    73. field.set(bean, updateVal);
    74. }
    75. else if (field.getType() == Integer.class) {
    76. Integer updateVal = environment.getProperty(value,Integer.class);
    77. field.set(bean, updateVal);
    78. }
    79. else if (field.getType() == int.class) {
    80. int updateVal = environment.getProperty(value,int.class);
    81. field.set(bean, updateVal);
    82. }
    83. else if (field.getType() == Boolean.class) {
    84. Boolean updateVal = environment.getProperty(value,Boolean.class);
    85. field.set(bean, updateVal);
    86. }
    87. else if (field.getType() == boolean.class) {
    88. boolean updateVal = environment.getProperty(value,boolean.class);
    89. field.set(bean, updateVal);
    90. }
    91. else {
    92. String updateVal = environment.getProperty(value);
    93. field.set(bean, JSONObject.parseObject(updateVal, field.getType()));
    94. }
    95. } catch (IllegalAccessException e) {
    96. e.printStackTrace();
    97. }
    98. field.setAccessible(access);
    99. }
    100. public Object getBean() {
    101. return bean;
    102. }
    103. public void setBean(Object bean) {
    104. this.bean = bean;
    105. }
    106. public Field getField() {
    107. return field;
    108. }
    109. public void setField(Field field) {
    110. this.field = field;
    111. }
    112. public String getValue() {
    113. return value;
    114. }
    115. public void setValue(String value) {
    116. this.value = value;
    117. }
    118. }
    119. public static class ConfigUpdateEvent extends ApplicationEvent {
    120. String key;
    121. public ConfigUpdateEvent(Object source, String key) {
    122. super(source);
    123. this.key = key;
    124. }
    125. }
    126. @EventListener
    127. public void updateConfig(ConfigUpdateEvent configUpdateEvent) {
    128. if(mapper.containsKey(configUpdateEvent.key)){
    129. List fieldPairList = mapper.get(configUpdateEvent.key);
    130. if(fieldPairList.size()>0){
    131. for (FieldPair fieldPair:fieldPairList) {
    132. fieldPair.updateValue(environment);
    133. }
    134. }
    135. }
    136. }
    137. }

  • 相关阅读:
    学习笔记--强化学习(1)
    【贪心基本算法】贪心算法常见题目
    【Redis】Redis 淘汰、雪崩、击穿、穿透、预热
    【cpolar】Ubuntu本地快速搭建web小游戏网站,公网用户远程访问
    GEE错误——Tile error: Arrays must have same lengths on all axes but the cat axis
    脚手架构建VUE项目
    vue双向绑定/小程序双向绑定?
    【ROS】RViz、Gazebo和Navigation的关系
    spring引入外部属性文件
    DNS外带注入
  • 原文地址:https://blog.csdn.net/weixin_48890074/article/details/133788680