文章目录
- 前言
- 技术积累
- 实战演示
- 1、引入maven依赖
- 2、覆盖注释工具类
- 3、snakeyaml工具类
- 4、测试用例
- 5、测试效果展示
- 写在最后
前言
最近在做一个动态整合框架的项目,需要根据需求动态组装各个功能模块。其中就涉及到了在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@AllArgsConstructorpublic 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);}}@SneakyThrowspublic 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;}@AllArgsConstructorpublic 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*/@SneakyThrowspublic 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;//将属性转为mapFileInputStream 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 {@Testpublic 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上即可。