Signed-off-by: odboy <tianjun@odboy.cn>
This commit is contained in:
parent
70c0ae9896
commit
88cd1c51c2
114
README.md
114
README.md
|
@ -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) 联系我。
|
Binary file not shown.
After Width: | Height: | Size: 163 KiB |
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,14 @@ import java.io.Serializable;
|
|||
*/
|
||||
@Data
|
||||
public class ConfigFileInfo implements Serializable {
|
||||
/**
|
||||
* 文件名变量,用于存储文件的名称
|
||||
*/
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件内容变量,用于存储文件的文本内容
|
||||
*/
|
||||
private String fileContent;
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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("缓存文件夹创建失败, 无读写权限");
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue