feat:自定义 @ConfigurationProperties注解的类属性值更新处理方式
This commit is contained in:
parent
b1d810de95
commit
49860000fc
|
@ -29,6 +29,9 @@
|
|||
- 20241206 读取本地配置缓存(连接服务端失败后的兜底操作)
|
||||
- 20241207 动态更新@Value注解的属性
|
||||
- 20241207 动态更新@ConfigurationProperties注解类中的属性
|
||||
- 感想:太艰难了
|
||||
- 感谢:spring-cloud-context 给我的灵感
|
||||
- 耗时:4小时
|
||||
- 202412xx 动态替换配置指令实现 [loading]
|
||||
|
||||
#### 服务端
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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对应的引用
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* 配置中心客户端 业务处理
|
||||
|
|
Loading…
Reference in New Issue