Signed-off-by: odboy <tianjun@odboy.cn>

This commit is contained in:
骑着蜗牛追导弹 2024-12-11 20:56:47 +08:00
parent 70c0ae9896
commit 88cd1c51c2
21 changed files with 783 additions and 501 deletions

114
README.md
View File

@ -1,72 +1,80 @@
<h1 style="text-align: center">Kenaito Config</h1>
# Kenaito Config
## 项目简介
## 简介
基于 Spring Boot 2.7.18 、 Mybatis-Plus、 JWT、Spring Security、Redis、Vue 的 配置中心
Kenaito Config 是一个轻量级的配置中心,旨在简化应用配置,使其更加优雅。它专为自研 DevOps 平台设计。
**账号密码:** `admin / 123456`
## 技术栈
- **后端**:
- Spring Boot 2.7.18
- Mybatis-Plus
- JWT
- Spring Security
- Redis
- **前端**:
- Vue 2
## 系统功能
- 用户管理提供用户的相关配置新增用户后默认密码为123456
- 角色管理:对权限与菜单进行分配,可根据部门设置角色的数据权限
- 菜单管理:已实现菜单动态路由,后端可配置化,支持多级菜单
- 部门管理:可配置系统组织架构,树形表格展示
- 岗位管理:配置各个部门的职位
- 字典管理:可维护常用一些固定的数据,如:状态,性别等
- SQL监控采用druid 监控数据库访问性能默认用户名admin密码123456
- 邮件工具配合富文本发送html格式的邮件
- 服务监控:监控服务器的负载情况
- **用户管理**: 提供用户相关配置,新增用户默认密码为 `123456`
- **角色管理**: 分配权限与菜单,支持按部门设置角色数据权限。
- **菜单管理**: 实现菜单动态路由,支持多级菜单配置。
- **部门管理**: 配置系统组织架构,以树形表格展示。
- **岗位管理**: 配置各部门职位。
- **字典管理**: 维护常用固定数据,如状态、性别等。
- **SQL 监控**: 使用 Druid 监控数据库访问性能,默认用户名和密码均为 `admin`
- **邮件工具**: 支持发送 HTML 格式的富文本邮件。
- **服务监控**: 监控服务器负载情况。
## 待办
## 默认账号与密码
#### 客户端
admin / 123456
- 20241205 启动后,主动拉取远程配置
- 20241205 定时刷盘(刷到一个文件中)
- 20241206 同步锁加载配置
- 20241206 定时刷盘(刷到多个文件中)
- 20241206 读取本地配置缓存(连接服务端失败后的兜底操作)
- 20241207 动态更新@Value注解的属性
- 20241207 动态更新@ConfigurationProperties注解类中的属性
- 感想:太艰难了
- 感谢spring-cloud-context 给我的灵感
- 耗时4小时
- 202412xx 动态替换配置指令实现 [loading]
## 待办事项
#### 服务
### 客户端
- 客户端注册、注销 [ok]
- 多客户端支持 [ok]
- 发布配置 [loading]
- 获取客户端节点列表 [ok]
- **2024-12-05**: 启动后主动拉取远程配置
- **2024-12-05**: 定时刷盘(刷到一个文件中)
- **2024-12-06**: 同步锁加载配置
- **2024-12-06**: 定时刷盘(刷到多个文件中)
- **2024-12-06**: 读取本地配置缓存(连接服务端失败后的兜底操作)
- **2024-12-07**: 动态更新 `@Value` 注解的属性
- 感想:太艰难了
- 感谢:感谢 `spring-cloud-context` 的灵感
- 耗时4 小时
- **2024-12-07**: 动态更新 `@ConfigurationProperties` 注解类中的属性
- **2024-12-xx**: 动态替换配置指令实现 [loading]
#### Web页面
### 服务端
> 这个apollo页面该有的选项我们统统都得有。当然咱们代码纯原创哈~ 毕竟写那么烂。
- 应用中心:
- 所有应用 [loading]
- 我的应用 [loading]
- 收藏的应用 [loading]
- 应用详情:
- 自定义环境 [loading]
- 变更历史 [loading]
- 发布历史 [loading]
- 回滚 [loading]
- 实例列表 [loading]
- 应用授权(用户可以访问哪些环境的配置) [loading]
- **客户端注册、注销** [已完成]
- **多客户端支持** [已完成]
- **发布配置** [进行中]
- **获取客户端节点列表** [已完成]
## 更新记录
- 20241205 成功获取远程配置,并启动子应用
![20241205](/doc/d20241205223929.png)
- **2024-12-05**: 成功获取远程配置并启动子应用
![2024-12-05](/doc/d20241205223929.png)
- **2024-12-10**: Web 页面大成
![2024-12-10](/doc/d20241210215050.png)
## 常见问题
## 贡献指南
#### win10端口被占用解决
欢迎贡献代码!请遵循以下步骤:
```shell
# 记下最后一列pid打开任务管理器知道对应的进程干掉
netstat -ano|findstr 28010
```
1. **Fork 仓库**
2. **创建新分支**: `git checkout -b feature/your-feature`
3. **提交更改**: `git commit -m 'Add some feature'`
4. **推送分支**: `git push origin feature/your-feature`
5. **发起 Pull Request**
## 许可证
本项目采用 [MIT License](LICENSE) 许可证。
## 联系我们
如果有任何问题或建议,请通过 [GitHub Issues](https://github.com/odboy-tianjun/kenaito-config/issues) 联系我。

BIN
doc/d20241210215050.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View File

@ -1,27 +0,0 @@
package cn.odboy.config.context;
/**
* @date: 2020/6/9 17:02
* @since: 1.0
* @see {@link SpringContextHolder}
* 针对某些初始化方法在SpringContextHolder 初始化前时<br>
* 可提交一个 提交回调任务<br>
* 在SpringContextHolder 初始化后进行回调使用
*/
public interface CallBack {
/**
* 回调执行方法
*/
void executor();
/**
* 本回调任务名称
*
* @return /
*/
default String getCallBackName() {
return Thread.currentThread().getId() + ":" + this.getClass().getName();
}
}

View File

@ -1,140 +0,0 @@
package cn.odboy.config.context;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Slf4j
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {
private static ApplicationContext applicationContext = null;
private static final List<CallBack> CALL_BACKS = new ArrayList<>();
private static boolean addCallback = true;
/**
* 针对 某些初始化方法在SpringContextHolder 未初始化时 提交回调方法
* 在SpringContextHolder 初始化后进行回调使用
*
* @param callBack 回调函数
*/
public synchronized static void addCallBacks(CallBack callBack) {
if (addCallback) {
SpringContextHolder.CALL_BACKS.add(callBack);
} else {
log.warn("CallBack{} 已无法添加!立即执行", callBack.getCallBackName());
callBack.executor();
}
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
assertContextInjected();
return (T) applicationContext.getBean(name);
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(Class<T> requiredType) {
assertContextInjected();
return applicationContext.getBean(requiredType);
}
/**
* 获取SpringBoot 配置信息
*
* @param property 属性key
* @param defaultValue 默认值
* @param requiredType 返回类型
* @return /
*/
public static <T> T getProperties(String property, T defaultValue, Class<T> requiredType) {
T result = defaultValue;
try {
result = getBean(Environment.class).getProperty(property, requiredType);
} catch (Exception ignored) {
}
return result;
}
/**
* 获取SpringBoot 配置信息
*
* @param property 属性key
* @return /
*/
public static String getProperties(String property) {
return getProperties(property, null, String.class);
}
/**
* 获取SpringBoot 配置信息
*
* @param property 属性key
* @param requiredType 返回类型
* @return /
*/
public static <T> T getProperties(String property, Class<T> requiredType) {
return getProperties(property, null, requiredType);
}
/**
* 检查ApplicationContext不为空.
*/
private static void assertContextInjected() {
if (applicationContext == null) {
throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" +
".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder.");
}
}
/**
* 清除SpringContextHolder中的ApplicationContext为Null.
*/
private static void clearHolder() {
log.debug("清除SpringContextHolder中的ApplicationContext:"
+ applicationContext);
applicationContext = null;
}
@Override
public void destroy() {
SpringContextHolder.clearHolder();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringContextHolder.applicationContext != null) {
log.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" + SpringContextHolder.applicationContext);
}
SpringContextHolder.applicationContext = applicationContext;
if (addCallback) {
for (CallBack callBack : SpringContextHolder.CALL_BACKS) {
callBack.executor();
}
CALL_BACKS.clear();
}
SpringContextHolder.addCallback = false;
}
/**
* 获取 @Service 的所有 bean 名称
*
* @return /
*/
public static List<String> getAllServiceBeanName() {
return new ArrayList<>(Arrays.asList(applicationContext
.getBeanNamesForAnnotation(Service.class)));
}
}

