diff --git a/README.md b/README.md index d474b89..51837d9 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,80 @@ -

Kenaito Config

+# 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) 联系我。 \ No newline at end of file diff --git a/doc/d20241210215050.png b/doc/d20241210215050.png new file mode 100644 index 0000000..99143c9 Binary files /dev/null and b/doc/d20241210215050.png differ diff --git a/kenaito-config-common/src/main/java/cn/odboy/config/context/CallBack.java b/kenaito-config-common/src/main/java/cn/odboy/config/context/CallBack.java deleted file mode 100644 index b60ffda..0000000 --- a/kenaito-config-common/src/main/java/cn/odboy/config/context/CallBack.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.odboy.config.context; - -/** - * @date: 2020/6/9 17:02 - * @since: 1.0 - * @see {@link SpringContextHolder} - * 针对某些初始化方法,在SpringContextHolder 初始化前时,
- * 可提交一个 提交回调任务。
- * 在SpringContextHolder 初始化后,进行回调使用 - */ - -public interface CallBack { - /** - * 回调执行方法 - */ - void executor(); - - /** - * 本回调任务名称 - * - * @return / - */ - default String getCallBackName() { - return Thread.currentThread().getId() + ":" + this.getClass().getName(); - } -} - diff --git a/kenaito-config-common/src/main/java/cn/odboy/config/context/SpringContextHolder.java b/kenaito-config-common/src/main/java/cn/odboy/config/context/SpringContextHolder.java deleted file mode 100644 index 1379562..0000000 --- a/kenaito-config-common/src/main/java/cn/odboy/config/context/SpringContextHolder.java +++ /dev/null @@ -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 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 getBean(String name) { - assertContextInjected(); - return (T) applicationContext.getBean(name); - } - - /** - * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型. - */ - public static T getBean(Class requiredType) { - assertContextInjected(); - return applicationContext.getBean(requiredType); - } - - /** - * 获取SpringBoot 配置信息 - * - * @param property 属性key - * @param defaultValue 默认值 - * @param requiredType 返回类型 - * @return / - */ - public static T getProperties(String property, T defaultValue, Class 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 getProperties(String property, Class 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 getAllServiceBeanName() { - return new ArrayList<>(Arrays.asList(applicationContext - .getBeanNamesForAnnotation(Service.class))); - } -} diff --git a/kenaito-config-common/src/main/java/cn/odboy/config/model/SmallMessage.java b/kenaito-config-common/src/main/java/cn/odboy/config/model/SmallMessage.java index c69176f..f31944a 100644 --- a/kenaito-config-common/src/main/java/cn/odboy/config/model/SmallMessage.java +++ b/kenaito-config-common/src/main/java/cn/odboy/config/model/SmallMessage.java @@ -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; + } + } } diff --git a/kenaito-config-common/src/main/java/cn/odboy/config/model/msgtype/ClientInfo.java b/kenaito-config-common/src/main/java/cn/odboy/config/model/msgtype/ClientInfo.java index bd19da2..13df834 100644 --- a/kenaito-config-common/src/main/java/cn/odboy/config/model/msgtype/ClientInfo.java +++ b/kenaito-config-common/src/main/java/cn/odboy/config/model/msgtype/ClientInfo.java @@ -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; + } diff --git a/kenaito-config-common/src/main/java/cn/odboy/config/model/msgtype/ConfigFileInfo.java b/kenaito-config-common/src/main/java/cn/odboy/config/model/msgtype/ConfigFileInfo.java index 7d3efcc..9700f18 100644 --- a/kenaito-config-common/src/main/java/cn/odboy/config/model/msgtype/ConfigFileInfo.java +++ b/kenaito-config-common/src/main/java/cn/odboy/config/model/msgtype/ConfigFileInfo.java @@ -10,6 +10,14 @@ import java.io.Serializable; */ @Data public class ConfigFileInfo implements Serializable { + /** + * 文件名变量,用于存储文件的名称 + */ private String fileName; + + /** + * 文件内容变量,用于存储文件的文本内容 + */ private String fileContent; + } diff --git a/kenaito-config-common/src/main/java/cn/odboy/config/util/ChannelUtil.java b/kenaito-config-common/src/main/java/cn/odboy/config/util/ChannelUtil.java index e0263f2..4f204a8 100644 --- a/kenaito-config-common/src/main/java/cn/odboy/config/util/ChannelUtil.java +++ b/kenaito-config-common/src/main/java/cn/odboy/config/util/ChannelUtil.java @@ -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(); + } } diff --git a/kenaito-config-common/src/main/java/cn/odboy/config/util/MessageUtil.java b/kenaito-config-common/src/main/java/cn/odboy/config/util/MessageUtil.java index 1c8158b..6492d00 100644 --- a/kenaito-config-common/src/main/java/cn/odboy/config/util/MessageUtil.java +++ b/kenaito-config-common/src/main/java/cn/odboy/config/util/MessageUtil.java @@ -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 toConfigFileInfoList(Object o) { if (o instanceof List) { List list = (List) o; diff --git a/kenaito-config-common/src/main/java/cn/odboy/config/util/PropertiesUtil.java b/kenaito-config-common/src/main/java/cn/odboy/config/util/PropertiesUtil.java new file mode 100644 index 0000000..f75bc48 --- /dev/null +++ b/kenaito-config-common/src/main/java/cn/odboy/config/util/PropertiesUtil.java @@ -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
+ * fix: 函数flattenMap中value为null导致的异常中断
+ * + * @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 propertiesMap = new LinkedHashMap<>(); + Map 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 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 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 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 readProperties(String input) { + Map 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 yamlMap, Map 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 properties) { + if (CollUtil.isEmpty(properties)) { + return null; + } + Map map = parseToMap(properties); + return map2Yaml(map).toString(); + } + + /** + * 递归地将 Properties 格式的 Map 解析为 LinkedHashMap + * + * @param propMap Properties 格式的 Map + * @return LinkedHashMap 对象 + */ + private static Map parseToMap(Map propMap) { + Map 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 childMap = getChildMap(propMap, currentKey); + Map 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 getChildMap(Map propMap, String currentKey) { + Map 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 map) { + return map2Yaml(map, 0); + } + + /** + * 将 Map 集合转换为 YAML 格式的字符串 + * + * @param propMap Map 对象 + * @param deep 树的层级 + * @return YAML 格式的字符串 + */ + private static StringBuffer map2Yaml(Map propMap, int deep) { + StringBuffer yamlBuffer = new StringBuffer(); + if (CollectionUtils.isEmpty(propMap)) { + return yamlBuffer; + } + String space = getSpace(deep); + for (Map.Entry 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 list = + ((List) 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)); + } +} diff --git a/kenaito-config-common/src/main/java/cn/odboy/config/util/PropertyNameUtil.java b/kenaito-config-common/src/main/java/cn/odboy/config/util/PropertyNameUtil.java index c1d460e..3bf0607 100644 --- a/kenaito-config-common/src/main/java/cn/odboy/config/util/PropertyNameUtil.java +++ b/kenaito-config-common/src/main/java/cn/odboy/config/util/PropertyNameUtil.java @@ -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; } + } diff --git a/kenaito-config-common/src/main/java/cn/odboy/config/util/ProtostuffUtil.java b/kenaito-config-common/src/main/java/cn/odboy/config/util/ProtostuffUtil.java index 8c28553..8474eab 100644 --- a/kenaito-config-common/src/main/java/cn/odboy/config/util/ProtostuffUtil.java +++ b/kenaito-config-common/src/main/java/cn/odboy/config/util/ProtostuffUtil.java @@ -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, Schema> CACHED_SCHEMA = new ConcurrentHashMap, Schema>(); + private static final Map, Schema> CACHED_SCHEMA = + new ConcurrentHashMap, Schema>(); - private static Schema getSchema(Class clazz) { - @SuppressWarnings("unchecked") - Schema schema = (Schema) 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 Schema getSchema(Class clazz) { + @SuppressWarnings("unchecked") + Schema schema = (Schema) CACHED_SCHEMA.get(clazz); + if (schema == null) { + schema = RuntimeSchema.getSchema(clazz); + if (schema != null) { + CACHED_SCHEMA.put(clazz, schema); + } } + return schema; + } - /** - * 序列化 - */ - public static byte[] serializer(T obj) { - @SuppressWarnings("unchecked") - Class clazz = (Class) obj.getClass(); - LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); - try { - Schema 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 byte[] serializer(T obj) { + @SuppressWarnings("unchecked") + Class clazz = (Class) obj.getClass(); + LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); + try { + Schema schema = getSchema(clazz); + return ProtostuffIOUtil.toByteArray(obj, schema, buffer); + } catch (Exception e) { + throw new IllegalStateException(e.getMessage(), e); + } finally { + buffer.clear(); } + } - /** - * 反序列化 - */ - public static T deserializer(byte[] data, Class clazz) { - try { - T obj = clazz.newInstance(); - Schema 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 deserializer(byte[] data, Class clazz) { + try { + T obj = clazz.newInstance(); + Schema 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); - } -} \ No newline at end of file + public static void main(String[] args) { + byte[] userBytes = ProtostuffUtil.serializer(new ConfigFileInfo()); + ConfigFileInfo user = ProtostuffUtil.deserializer(userBytes, ConfigFileInfo.class); + System.out.println(user); + } +} diff --git a/kenaito-config-core/src/main/java/cn/odboy/config/context/ClientConfigLoader.java b/kenaito-config-core/src/main/java/cn/odboy/config/context/ClientConfigLoader.java index c7a39bc..9489d99 100644 --- a/kenaito-config-core/src/main/java/cn/odboy/config/context/ClientConfigLoader.java +++ b/kenaito-config-core/src/main/java/cn/odboy/config/context/ClientConfigLoader.java @@ -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("缓存文件夹创建失败, 无读写权限"); } diff --git a/kenaito-config-core/src/main/java/cn/odboy/config/context/ClientPropertyHelper.java b/kenaito-config-core/src/main/java/cn/odboy/config/context/ClientPropertyHelper.java new file mode 100644 index 0000000..1d85b8d --- /dev/null +++ b/kenaito-config-core/src/main/java/cn/odboy/config/context/ClientPropertyHelper.java @@ -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; + +/** + * 客户端配置 辅助类 + * + *

依赖 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 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(); + } + } + + /** + * 更新所有配置属性
+ * 此方法遍历缓存的配置,更新应用程序中的相应属性
+ * 它主要针对的是那些使用@Value注解注入的配置属性
+ * 当缓存的配置发生变化时,通过此方法可以确保应用中的配置是最新的 + */ + public void updateAll() { + // 获取所有可变属性源 + MutablePropertySources propertySources = environment.getPropertySources(); + // 检查是否包含特定的属性源 + if (propertySources.contains(ClientConfigConsts.PROPERTY_SOURCE_NAME)) { + // 获取属性源 + PropertySource propertySource = + propertySources.get(ClientConfigConsts.PROPERTY_SOURCE_NAME); + // 将属性源转换为Map形式,以便于更新属性 + Map source = ((MapPropertySource) propertySource).getSource(); + // 遍历缓存的配置 + for (Map.Entry kvMap : ClientConfigVars.cacheConfigs.entrySet()) { + // 更新属性值 + source.put(kvMap.getKey(), kvMap.getValue()); + // 单独更新@Value对应的值 + valueAnnotationProcessor.setValue(kvMap.getKey(), kvMap.getValue()); + } + // 刷新所有应用上下文,使更新后的配置生效 + contextRefresher.refreshAll(); + } + } +} diff --git a/kenaito-config-core/src/main/java/cn/odboy/config/context/ClientPropertyRefresher.java b/kenaito-config-core/src/main/java/cn/odboy/config/context/ClientPropertyRefresher.java deleted file mode 100644 index 34664a8..0000000 --- a/kenaito-config-core/src/main/java/cn/odboy/config/context/ClientPropertyRefresher.java +++ /dev/null @@ -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; - -/** - * 客户端配置 辅助类 - *

- * 依赖 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 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 source = ((MapPropertySource) propertySource).getSource(); - for (Map.Entry kvMap : ClientConfigVars.cacheConfigs.entrySet()) { - source.put(kvMap.getKey(), kvMap.getValue()); - // 单独更新@Value对应的值 - valueAnnotationProcessor.setValue(kvMap.getKey(), kvMap.getValue()); - } - contextRefresher.refreshAll(); - } - } -} diff --git a/kenaito-config-demo/src/main/java/cn/odboy/rest/DemoController.java b/kenaito-config-demo/src/main/java/cn/odboy/rest/DemoController.java index 0bcc453..430d2d8 100644 --- a/kenaito-config-demo/src/main/java/cn/odboy/rest/DemoController.java +++ b/kenaito-config-demo/src/main/java/cn/odboy/rest/DemoController.java @@ -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"); diff --git a/kenaito-config-service/src/main/java/cn/odboy/domain/ConfigApp.java b/kenaito-config-service/src/main/java/cn/odboy/domain/ConfigApp.java index 5f7b002..dcf346a 100644 --- a/kenaito-config-service/src/main/java/cn/odboy/domain/ConfigApp.java +++ b/kenaito-config-service/src/main/java/cn/odboy/domain/ConfigApp.java @@ -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 diff --git a/kenaito-config-service/src/main/java/cn/odboy/infra/netty/ConfigClientManage.java b/kenaito-config-service/src/main/java/cn/odboy/infra/netty/ConfigClientManage.java index 381803f..d2c286d 100644 --- a/kenaito-config-service/src/main/java/cn/odboy/infra/netty/ConfigClientManage.java +++ b/kenaito-config-service/src/main/java/cn/odboy/infra/netty/ConfigClientManage.java @@ -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 CLIENT = new ConcurrentHashMap<>(); + /** 所有的客户端连接: {env}_{dataId}_{channelId} to ctx */ + private static final ConcurrentMap CLIENT = new ConcurrentHashMap<>(); - /** - * 查询客户端节点列表 - * - * @param env 环境编码 - * @param dataId 应用名称 - * @return / - */ - private static List 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 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 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 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 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 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); + } } diff --git a/kenaito-config-service/src/main/java/cn/odboy/service/impl/ConfigAppEnvServiceImpl.java b/kenaito-config-service/src/main/java/cn/odboy/service/impl/ConfigAppEnvServiceImpl.java index 2a196dc..c9391fc 100644 --- a/kenaito-config-service/src/main/java/cn/odboy/service/impl/ConfigAppEnvServiceImpl.java +++ b/kenaito-config-service/src/main/java/cn/odboy/service/impl/ConfigAppEnvServiceImpl.java @@ -71,11 +71,7 @@ public class ConfigAppEnvServiceImpl extends ServiceImpl 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); } } } diff --git a/kenaito-config-service/src/main/java/cn/odboy/service/impl/ConfigFileServiceImpl.java b/kenaito-config-service/src/main/java/cn/odboy/service/impl/ConfigFileServiceImpl.java index ebcfff1..89ad619 100644 --- a/kenaito-config-service/src/main/java/cn/odboy/service/impl/ConfigFileServiceImpl.java +++ b/kenaito-config-service/src/main/java/cn/odboy/service/impl/ConfigFileServiceImpl.java @@ -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