文章目录
- 前言
- 技术积累
- 实战演示
- 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
@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上即可。