View File

@ -1,54 +1,71 @@
package cn.odboy.config.model;
import cn.odboy.config.constant.TransferMessageType;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmallMessage implements Serializable {
/** 消息类型cn.odboy.config.constant.TransferMessageType */
private TransferMessageType type;
private Response resp;
@Data
public static class Response implements Serializable {
private Boolean success = true;
private String errorCode = "0";
private String errorMessage = "success";
private Object data;
/**
* 消息类型cn.odboy.config.constant.TransferMessageType
* 创建一个表示错误响应的对象 该方法用于当请求出现问题时返回信息给客户端
*
* @param errorMessage 错误信息用于向客户端描述错误情况
* @return 返回一个包含错误信息的Response对象
*/
private TransferMessageType type;
private Response resp;
@Data
public static class Response implements Serializable {
private Boolean success = true;
private String errorCode = "0";
private String errorMessage = "success";
private Object data;
public static Response bad(String errorMessage) {
Response response = new Response();
response.setSuccess(false);
response.setErrorCode("400");
response.setErrorMessage(errorMessage);
response.setData(null);
return response;
}
public static Response ok(Object data, String errorMessage) {
Response response = new Response();
response.setSuccess(true);
response.setErrorCode("0");
response.setErrorMessage(errorMessage);
response.setData(data);
return response;
}
public static Response ok(Object data) {
Response response = new Response();
response.setSuccess(true);
response.setErrorCode("0");
response.setErrorMessage("success");
response.setData(data);
return response;
}
public static Response bad(String errorMessage) {
Response response = new Response();
response.setSuccess(false);
response.setErrorCode("400");
response.setErrorMessage(errorMessage);
response.setData(null);
return response;
}
/**
* 创建一个表示成功响应的对象包含数据和错误信息 该方法用于当请求成功时返回信息和数据给客户端
*
* @param data 成功响应的数据
* @param errorMessage 即使在成功的情况下也可能需要提供一些错误信息
* @return 返回一个包含成功状态数据和错误信息的Response对象
*/
public static Response ok(Object data, String errorMessage) {
Response response = new Response();
response.setSuccess(true);
response.setErrorCode("0");
response.setErrorMessage(errorMessage);
response.setData(data);
return response;
}
/**
* 创建一个表示成功响应的对象仅包含数据 该方法用于当请求成功且不需要提供错误信息时使用
*
* @param data 成功响应的数据
* @return 返回一个包含成功状态和数据的Response对象
*/
public static Response ok(Object data) {
Response response = new Response();
response.setSuccess(true);
response.setErrorCode("0");
response.setErrorMessage("success");
response.setData(data);
return response;
}
}
}

View File

@ -12,9 +12,34 @@ import java.io.Serializable;
*/
@Data
public class ClientInfo implements Serializable {
/**
* 服务器地址
* 用于指定服务的主机名或IP地址
*/
private String server;
/**
* 端口号
* 用于指定服务的通信端口
*/
private Integer port;
/**
* 环境标识
* 用于标识当前配置所属的运行环境例如开发测试或生产环境
*/
private String env;
/**
* 数据ID
* 用于唯一标识配置数据在分布式系统中起到关键作用
*/
private String dataId;
/**
* 缓存目录
* 用于存储缓存数据的目录路径
*/
private String cacheDir;
}

View File

@ -10,6 +10,14 @@ import java.io.Serializable;
*/
@Data
public class ConfigFileInfo implements Serializable {
/**
* 文件名变量用于存储文件的名称
*/
private String fileName;
/**
* 文件内容变量用于存储文件的文本内容
*/
private String fileContent;
}

View File

@ -2,17 +2,31 @@ package cn.odboy.config.util;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
/**
* 统一操作Channel
*
* @author odboy
* @date 2024-12-06
*/
public class ChannelUtil {
public static String getId(ChannelHandlerContext ctx){
return ctx.channel().id().asShortText();
}
/**
* 根据ChannelHandlerContext获取通道的唯一标识符 该方法用于在处理通道相关的操作时能够快速获取到通道的唯一标识符以便进行后续的处理
*
* @param ctx 通道的上下文对象包含了通道的所有相关信息和操作方法
* @return 返回通道的唯一标识符以短文本形式呈现
*/
public static String getId(ChannelHandlerContext ctx) {
return ctx.channel().id().asShortText();
}
public static String getId(ChannelId channelId){
return channelId.asShortText();
}
/**
* 根据ChannelId获取通道的唯一标识符 当只有通道的标识符时可以使用该方法获取通道的唯一标识符的短文本形式
*
* @param channelId 通道的标识符对象唯一标识了一个通道
* @return 返回通道的唯一标识符以短文本形式呈现
*/
public static String getId(ChannelId channelId) {
return channelId.asShortText();
}
}

View File

@ -15,10 +15,22 @@ import java.util.List;
* @date 2024-12-06
*/
public class MessageUtil {
/**
* 将给定的对象序列化为ByteBuf实例 该方法使用Protostuff库对对象进行序列化便于在网络传输或存储
*
* @param data 待序列化的对象
* @return 序列化后的ByteBuf实例
*/
public static ByteBuf toByteBuf(Object data) {
return Unpooled.copiedBuffer(ProtostuffUtil.serializer(data));
}
/**
* 反序列化ByteBuf为指定的SmallMessage对象 该方法主要用于处理接收到的字节数据将其还原为对象形式
*
* @param msg 待反序列化的ByteBuf对象被视为字节数据源
* @return 反序列化后的SmallMessage对象
*/
public static SmallMessage getMessage(Object msg) {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
@ -26,27 +38,56 @@ public class MessageUtil {
return ProtostuffUtil.deserializer(bytes, SmallMessage.class);
}
// ================ 以下为很糙的自定义方法
/**
* 创建一个表示注册失败的ByteBuf消息 该方法用于生成一个包含错误信息的注册响应消息便于在网络中传输
*
* @param errorMessage 注册失败的错误信息
* @return 包含注册失败信息的ByteBuf消息
*/
public static ByteBuf toRegisterBad(String errorMessage) {
return toByteBuf(
new SmallMessage(TransferMessageType.REGISTER, SmallMessage.Response.bad(errorMessage)));
}
/**
* 创建一个表示注册成功的ByteBuf消息 该方法用于生成一个包含成功信息的注册响应消息便于在网络中传输
*
* @param data 注册成功时附带的数据
* @return 包含注册成功信息的ByteBuf消息
*/
public static ByteBuf toRegisterOk(Object data) {
return toByteBuf(
new SmallMessage(TransferMessageType.REGISTER, SmallMessage.Response.ok(data)));
}
/**
* 创建一个表示推送配置失败的ByteBuf消息 该方法用于生成一个包含错误信息的推送配置响应消息便于在网络中传输
*
* @param errorMessage 推送配置失败的错误信息
* @return 包含推送配置失败信息的ByteBuf消息
*/
public static ByteBuf toPushConfigBad(String errorMessage) {
return toByteBuf(
new SmallMessage(TransferMessageType.PUSH_CONFIG, SmallMessage.Response.bad(errorMessage)));
}
/**
* 创建一个表示推送配置成功的ByteBuf消息 该方法用于生成一个包含成功信息的推送配置响应消息便于在网络中传输
*
* @param data 推送配置成功时附带的数据
* @return 包含推送配置成功信息的ByteBuf消息
*/
public static ByteBuf toPushConfigOk(Object data) {
return toByteBuf(
new SmallMessage(TransferMessageType.PUSH_CONFIG, SmallMessage.Response.ok(data)));
}
/**
* 将给定的对象转换为ConfigFileInfo对象列表 该方法主要用于处理接收到的消息将其转换为配置文件信息列表 如果给定对象不是预期的列表类型或列表为空则返回一个新的空列表
*
* @param o 待转换的对象
* @return 转换后的ConfigFileInfo对象列表如果转换失败则返回空列表
*/
public static List<ConfigFileInfo> toConfigFileInfoList(Object o) {
if (o instanceof List) {
List<?> list = (List<?>) o;

View File

@ -0,0 +1,264 @@
package cn.odboy.config.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.yaml.snakeyaml.Yaml;
/**
* 工具类出处https://blog.csdn.net/qq_27574367/article/details/134684434 <br>
* fix: 函数flattenMap中value为null导致的异常中断 <br>
*
* @author Deng.Weiping
* @since 2023/11/28 13:57
*/
@Slf4j
public class PropertiesUtil {
private static final Pattern PATTERN = Pattern.compile("\\s*([^=\\s]*)\\s*=\\s*(.*)\\s*");
private static final String PATH_SEP = ".";
/**
* YAML 字符串转换为 Properties 字符串
*
* @param input YAML 字符串
* @return Properties 字符串
*/
public static String castToProperties(String input) {
Map<String, Object> propertiesMap = new LinkedHashMap<>();
Map<String, Object> yamlMap = new Yaml().load(input);
flattenMap("", yamlMap, propertiesMap);
return propertiesMap.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining(StrUtil.LF));
}
/**
* Properties 字符串转换为 YAML 字符串
*
* @param input Properties 字符串
* @return YAML 字符串
*/
public static String castToYaml(String input) {
try {
Map<String, Object> properties = readProperties(input);
return properties2Yaml(properties);
} catch (Exception e) {
log.error("property 转 Yaml 转换异常", e);
}
return null;
}
/**
* InputStream 中的 Properties 转换为 YAML 字符串
*
* @param inputStream 输入流
* @return YAML 字符串
*/
public static String castToYaml(InputStream inputStream) {
try {
Map<String, Object> properties =
readProperties(StrUtil.str(inputStream.readAllBytes(), StandardCharsets.UTF_8));
return properties2Yaml(properties);
} catch (Exception e) {
log.error("property 转 Yaml 转换异常", e);
}
return null;
}
/**
* 将字节数组中的 Properties 转换为 YAML 字符串
*
* @param bytes 字节数组
* @return YAML 字符串
*/
public static String castToYaml(byte[] bytes) {
try {
Map<String, Object> properties = readProperties(StrUtil.str(bytes, StandardCharsets.UTF_8));
return properties2Yaml(properties);
} catch (Exception e) {
log.error("property 转 Yaml 转换异常", e);
}
return null;
}
/**
* 读取 Properties 字符串并转换为 Map
*
* @param input Properties 字符串
* @return Map 对象
*/
private static Map<String, Object> readProperties(String input) {
Map<String, Object> propertiesMap = new LinkedHashMap<>();
for (String line : input.split(StrUtil.LF)) {
if (StrUtil.isNotBlank(line)) {
Matcher matcher = PATTERN.matcher(line);
if (matcher.matches()) {
String key = matcher.group(1);
String value = matcher.group(2);
propertiesMap.put(key, value);
}
}
}
return propertiesMap;
}
/**
* 递归地将 Map 转换为 Properties 格式的 Map
*
* @param prefix 前缀
* @param yamlMap YAML 格式的 Map
* @param treeMap 目标 Properties 格式的 Map
*/
private static void flattenMap(
String prefix, Map<String, Object> yamlMap, Map<String, Object> treeMap) {
yamlMap.forEach(
(key, value) -> {
if (value != null) {
String fullKey = prefix + key;
if (value instanceof LinkedHashMap) {
flattenMap(fullKey + ".", (LinkedHashMap) value, treeMap);
} else if (value instanceof ArrayList) {
List<?> values = (List<?>) value;
for (int i = 0; i < values.size(); i++) {
String itemKey = String.format("%s[%d]", fullKey, i);
Object itemValue = values.get(i);
if (itemValue instanceof String) {
treeMap.put(itemKey, itemValue);
} else {
flattenMap(itemKey + ".", (LinkedHashMap) itemValue, treeMap);
}
}
} else {
treeMap.put(fullKey, value.toString());
}
}
});
}
/**
* Properties 格式的 Map 转换为 YAML 格式的字符串
*
* @param properties Properties 格式的 Map
* @return YAML 格式的字符串
*/
private static String properties2Yaml(Map<String, Object> properties) {
if (CollUtil.isEmpty(properties)) {
return null;
}
Map<String, Object> map = parseToMap(properties);
return map2Yaml(map).toString();
}
/**
* 递归地将 Properties 格式的 Map 解析为 LinkedHashMap
*
* @param propMap Properties 格式的 Map
* @return LinkedHashMap 对象
*/
private static Map<String, Object> parseToMap(Map<String, Object> propMap) {
Map<String, Object> resultMap = new LinkedHashMap<>();
if (CollectionUtils.isEmpty(propMap)) {
return resultMap;
}
propMap.forEach(
(key, value) -> {
if (key.contains(PATH_SEP)) {
String currentKey = key.substring(0, key.indexOf("."));
if (resultMap.get(currentKey) != null) {
return;
}
Map<String, Object> childMap = getChildMap(propMap, currentKey);
Map<String, Object> map = parseToMap(childMap);
resultMap.put(currentKey, map);
} else {
resultMap.put(key, value);
}
});
return resultMap;
}
/**
* 获取拥有相同父级节点的子节点
*
* @param propMap Properties 格式的 Map
* @param currentKey 当前父级节点的键
* @return 子节点的 Map
*/
private static Map<String, Object> getChildMap(Map<String, Object> propMap, String currentKey) {
Map<String, Object> childMap = new LinkedHashMap<>();
propMap.forEach(
(key, value) -> {
if (key.contains(currentKey + PATH_SEP)) {
String subKey = key.substring(key.indexOf(".") + 1);
childMap.put(subKey, value);
}
});
return childMap;
}
/**
* Map 集合转换为 YAML 格式的字符串
*
* @param map Map 对象
* @return YAML 格式的字符串
*/
public static StringBuffer map2Yaml(Map<String, Object> map) {
return map2Yaml(map, 0);
}
/**
* Map 集合转换为 YAML 格式的字符串
*
* @param propMap Map 对象
* @param deep 树的层级
* @return YAML 格式的字符串
*/
private static StringBuffer map2Yaml(Map<String, Object> propMap, int deep) {
StringBuffer yamlBuffer = new StringBuffer();
if (CollectionUtils.isEmpty(propMap)) {
return yamlBuffer;
}
String space = getSpace(deep);
for (Map.Entry<String, Object> entry : propMap.entrySet()) {
Object valObj = entry.getValue();
String key = space + entry.getKey() + ":";
if (valObj instanceof String) {
yamlBuffer.append(key).append(" ").append(valObj).append("\n");
} else if (valObj instanceof List) {
yamlBuffer.append(key).append("\n");
List<String> list =
((List<String>) valObj).stream().map(Object::toString).collect(Collectors.toList());
String lSpace = getSpace(deep + 1);
for (String str : list) {
yamlBuffer.append(lSpace).append("- ").append(str).append("\n");
}
} else if (valObj instanceof Map) {
yamlBuffer.append(key).append("\n");
yamlBuffer.append(map2Yaml((LinkedHashMap) valObj, deep + 1));
} else {
yamlBuffer.append(key).append(" ").append(valObj).append("\n");
}
}
return yamlBuffer;
}
/**
* 获取缩进空格
*
* @param deep 树的层级
* @return 缩进空格字符串
*/
private static String getSpace(int deep) {
return " ".repeat(Math.max(0, deep));
}
}

View File

@ -9,7 +9,14 @@ package cn.odboy.config.util;
public class PropertyNameUtil {
private static final String DEFAULT_PREFIX = "kenaito";
/**
* 根据文件名生成带有默认前缀的文件名
*
* @param fileName 文件名不包含路径信息
* @return 带有默认前缀的文件名格式为DEFAULT_PREFIX_fileName
*/
public static String get(String fileName) {
return DEFAULT_PREFIX + "_" + fileName;
}
}

View File

@ -5,7 +5,6 @@ import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.Schema;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -14,54 +13,68 @@ import java.util.concurrent.ConcurrentHashMap;
*/
public class ProtostuffUtil {
private static final Map<Class<?>, Schema<?>> CACHED_SCHEMA = new ConcurrentHashMap<Class<?>, Schema<?>>();
private static final Map<Class<?>, Schema<?>> CACHED_SCHEMA =
new ConcurrentHashMap<Class<?>, Schema<?>>();
private static <T> Schema<T> getSchema(Class<T> clazz) {
@SuppressWarnings("unchecked")
Schema<T> schema = (Schema<T>) CACHED_SCHEMA.get(clazz);
if (schema == null) {
schema = RuntimeSchema.getSchema(clazz);
if (schema != null) {
CACHED_SCHEMA.put(clazz, schema);
}
}
return schema;
/**
* 获取指定类的Schema Schema用于描述对象的结构以便于序列化和反序列化 该方法首先尝试从缓存中获取Schema如果缓存中没有则创建一个新的Schema并添加到缓存中
*
* @param clazz 需要获取Schema的类
* @return 指定类的Schema
*/
private static <T> Schema<T> getSchema(Class<T> clazz) {
@SuppressWarnings("unchecked")
Schema<T> schema = (Schema<T>) CACHED_SCHEMA.get(clazz);
if (schema == null) {
schema = RuntimeSchema.getSchema(clazz);
if (schema != null) {
CACHED_SCHEMA.put(clazz, schema);
}
}
return schema;
}
/**
* 序列化
*/
public static <T> byte[] serializer(T obj) {
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) obj.getClass();
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
Schema<T> schema = getSchema(clazz);
return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
} finally {
buffer.clear();
}
/**
* 序列化对象 使用Protostuff库将对象序列化为字节数组
*
* @param obj 需要序列化的对象
* @return 序列化后的字节数组
*/
public static <T> byte[] serializer(T obj) {
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) obj.getClass();
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
Schema<T> schema = getSchema(clazz);
return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
} finally {
buffer.clear();
}
}
/**
* 反序列化
*/
public static <T> T deserializer(byte[] data, Class<T> clazz) {
try {
T obj = clazz.newInstance();
Schema<T> schema = getSchema(clazz);
ProtostuffIOUtil.mergeFrom(data, obj, schema);
return obj;
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
/**
* 反序列化字节数组为对象 使用Protostuff库将字节数组反序列化为指定类的对象
*
* @param data 序列化后的字节数组
* @param clazz 需要反序列化的对象类
* @return 反序列化后的对象
*/
public static <T> T deserializer(byte[] data, Class<T> clazz) {
try {
T obj = clazz.newInstance();
Schema<T> schema = getSchema(clazz);
ProtostuffIOUtil.mergeFrom(data, obj, schema);
return obj;
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
public static void main(String[] args) {
byte[] userBytes = ProtostuffUtil.serializer(new ConfigFileInfo());
ConfigFileInfo user = ProtostuffUtil.deserializer(userBytes, ConfigFileInfo.class);
System.out.println(user);
}
public static void main(String[] args) {
byte[] userBytes = ProtostuffUtil.serializer(new ConfigFileInfo());
ConfigFileInfo user = ProtostuffUtil.deserializer(userBytes, ConfigFileInfo.class);
System.out.println(user);
}
}

View File

@ -117,9 +117,7 @@ public class ClientConfigLoader {
ClientConfigConsts.clientInfo.wait();
} catch (InterruptedException e) {
Thread currentThread = Thread.currentThread();
String currentThreadName = currentThread.getName();
currentThread.interrupt();
logger.error("中断线程: {}", currentThreadName, e);
}
}
// 判断配置中心服务是否处于离线状态
@ -175,32 +173,48 @@ public class ClientConfigLoader {
};
}
/**
* 初始化客户端信息
*
* @param defaultCacheDir 默认缓存目录如果环境变量中未指定缓存目录则使用此默认值
* @param environment 应用程序环境变量用于从中获取配置信息
*/
private static void initClientInfo(String defaultCacheDir, ConfigurableEnvironment environment) {
// 设置服务器地址
ClientConfigConsts.clientInfo.setServer(
environment.getProperty(
ClientConfigConsts.DEFAULT_CONFIG_NAME_SERVER,
String.class,
ClientConfigConsts.DEFAULT_CONFIG_SERVER));
// 设置端口
ClientConfigConsts.clientInfo.setPort(
environment.getProperty(
ClientConfigConsts.DEFAULT_CONFIG_NAME_PORT,
Integer.class,
ClientConfigConsts.DEFAULT_CONFIG_PORT));
// 设置环境
ClientConfigConsts.clientInfo.setEnv(
environment.getProperty(
ClientConfigConsts.DEFAULT_CONFIG_NAME_ENV,
String.class,
ClientConfigConsts.DEFAULT_CONFIG_ENV));
// 设置数据ID
ClientConfigConsts.clientInfo.setDataId(
environment.getProperty(
ClientConfigConsts.DEFAULT_CONFIG_NAME_DATA_ID,
String.class,
ClientConfigConsts.DEFAULT_CONFIG_DATA_ID));
// 设置缓存目录
ClientConfigConsts.clientInfo.setCacheDir(
environment.getProperty(
ClientConfigConsts.DEFAULT_CONFIG_NAME_CACHE_DIR, String.class, defaultCacheDir));
}
/**
* 获取默认的缓存目录路径 根据操作系统类型返回对应的缓存目录路径
*
* @return 默认的缓存目录路径
*/
private static String getDefaultCacheDir() {
String defaultCacheDir;
String os = System.getProperty("os.name");
@ -209,16 +223,25 @@ public class ClientConfigLoader {
} else if (os.toLowerCase().startsWith(ClientConfigConsts.OS_TYPE_MAC)) {
defaultCacheDir = ClientConfigConsts.DEFAULT_PATH_MAC;
} else {
// 对于未知操作系统默认使用Mac操作系统的缓存路径
defaultCacheDir = ClientConfigConsts.DEFAULT_PATH_MAC;
}
return defaultCacheDir;
}
/**
* 验证缓存目录路径的合法性 确保提供的缓存路径与默认路径格式相符防止路径配置错误
*
* @param defaultCacheDir 默认的缓存目录路径
* @param cacheDir 用户配置的缓存目录路径
*/
private static void validateCacheDirPath(String defaultCacheDir, String cacheDir) {
// 检查是否为Windows系统默认路径格式且用户配置的路径是否符合该格式
if (defaultCacheDir.contains(ClientConfigConsts.DEFAULT_PATH_WIN_SEP)
&& !cacheDir.contains(ClientConfigConsts.DEFAULT_PATH_WIN_SEP)) {
throw new RuntimeException(ClientConfigConsts.DEFAULT_CONFIG_NAME_CACHE_DIR + " 配置的路径不正确");
}
// 检查用户配置的路径是否包含Windows系统路径分隔符且是否正确使用
if (cacheDir.contains(ClientConfigConsts.DEFAULT_PATH_WIN_SEP)
&& !cacheDir.contains(ClientConfigConsts.DEFAULT_PATH_SEP_WIN)) {
throw new RuntimeException(
@ -228,11 +251,20 @@ public class ClientConfigLoader {
}
}
/** 创建缓存文件夹 */
/**
* 创建缓存目录 如果目录不存在将尝试创建它并检查写权限
*
* @param cacheDir 缓存目录的路径
* @throws RuntimeException 如果目录创建失败或没有写权限
*/
private static void createCacheDir(String cacheDir) {
// 获取缓存目录的路径对象
Path path = Paths.get(cacheDir);
// 检查缓存目录是否存在如果不存在则尝试创建
if (!Files.exists(path)) {
// 使用FileUtil工具类创建目录
File mkdir = FileUtil.mkdir(cacheDir);
// 检查创建后的目录是否可写如果不可写则抛出异常
if (!mkdir.canWrite()) {
throw new RuntimeException("缓存文件夹创建失败, 无读写权限");
}

View File

@ -0,0 +1,84 @@
package cn.odboy.config.context;
import cn.hutool.core.util.StrUtil;
import cn.odboy.config.constant.ClientConfigConsts;
import cn.odboy.config.constant.ClientConfigVars;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.stereotype.Component;
/**
* 客户端配置 辅助类
*
* <p>依赖 spring-cloud-context
*
* @author odboy
* @date 2024-12-07
*/
@Component
@RequiredArgsConstructor
public class ClientPropertyHelper {
private final ConfigurableEnvironment environment;
private final ValueAnnotationProcessor valueAnnotationProcessor;
// private final ConfigDataContextRefresher configDataContextRefresher;
private final ConfigPropertyContextRefresher contextRefresher;
/**
* 动态更新配置值
*
* @param propertyName 属性路径名
* @param value 属性值
*/
public void updateValue(String propertyName, Object value) {
if (StrUtil.isNotBlank(propertyName)) {
// 设置属性值
MutablePropertySources propertySources = environment.getPropertySources();
if (propertySources.contains(ClientConfigConsts.PROPERTY_SOURCE_NAME)) {
// 更新属性值
PropertySource<?> propertySource =
propertySources.get(ClientConfigConsts.PROPERTY_SOURCE_NAME);
Map<String, Object> source = ((MapPropertySource) propertySource).getSource();
source.put(propertyName, value);
}
// 单独更新@Value对应的值
valueAnnotationProcessor.setValue(propertyName, value);
// 刷新上下文(解决 @ConfigurationProperties注解的类属性值更新 问题)
// Spring Cloud只会对被@RefreshScope和@ConfigurationProperties标注的bean进行刷新
// 这个方法主要做了两件事刷新配置源也就是PropertySource然后刷新了@ConfigurationProperties注解的类
// configDataContextRefresher.refresh();
contextRefresher.refreshAll();
}
}
/**
* 更新所有配置属性<br>
* 此方法遍历缓存的配置更新应用程序中的相应属性 <br>
* 它主要针对的是那些使用@Value注解注入的配置属性 <br>
* 当缓存的配置发生变化时通过此方法可以确保应用中的配置是最新的
*/
public void updateAll() {
// 获取所有可变属性源
MutablePropertySources propertySources = environment.getPropertySources();
// 检查是否包含特定的属性源
if (propertySources.contains(ClientConfigConsts.PROPERTY_SOURCE_NAME)) {
// 获取属性源
PropertySource<?> propertySource =
propertySources.get(ClientConfigConsts.PROPERTY_SOURCE_NAME);
// 将属性源转换为Map形式以便于更新属性
Map<String, Object> source = ((MapPropertySource) propertySource).getSource();
// 遍历缓存的配置
for (Map.Entry<String, Object> kvMap : ClientConfigVars.cacheConfigs.entrySet()) {
// 更新属性值
source.put(kvMap.getKey(), kvMap.getValue());
// 单独更新@Value对应的值
valueAnnotationProcessor.setValue(kvMap.getKey(), kvMap.getValue());
}
// 刷新所有应用上下文使更新后的配置生效
contextRefresher.refreshAll();
}
}
}

View File

@ -1,72 +0,0 @@
package cn.odboy.config.context;
import cn.hutool.core.util.StrUtil;
import cn.odboy.config.constant.ClientConfigConsts;
import cn.odboy.config.constant.ClientConfigVars;
import lombok.RequiredArgsConstructor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 客户端配置 辅助类
* <p>
* 依赖 spring-cloud-context
*
* @author odboy
* @date 2024-12-07
*/
@Component
@RequiredArgsConstructor
public class ClientPropertyRefresher {
private final ConfigurableEnvironment environment;
private final ValueAnnotationProcessor valueAnnotationProcessor;
// private final ConfigDataContextRefresher configDataContextRefresher;
private final ConfigPropertyContextRefresher contextRefresher;
/**
* 动态更新配置值
*
* @param propertyName 属性路径名
* @param value 属性值
*/
public void updateValue(String propertyName, Object value) {
if (StrUtil.isNotBlank(propertyName)) {
// 设置属性值
MutablePropertySources propertySources = environment.getPropertySources();
if (propertySources.contains(ClientConfigConsts.PROPERTY_SOURCE_NAME)) {
// 更新属性值
PropertySource<?> propertySource = propertySources.get(ClientConfigConsts.PROPERTY_SOURCE_NAME);
Map<String, Object> source = ((MapPropertySource) propertySource).getSource();
source.put(propertyName, value);
}
// 单独更新@Value对应的值
valueAnnotationProcessor.setValue(propertyName, value);
// 刷新上下文(解决 @ConfigurationProperties注解的类属性值更新 问题)
// Spring Cloud只会对被@RefreshScope和@ConfigurationProperties标注的bean进行刷新
// 这个方法主要做了两件事刷新配置源也就是PropertySource然后刷新了@ConfigurationProperties注解的类
// configDataContextRefresher.refresh();
contextRefresher.refreshAll();
}
}
public void updateAll() {
// 设置属性值
MutablePropertySources propertySources = environment.getPropertySources();
if (propertySources.contains(ClientConfigConsts.PROPERTY_SOURCE_NAME)) {
// 更新属性值
PropertySource<?> propertySource = propertySources.get(ClientConfigConsts.PROPERTY_SOURCE_NAME);
Map<String, Object> source = ((MapPropertySource) propertySource).getSource();
for (Map.Entry<String, Object> kvMap : ClientConfigVars.cacheConfigs.entrySet()) {
source.put(kvMap.getKey(), kvMap.getValue());
// 单独更新@Value对应的值
valueAnnotationProcessor.setValue(kvMap.getKey(), kvMap.getValue());
}
contextRefresher.refreshAll();
}
}
}

View File

@ -1,6 +1,6 @@
package cn.odboy.rest;
import cn.odboy.config.context.ClientPropertyRefresher;
import cn.odboy.config.context.ClientPropertyHelper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
@ -21,7 +21,7 @@ public class DemoController {
@Value("${kenaito.config-center.test}")
private String testStr;
private final ConfigCenterProperties configCenterProperties;
private final ClientPropertyRefresher clientPropertyRefresher;
private final ClientPropertyHelper clientPropertyHelper;
/** 配置变化了 */
@GetMapping("/test")
@ -29,7 +29,7 @@ public class DemoController {
String propertyName = "kenaito.config-center.test";
System.err.println("@Value注解的值1=" + testStr);
System.err.println("@ConfigurationProperties注解的值1=" + configCenterProperties.getTest());
clientPropertyRefresher.updateValue(propertyName, "Hello World");
clientPropertyHelper.updateValue(propertyName, "Hello World");
System.err.println("@Value注解的值2=" + testStr);
System.err.println("@ConfigurationProperties注解的值2=" + configCenterProperties.getTest());
return ResponseEntity.ok("success");

View File

@ -65,9 +65,9 @@ public class ConfigApp extends MyNormalEntity {
@Data
public static class QueryClientArgs {
@NotBlank(message = "必填")
private String env;
private String envCode;
@NotBlank(message = "必填")
private String dataId;
private String appName;
}
@Data

View File

@ -6,13 +6,12 @@ import cn.odboy.infra.exception.BadRequestException;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
/**
* 客户端管理
@ -22,110 +21,108 @@ import java.util.stream.Collectors;
*/
@Slf4j
public class ConfigClientManage {
/**
* 所有的客户端连接: {env}_{dataId}_{channelId} to ctx
*/
private static final ConcurrentMap<String, Channel> CLIENT = new ConcurrentHashMap<>();
/** 所有的客户端连接: {env}_{dataId}_{channelId} to ctx */
private static final ConcurrentMap<String, Channel> CLIENT = new ConcurrentHashMap<>();
/**
* 查询客户端节点列表
*
* @param env 环境编码
* @param dataId 应用名称
* @return /
*/
private static List<ConfigApp.ClientInfo> queryClientInfos(String env, String dataId) {
String filterKey = String.format("%s_%s_", env, dataId);
return CLIENT.entrySet().stream()
.filter(f -> f.getKey().startsWith(filterKey))
.map(Map.Entry::getValue)
.map(
m -> {
ConfigApp.ClientInfo clientInfo = new ConfigApp.ClientInfo();
clientInfo.setIp(m.remoteAddress().toString().replaceAll("/", ""));
clientInfo.setIsActive(m.isActive());
return clientInfo;
})
.collect(Collectors.toList());
}
/**
* 查询客户端节点列表
*
* @param envCode 环境编码
* @param appName 应用名称
* @return /
*/
private static List<ConfigApp.ClientInfo> queryClientInfos(String envCode, String appName) {
String filterKey = String.format("%s_%s_", envCode, appName);
return CLIENT.entrySet().stream()
.filter(f -> f.getKey().startsWith(filterKey))
.map(Map.Entry::getValue)
.map(
m -> {
ConfigApp.ClientInfo clientInfo = new ConfigApp.ClientInfo();
clientInfo.setIp(m.remoteAddress().toString().replaceAll("/", ""));
clientInfo.setIsActive(m.isActive());
return clientInfo;
})
.collect(Collectors.toList());
}
/**
* 客户端注册
*
* @param env 环境编码
* @param dataId 应用名称
* @param ctx 信道
*/
public static void register(String env, String dataId, ChannelHandlerContext ctx) {
String envClientKey = String.format("%s_%s_%s", env, dataId, ChannelUtil.getId(ctx));
CLIENT.put(envClientKey, ctx.channel());
log.info("客户端 {} 注册成功", envClientKey);
}
/**
* 客户端注册
*
* @param env 环境编码
* @param dataId 应用名称
* @param ctx 信道
*/
public static void register(String env, String dataId, ChannelHandlerContext ctx) {
String envClientKey = String.format("%s_%s_%s", env, dataId, ChannelUtil.getId(ctx));
CLIENT.put(envClientKey, ctx.channel());
log.info("客户端 {} 注册成功", envClientKey);
}
/**
* 客户端注销
*
* @param channelId /
*/
public static void unregister(ChannelId channelId) {
List<String> envClientKeys =
CLIENT.keySet().stream()
.filter(f -> f.endsWith(ChannelUtil.getId(channelId)))
.collect(Collectors.toList());
for (String envClientKey : envClientKeys) {
Channel channel = CLIENT.getOrDefault(envClientKey, null);
if (channel != null) {
if (channel.isOpen()) {
channel.closeFuture();
}
CLIENT.remove(envClientKey);
log.info("客户端 {} 注销成功", envClientKey);
}
/**
* 客户端注销
*
* @param channelId /
*/
public static void unregister(ChannelId channelId) {
List<String> envClientKeys =
CLIENT.keySet().stream()
.filter(f -> f.endsWith(ChannelUtil.getId(channelId)))
.collect(Collectors.toList());
for (String envClientKey : envClientKeys) {
Channel channel = CLIENT.getOrDefault(envClientKey, null);
if (channel != null) {
if (channel.isOpen()) {
channel.closeFuture();
}
CLIENT.remove(envClientKey);
log.info("客户端 {} 注销成功", envClientKey);
}
}
}
/**
* 根据env和dataId查询所有客户端节点
*
* @param env 环境编码
* @param dataId 应用名称
* @return /
*/
public static List<Channel> queryChannels(String env, String dataId) {
String filterKey = String.format("%s_%s_", env, dataId);
return CLIENT.entrySet().stream()
.filter(f -> f.getKey().startsWith(filterKey))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
}
/**
* 根据env和dataId查询所有客户端节点
*
* @param env 环境编码
* @param dataId 应用名称
* @return /
*/
public static List<Channel> queryChannels(String env, String dataId) {
String filterKey = String.format("%s_%s_", env, dataId);
return CLIENT.entrySet().stream()
.filter(f -> f.getKey().startsWith(filterKey))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
}
/**
* 根据channelId获取env和dataId
*
* @param channelId /
* @return /
*/
public static String[] getEnvDataId(ChannelId channelId) {
String envClientKey =
CLIENT.keySet().stream()
.filter(f -> f.endsWith(ChannelUtil.getId(channelId)))
.findFirst()
.orElse(null);
if (envClientKey == null) {
throw new BadRequestException("获取配置数据ID失败");
}
String[] s = envClientKey.split("_");
// 最大分割块数
int maxSplitLength = 3;
if (s.length != maxSplitLength) {
throw new BadRequestException("获取配置数据ID失败");
}
return s;
/**
* 根据channelId获取env和dataId
*
* @param channelId /
* @return /
*/
public static String[] getEnvDataId(ChannelId channelId) {
String envClientKey =
CLIENT.keySet().stream()
.filter(f -> f.endsWith(ChannelUtil.getId(channelId)))
.findFirst()
.orElse(null);
if (envClientKey == null) {
throw new BadRequestException("获取配置数据ID失败");
}
String[] s = envClientKey.split("_");
// 最大分割块数
int maxSplitLength = 3;
if (s.length != maxSplitLength) {
throw new BadRequestException("获取配置数据ID失败");
}
return s;
}
public static Object queryClientInfos(ConfigApp.QueryClientArgs args) {
String dataId = args.getDataId();
String env = args.getEnv();
return queryClientInfos(env, dataId);
}
public static Object queryClientInfos(ConfigApp.QueryClientArgs args) {
String appName = args.getAppName();
String envCode = args.getEnvCode();
return queryClientInfos(envCode, appName);
}
}

View File

@ -71,11 +71,7 @@ public class ConfigAppEnvServiceImpl extends ServiceImpl<ConfigAppEnvMapper, Con
List<Long> configFileId =
configFiles.stream().map(ConfigFile::getId).collect(Collectors.toList());
configFileService.removeBatchByIds(configFileId);
Long fileId = configFileId.stream().findFirst().orElse(null);
if (fileId != null) {
// delete from config_version
configVersionService.removeBatchByFileId(fileId);
}
configFileId.stream().findFirst().ifPresent(configVersionService::removeBatchByFileId);
}
}
}

View File

@ -1,8 +1,11 @@
package cn.odboy.service.impl;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.stream.StreamUtil;
import cn.hutool.core.util.StrUtil;
import cn.odboy.config.model.msgtype.ConfigFileInfo;
import cn.odboy.config.util.PropertiesUtil;
import cn.odboy.domain.ConfigFile;
import cn.odboy.domain.ConfigVersion;
import cn.odboy.infra.exception.BadRequestException;
@ -14,10 +17,14 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.yaml.snakeyaml.Yaml;
/**
* 配置文件 服务实现类
@ -117,6 +124,14 @@ public class ConfigFileServiceImpl extends ServiceImpl<ConfigFileMapper, ConfigF
if (file == null) {
throw new BadRequestException("file必填");
}
String suffix = FileUtil.getSuffix(file.getOriginalFilename());
// Properties properties = new Properties();
// if ("yml".equals(suffix) || "yaml".equals(suffix)) {
// String content = PropertiesUtil.castToProperties(StrUtil.str(file.getBytes(), StandardCharsets.UTF_8));
// properties.load(IoUtil.toStream(content, StandardCharsets.UTF_8));
// } else {
// properties.load(file.getInputStream());
// }
ConfigFile oldConfigFile = getVersionBy(appId, envCode, file.getOriginalFilename());
ConfigFile newConfigFile = new ConfigFile();
newConfigFile.setAppId(appId);
@ -136,7 +151,7 @@ public class ConfigFileServiceImpl extends ServiceImpl<ConfigFileMapper, ConfigF
ConfigVersion newConfigVersion = new ConfigVersion();
newConfigVersion.setFileId(newConfigFile.getId());
newConfigVersion.setFileContent(StrUtil.str(file.getBytes(), StandardCharsets.UTF_8));
newConfigVersion.setFileType(FileUtil.getSuffix(file.getOriginalFilename()));
newConfigVersion.setFileType(suffix);
newConfigVersion.setVersion(newVersion);
configVersionService.save(newConfigVersion);
}

File diff suppressed because one or more lines are too long