snakeyaml编辑yaml文件并覆盖注释

前言

最近在做一个动态整合框架的项目,需要根据需求动态组装各个功能模块。其中就涉及到了在application.yaml中加入其他模块的配置,这里我们采用了snakeyaml进行配置信息写入,并采用文件回写保证注释不丢失。

技术积累

SnakeYaml就是用于解析YAML,序列化以及反序列化的第三方框架,解析yml的三方框架有很多,SnakeYaml,jYaml,Jackson等,但是不同的工具功能还是差距较大,比如jYaml就不支持合并。

SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。

SnakeYaml官方地址:http://yaml.org/type/index.html

在这里插入图片描述

实战演示

1、引入maven依赖

<!--yaml编辑-->
<dependency>
  <groupId>org.yaml</groupId>
  <artifactId>snakeyaml</artifactId>
  <version>1.23</version>
</dependency>
<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.11.0</version>
</dependency>
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.9</version>
</dependency>

2、覆盖注释工具类

由于snakeyaml在操作文件时候,会先将yaml转为map然后再回写到文件,这个操作会导致注释丢失。
目前有效的方案是将修改前文件注释进行缓存,然后当业务操作完文件后进行注释会写,这样就能够保证注释不会被覆盖。

当然,目前的方案并没有增加新的配置文件注释写入功能,有需要的同学可以自己实现。大概的思路是根据在回写注释的时候根据key将新增的注释写入,此时需要注释多个key相同的情况,故需要判断全链路key以防止重复注释乱序。

package com.example.demo.utils;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * CommentUtils
 * @author senfel
 * @version 1.0
 * @date 2023/12/6 18:20
 */
public class CommentUtils {

    public static final String END = "END###";
    public static final Pattern COMMENT_LINE = Pattern.compile("^\\s*#.*$");
    public static final Pattern BLANK_LINE = Pattern.compile("^\\s*$");
    //带注释的有效行,  使用非贪婪模式匹配有效内容
    public static final Pattern LINE_WITH_COMMENT = Pattern.compile("^(.*?)\\s+#.*$");


    @Data
    @AllArgsConstructor
    public static class Comment {
        private String lineNoComment;
        private String lineWithComment;
        private Integer indexInDuplicates;    // 存在相同行时的索引 (不同key下相同的行, 如 a:\n name: 1  和  b:\n name: 1 )
        private boolean isEndLine() {
            return END.equals(lineNoComment);
        }
    }


    @SneakyThrows
    public static CommentHolder buildCommentHolder(File file) {
        List<Comment> comments = new ArrayList<>();
        Map<String, Integer> duplicatesLineIndex = new HashMap<>();
        CommentHolder holder = new CommentHolder(comments);
        List<String> lines = FileUtils.readLines(file, StandardCharsets.UTF_8);
        // 末尾加个标志, 防止最后的注释丢失
        lines.add(END);
        StringBuilder lastLinesWithComment = new StringBuilder();
        for (String line : lines) {
            if (StringUtils.isBlank(line) || BLANK_LINE.matcher(line).find()) {
                lastLinesWithComment.append(line).append('\n');
                continue;
            }
            // 注释行/空行 都拼接起来
            if (COMMENT_LINE.matcher(line).find()) {
                lastLinesWithComment.append(line).append('\n');
                continue;
            }
            String lineNoComment = line;
            boolean lineWithComment = false;
            // 如果是带注释的行, 也拼接起来, 但是记录非注释的部分
            Matcher matcher = LINE_WITH_COMMENT.matcher(line);
            if (matcher.find()) {
                lineNoComment = matcher.group(1);
                lineWithComment = true;
            }
            // 去除后面的空格
            lineNoComment = lineNoComment.replace("\\s*$", "");
            // 记录下相同行的索引
            Integer idx = duplicatesLineIndex.merge(lineNoComment, 1, Integer::sum);
            // 存在注释内容, 记录
            if (lastLinesWithComment.length() > 0 || lineWithComment) {
                lastLinesWithComment.append(line);
                comments.add(new Comment(lineNoComment, lastLinesWithComment.toString(), idx));
                // 清空注释内容
                lastLinesWithComment = new StringBuilder();
            }
        }

        return holder;
    }


    @AllArgsConstructor
    public static class CommentHolder {
        private List<Comment> comments;

