文章目錄
- 前言
- 技術積累
- 實戰演示
- 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上即可。