feat:自定义 @ConfigurationProperties注解的类属性值更新处理方式

This commit is contained in:
骑着蜗牛追导弹 2024-12-07 15:34:12 +08:00
parent b1d810de95
commit 49860000fc
9 changed files with 713 additions and 503 deletions

View File

@ -29,6 +29,9 @@
- 20241206 读取本地配置缓存(连接服务端失败后的兜底操作)
- 20241207 动态更新@Value注解的属性
- 20241207 动态更新@ConfigurationProperties注解类中的属性
- 感想:太艰难了
- 感谢spring-cloud-context 给我的灵感
- 耗时4小时
- 202412xx 动态替换配置指令实现 [loading]
#### 服务端

View File

@ -19,10 +19,10 @@
<artifactId>kenaito-config-common</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<version>4.2.0</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-context</artifactId>-->
<!-- <version>4.2.0</version>-->
<!-- </dependency>-->
</dependencies>
</project>

View File

@ -5,15 +5,6 @@ import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import cn.odboy.config.model.msgtype.ClientInfo;
import cn.odboy.config.netty.ConfigClient;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
@ -25,6 +16,16 @@ import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* 配置加载器
*
@ -35,7 +36,9 @@ import org.yaml.snakeyaml.Yaml;
public class ClientConfigLoader {
private static final Logger logger = LoggerFactory.getLogger(ClientConfigLoader.class);
/** 默认的配置值 */
/**
* 默认的配置值
*/
private static final String OS_TYPE_WIN = "win";
private static final String OS_TYPE_MAC = "mac";
@ -47,49 +50,79 @@ public class ClientConfigLoader {
private static final String DEFAULT_CONFIG_DATA_ID = "default";
private static final String DEFAULT_PATH_WIN_SEP = ":";
/** 默认配置项配置中心服务ip */
/**
* 默认配置项配置中心服务ip
*/
private static final String DEFAULT_CONFIG_NAME_SERVER = "kenaito.config-center.server";
/** 默认配置项:配置中心服务端口 */
/**
* 默认配置项配置中心服务端口
*/
private static final String DEFAULT_CONFIG_NAME_PORT = "kenaito.config-center.port";
/** 默认配置项:将拉取的配置环境 */
/**
* 默认配置项将拉取的配置环境
*/
private static final String DEFAULT_CONFIG_NAME_ENV = "kenaito.config-center.env";
/** 默认配置项:将拉取配置的应用的名称 */
/**
* 默认配置项将拉取配置的应用的名称
*/
private static final String DEFAULT_CONFIG_NAME_DATA_ID = "kenaito.config-center.data-id";
/** 默认配置项:配置缓存目录 */
/**
* 默认配置项配置缓存目录
*/
private static final String DEFAULT_CONFIG_NAME_CACHE_DIR = "kenaito.config-center.cache-dir";
/** Win路径分割符 */
/**
* Win路径分割符
*/
private static final String DEFAULT_PATH_SEP_WIN = "\\";
/** Mac路径分割符 */
/**
* Mac路径分割符
*/
private static final String DEFAULT_PATH_SEP_MAC = "/";
/** 配置源名称 */
/**
* 配置源名称
*/
public static final String PROPERTY_SOURCE_NAME = "kenaito-dynamic-config";
/** 当前客户端配置 */
/**
* 当前客户端配置
*/
public static final ClientInfo clientInfo = new ClientInfo();
/** 配置是否加载完毕 */
/**
* 配置是否加载完毕
*/
public static boolean isConfigLoaded = false;
/** 服务器是否离线 */
/**
* 服务器是否离线
*/
public static boolean isServerOffline = false;
/** 原有的配置信息filename -> file content */
/**
* 原有的配置信息filename -> file content
*/
public static Map<String, String> originConfigs = new HashMap<>();
/** 转换后的配置信息filename -> {configKey: configValue} */
/**
* 转换后的配置信息filename -> {configKey: configValue}
*/
public static Map<String, Map<String, Object>> lastConfigs = new HashMap<>();
/** 所有自定义配置项缓存 */
/**
* 所有自定义配置项缓存
*/
public static Map<String, Object> cacheConfigs = new HashMap<>();
/** 定时将配置写盘,缓存配置信息 */
/**
* 定时将配置写盘缓存配置信息
*/
private final Thread fixedTimeFlushConfigFileThread =
ThreadUtil.newThread(
() -> {
@ -257,7 +290,9 @@ public class ClientConfigLoader {
}
}
/** 创建缓存文件夹 */
/**
* 创建缓存文件夹
*/
private static void createCacheDir(String cacheDir) {
Path path = Paths.get(cacheDir);
if (!Files.exists(path)) {

View File

@ -1,18 +1,20 @@
package cn.odboy.config.context;
import cn.hutool.core.util.StrUtil;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.context.refresh.ConfigDataContextRefresher;
import org.springframework.cloud.context.refresh.ContextRefresher;
//import org.springframework.cloud.context.refresh.ConfigDataContextRefresher;
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
@ -22,7 +24,8 @@ import org.springframework.stereotype.Component;
public class ClientPropertyHelper {
private final ConfigurableEnvironment environment;
private final ValueAnnotationProcessor valueAnnotationProcessor;
private final ConfigDataContextRefresher configDataContextRefresher;
// private final ConfigDataContextRefresher configDataContextRefresher;
private final ConfigPropertyContextRefresher contextRefresher;
/**
* 动态更新配置值
@ -36,8 +39,7 @@ public class ClientPropertyHelper {
MutablePropertySources propertySources = environment.getPropertySources();
if (propertySources.contains(ClientConfigLoader.PROPERTY_SOURCE_NAME)) {
// 更新属性值
PropertySource<?> propertySource =
propertySources.get(ClientConfigLoader.PROPERTY_SOURCE_NAME);
PropertySource<?> propertySource = propertySources.get(ClientConfigLoader.PROPERTY_SOURCE_NAME);
Map<String, Object> source = ((MapPropertySource) propertySource).getSource();
source.put(propertyName, value);
}
@ -46,7 +48,8 @@ public class ClientPropertyHelper {
// 刷新上下文(解决 @ConfigurationProperties注解的类属性值更新 问题)
// Spring Cloud只会对被@RefreshScope和@ConfigurationProperties标注的bean进行刷新
// 这个方法主要做了两件事刷新配置源也就是PropertySource然后刷新了@ConfigurationProperties注解的类
configDataContextRefresher.refresh();
// configDataContextRefresher.refresh();
contextRefresher.refreshAll();
}
}
}

View File

@ -0,0 +1,74 @@
package cn.odboy.config.context;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
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.HashMap;
import java.util.Map;
/**
* 配置属性上下文 刷新
*
* @author odboy
* @date 2024-12-07
*/
@Component
@RequiredArgsConstructor
public class ConfigPropertyContextRefresher {
private final ConfigurationPropertiesAnnotationProcessor processor;
private final ConfigurableEnvironment environment;
/**
* 刷新单个属性
*
* @param propertyName 属性名表达式
* @param value 属性值
*/
public void refreshSingle(String propertyName, Object value) {
MutablePropertySources propertySources = environment.getPropertySources();
if (propertySources.contains(ClientConfigLoader.PROPERTY_SOURCE_NAME)) {
// 更新属性值
PropertySource<?> propertySource = propertySources.get(ClientConfigLoader.PROPERTY_SOURCE_NAME);
Map<String, Object> source = ((MapPropertySource) propertySource).getSource();
source.put(propertyName, value);
} else {
// 新增属性值
Map<String, Object> propertyMap = new HashMap<>(1);
MapPropertySource propertySource = new MapPropertySource(ClientConfigLoader.PROPERTY_SOURCE_NAME, propertyMap);
propertySources.addFirst(propertySource);
}
// 使用 Binder 重新绑定 @ConfigurationProperties
Binder binder = Binder.get(environment);
for (Map.Entry<String, Object> propertyPrefixBean : processor.getPrefixBeanMap().entrySet()) {
binder.bind(propertyPrefixBean.getKey(), Bindable.ofInstance(propertyPrefixBean.getValue()));
}
}
/**
* 刷新所有属性
*/
public void refreshAll() {
MutablePropertySources propertySources = environment.getPropertySources();
if (propertySources.contains(ClientConfigLoader.PROPERTY_SOURCE_NAME)) {
// 替换属性值
MapPropertySource propertySource = new MapPropertySource(ClientConfigLoader.PROPERTY_SOURCE_NAME, ClientConfigLoader.cacheConfigs);
propertySources.replace(ClientConfigLoader.PROPERTY_SOURCE_NAME, propertySource);
} else {
// 新增属性值
Map<String, Object> propertyMap = new HashMap<>(1);
MapPropertySource propertySource = new MapPropertySource(ClientConfigLoader.PROPERTY_SOURCE_NAME, propertyMap);
propertySources.addFirst(propertySource);
}
// 使用 Binder 重新绑定 @ConfigurationProperties
Binder binder = Binder.get(environment);
for (Map.Entry<String, Object> propertyPrefixBean : processor.getPrefixBeanMap().entrySet()) {
binder.bind(propertyPrefixBean.getKey(), Bindable.ofInstance(propertyPrefixBean.getValue()));
}
}
}

View File

@ -0,0 +1,65 @@
package cn.odboy.config.context;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 加载并处理@ConfigurationProperties对应的引用
*
* @author odboy
* @date 2024-12-07
*/
@Component
public class ConfigurationPropertiesAnnotationProcessor implements BeanPostProcessor {
private final Logger logger =
LoggerFactory.getLogger(ConfigurationPropertiesAnnotationProcessor.class);
@Getter
private final Map<String, Object> prefixBeanMap = new HashMap<>();
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> clazz = bean.getClass();
while (clazz != null) {
if (clazz.isAnnotationPresent(ConfigurationProperties.class)) {
// 排除springboot框架的配置
if (beanName.contains("springframework")) {
clazz = clazz.getSuperclass();
continue;
}
// 排除数据源框架的配置
if (beanName.contains("dataSource") || beanName.contains("druid")) {
clazz = clazz.getSuperclass();
continue;
}
// 排除ORM框架的配置
if (beanName.contains("mybatis")) {
clazz = clazz.getSuperclass();
continue;
}
// 排除ip2region的配置
if (beanName.contains("ip2region")) {
clazz = clazz.getSuperclass();
continue;
}
logger.info("扫描到自定义的@ConfigurationProperties注解类: {}", beanName);
this.processConfigBean(clazz, bean);
}
clazz = clazz.getSuperclass();
}
return bean;
}
private void processConfigBean(Class<?> clazz, Object bean) {
ConfigurationProperties annotation = clazz.getAnnotation(ConfigurationProperties.class);
// 比如: kenaito.config-center
prefixBeanMap.put(annotation.prefix(), bean);
}
}

View File

@ -1,8 +1,5 @@
package cn.odboy.config.context;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
@ -10,6 +7,10 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* 加载并处理@Value对应的引用
*

View File

@ -6,11 +6,12 @@ import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.util.concurrent.TimeUnit;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
/**
* 配置中心客户端
*
@ -21,37 +22,60 @@ import org.slf4j.LoggerFactory;
public class ConfigClient {
private static final Logger logger = LoggerFactory.getLogger(ConfigClient.class);
/** 客户端 */
/**
* 客户端
*/
private EventLoopGroup eventLoopGroup;
/** 存放客户端bootstrap对象 */
/**
* 存放客户端bootstrap对象
*/
private Bootstrap bootstrap;
/** 存放客户端channel对象 */
/**
* 存放客户端channel对象
*/
private Channel channel;
/** 重连间隔,单位秒 */
/**
* 重连间隔单位秒
*/
private Integer delaySeconds = 5;
/** 连接属性服务ip */
/**
* 连接属性服务ip
*/
private String serverIp;
/** 连接属性:服务端口 */
/**
* 连接属性服务端口
*/
private Integer serverPort;
/** 私有静态实例 */
/**
* 私有静态实例
*/
private static volatile ConfigClient instance;
/** 最大重试次数 */
/**
* 最大重试次数
*/
private static final int MAX_RETRY_COUNT = 2;
/** 当前重试次数 */
/**
* 当前重试次数
*/
private static int retryCount = 0;
/** 私有化构造函数 */
private ConfigClient() {}
/**
* 私有化构造函数
*/
private ConfigClient() {
}
/** 获取 */
/**
* 获取
*/
public static ConfigClient getInstance() {
if (instance == null) {
synchronized (ConfigClient.class) {
@ -99,7 +123,9 @@ public class ConfigClient {
}
private class ConfigClientChannelListener implements ChannelFutureListener {
/** 该方法会在channelActive之前执行去判断客户端连接是否成功并做失败重连的操作 */
/**
* 该方法会在channelActive之前执行去判断客户端连接是否成功并做失败重连的操作
*/
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
// 连接成功后保存Channel
@ -130,7 +156,9 @@ public class ConfigClient {
}
}
/** 重新连接 */
/**
* 重新连接
*/
protected void reConnect() {
try {
logger.info("重连配置中心服务 {}:{}", this.serverIp, this.serverPort);

View File

@ -9,14 +9,15 @@ import cn.odboy.config.model.msgtype.ConfigFileInfo;
import cn.odboy.config.util.MessageUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
/**
* 配置中心客户端 业务处理