        /**
         * 通过正则表达式移除匹配的行 (防止被移除的行携带注释信息, 导致填充注释时无法正常匹配)
         */
        public void removeLine(String regex) {
            comments.removeIf(comment -> comment.getLineNoComment().matches(regex));
        }

        /**
         * fillComments
         * @param file
         * @author senfel
         * @date 2023/12/7 11:24
         * @return void
         */
        @SneakyThrows
        public void fillComments(File file) {
            if (comments == null || comments.isEmpty()) {
                return;
            }
            if (file == null || !file.exists()) {
                throw new IllegalArgumentException("file is not exist");
            }
            List<String> lines = FileUtils.readLines(file, StandardCharsets.UTF_8);
            Map<String, Integer> duplicatesLineIndex = new HashMap<>();
            int comIdx = 0;
            StringBuilder res = new StringBuilder();
            for (String line : lines) {
                Integer idx = duplicatesLineIndex.merge(line, 1, Integer::sum);
                Comment comment = getOrDefault(comments, comIdx, null);
                if (comment != null &&
                        Objects.equals(line, comment.lineNoComment)
                        && Objects.equals(comment.indexInDuplicates, idx)) {

                    res.append(comment.lineWithComment).append('\n');
                    comIdx++;
                } else {
                    res.append(line).append('\n');
                }
            }
            Comment last = comments.get(comments.size() - 1);
            if (last.isEndLine()) {
                res.append(last.lineWithComment.substring(0, last.lineWithComment.indexOf(END)));
            }
            FileUtils.write(file, res.toString(), StandardCharsets.UTF_8);
        }
    }

    public static <T> T getOrDefault(List<T> vals, int index, T defaultVal) {
        if (vals == null || vals.isEmpty()) {
            return defaultVal;
        }
        if (index >= vals.size()) {
            return defaultVal;
        }
        T v = vals.get(index);
        return v == null ? defaultVal : v;
    }

}

3、snakeyaml工具类

snakeyaml工具类主要作用就是将yaml文件转为map的格式,然后依次进行判断写入或者修改value。

package com.example.demo.utils;

import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * YamlActionUtils 
 * @author senfel
 * @version 1.0
 * @date 2023/12/7 13:48
 */
public class YamlActionUtils {

    /**
     * 配置
     * @author senfel
     * @date 2023/12/7 13:49
     * @return
     */
    private static DumperOptions dumperOptions = new DumperOptions();

    static{
        //设置yaml读取方式为块读取
        dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
        dumperOptions.setDefaultScalarStyle(DumperOptions.ScalarStyle.PLAIN);
        dumperOptions.setPrettyFlow(false);
    }

    /**
     * insertYaml
     * @param key a.b.c
     * @param value
     * @param path
     * @author senfel
     * @date 2023/12/7 10:11
     * @return boolean
     */
    public static boolean insertYaml(String key, Object value, String path) throws Exception {
        Yaml yaml = new Yaml(dumperOptions);
        String[] keys = key.split("\\.");
        int len = keys.length;
        //将属性转为map
        FileInputStream fileInputStream = new FileInputStream(new File(path));
        Map<String, Object> yamlToMap = (Map<String, Object>)yaml.load(fileInputStream);
        Object oldVal = getValue(key, yamlToMap);
        //找到key不再新增
        if (null != oldVal) {
            return true;
        }
        Map<String,Object> temp = yamlToMap;
        for (int i = 0; i < len - 1; i++) {
            if (temp.containsKey(keys[i])) {
                temp = (Map) temp.get(keys[i]);
            } else {
                temp.put(keys[i],new HashMap<String,Object>());
                temp =(Map)temp.get(keys[i]);
            }
            if (i == len - 2) {
                temp.put(keys[i + 1], value);
            }
        }
        try {
            yaml.dump(yamlToMap, new FileWriter(path));
        } catch (Exception e) {
            System.out.println("yaml file insert failed !");
            return false;
        }
        return true;
    }

    /**
     * updateYaml
     * @param paramKey a.b.c
     * @param paramValue
     * @param path
     * @author senfel
     * @date 2023/12/7 10:03
     * @return boolean
     */
    public static boolean updateYaml(String paramKey, Object paramValue,String path) throws Exception{
        Yaml yaml = new Yaml(dumperOptions);
        //yaml文件路径
        String yamlUr = path;
        Map map = null;
        try {
            //将yaml文件加载为map格式
            map = yaml.loadAs(new FileInputStream(yamlUr), Map.class);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        //获取当前参数值并且修改
        boolean flag = updateYaml(paramKey, paramValue, map, yamlUr, yaml);
        return flag;
    }


    /**
     * updateYaml
     * @param key a.b.c
     * @param value
     * @param yamlToMap
     * @param path
     * @param yaml
     * @author senfel
     * @date 2023/12/7 10:51
     * @return boolean
     */
    public static boolean updateYaml(String key, Object value, Map<String, Object> yamlToMap, String path, Yaml yaml) {
        Object oldVal = getValue(key, yamlToMap);
        //未找到key 不修改
        if (null == oldVal) {
            return false;
        }
        try {
            Map<String, Object> resultMap = setValue(yamlToMap, key, value);
            if (resultMap != null) {
                yaml.dump(resultMap, new FileWriter(path));
                return true;
            } else {
                return false;
            }
        } catch (Exception e) {
            System.out.println("yaml file update failed !");
        }
        return false;
    }


    /**
     * getValue
     * @param key a.b.c
     * @param yamlMap
     * @author senfel
     * @date 2023/12/7 10:51
     * @return java.lang.Object
     */
    public static Object getValue(String key, Map<String, Object> yamlMap) {
        String[] keys = key.split("[.]");
        Object o = yamlMap.get(keys[0]);
        if (key.contains(".")) {
            if (o instanceof Map) {
                return getValue(key.substring(key.indexOf(".") + 1), (Map<String, Object>) o);
            } else {
                return null;
            }
        } else {
            return o;
        }
    }


    /**
     * setValue
     * @param map
     * @param key a.b.c
     * @param value
     * @author senfel
     * @date 2023/12/7 9:59
     * @return java.util.Map<java.lang.String, java.lang.Object>
     */
    public static Map<String, Object> setValue(Map<String, Object> map, String key, Object value) {
        String[] keys = key.split("\\.");
        int len = keys.length;
        Map temp = map;
        for (int i = 0; i < len - 1; i++) {
            if (temp.containsKey(keys[i])) {
                temp = (Map) temp.get(keys[i]);
            } else {
                return null;
            }
            if (i == len - 2) {
                temp.put(keys[i + 1], value);
            }
        }
        for (int j = 0; j < len - 1; j++) {
            if (j == len - 1) {
                map.put(keys[j], temp);
            }
        }
        return map;
    }

}

4、测试用例

我们分别新增、修改yaml文件进行测试。

package com.example.demo;

import com.example.demo.utils.CommentUtils;
import com.example.demo.utils.YamlActionUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.File;

/**
 * YamlActionTest
 * @author senfel
 * @version 1.0
 * @date 2023/12/6 17:55
 */
@SpringBootTest
public class YamlActionTest {

    @Test
    public void addKey() throws Exception{
        String filePath = "D:\\workspace\\demo\\src\\main\\resources\\application.yaml";
        File file = new File(filePath);
        //记录yaml文件的注释信息
        CommentUtils.CommentHolder holder = CommentUtils.buildCommentHolder(file);
        //YamlActionUtils.insertYaml("spring.activemq.broker-url","http://127.0.0.1/test",filePath);
        //YamlActionUtils.insertYaml("spring.activemq.pool.enabled",false,filePath);
        YamlActionUtils.insertYaml("wx.pc.lx.enable",false,filePath);
        //YamlActionUtils.insertYaml("spring.activemq.in-memory",false,filePath);
        //YamlActionUtils.updateYaml("spring.activemq.in-memory",false,filePath);
        //填充注释信息
        holder.fillComments(file);
    }
}

5、测试效果展示

server:
  port: 8888
spring:
  activemq:
    close-timeout: 15 #超时
    broker-url: http://127.0.0.1/test #路径
    pool:
      enabled: false # 是否开启
wx:
  pc:
    lx:
      enable: false

如上所示 wx.pc.lx.enable=false已经写入。

写在最后

snakeyaml编辑yaml文件并覆盖注释还是比较简单,大致就是在操作yaml文件之前对注释进行缓存,操作文件时先将yaml转为map,然后配置数据写入并转换成yaml文件,最后再将注释覆盖在yaml上即可。

  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小沈同学呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值