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

View File

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

View File

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

View File

@ -1,18 +1,20 @@
package cn.odboy.config.context; package cn.odboy.config.context;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import java.util.Map;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.cloud.context.refresh.ConfigDataContextRefresher; //import org.springframework.cloud.context.refresh.ConfigDataContextRefresher;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.Map;
/** /**
* 客户端配置 辅助类 * 客户端配置 辅助类
* <p>
* 依赖 spring-cloud-context
* *
* @author odboy * @author odboy
* @date 2024-12-07 * @date 2024-12-07
@ -22,7 +24,8 @@ import org.springframework.stereotype.Component;
public class ClientPropertyHelper { public class ClientPropertyHelper {
private final ConfigurableEnvironment environment; private final ConfigurableEnvironment environment;
private final ValueAnnotationProcessor valueAnnotationProcessor; 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(); MutablePropertySources propertySources = environment.getPropertySources();
if (propertySources.contains(ClientConfigLoader.PROPERTY_SOURCE_NAME)) { if (propertySources.contains(ClientConfigLoader.PROPERTY_SOURCE_NAME)) {
// 更新属性值 // 更新属性值
PropertySource<?> propertySource = PropertySource<?> propertySource = propertySources.get(ClientConfigLoader.PROPERTY_SOURCE_NAME);
propertySources.get(ClientConfigLoader.PROPERTY_SOURCE_NAME);
Map<String, Object> source = ((MapPropertySource) propertySource).getSource(); Map<String, Object> source = ((MapPropertySource) propertySource).getSource();
source.put(propertyName, value); source.put(propertyName, value);
} }
@ -46,7 +48,8 @@ public class ClientPropertyHelper {
// 刷新上下文(解决 @ConfigurationProperties注解的类属性值更新 问题) // 刷新上下文(解决 @ConfigurationProperties注解的类属性值更新 问题)
// Spring Cloud只会对被@RefreshScope和@ConfigurationProperties标注的bean进行刷新 // Spring Cloud只会对被@RefreshScope和@ConfigurationProperties标注的bean进行刷新
// 这个方法主要做了两件事刷新配置源也就是PropertySource然后刷新了@ConfigurationProperties注解的类 // 这个方法主要做了两件事刷新配置源也就是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; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException; 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.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/** /**
* 加载并处理@Value对应的引用 * 加载并处理@Value对应的引用
* *

View File

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