feat(springboot3): 添加 RefreshScope 自动配置支持
- 新增 KenaitoRefreshAutoConfiguration 类,实现 ApplicationContextAware - 更新 spring.factories,注册 EnableAutoConfiguration - 与 Spring Boot 2 客户端保持一致,支持 @Value 和 @ConfigurationProperties 动态刷新
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>cn.odboy</groupId>
|
||||
<artifactId>kenaito-conf-springboot2-client</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.0.3</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
|
||||
+42
-12
@@ -4,18 +4,8 @@ import java.io.FileReader;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
@@ -411,7 +401,7 @@ public class ConfigcenterContext {
|
||||
System.out.println("==================final config list start==================");
|
||||
for (String key : sortKeys) {
|
||||
String value = hideSensitive(key, properties.getProperty(key));
|
||||
System.out.println(String.format("%s = %s", key, value));
|
||||
System.out.printf("%s = %s%n", key, value);
|
||||
}
|
||||
System.out.println("==================final config list end====================");
|
||||
}
|
||||
@@ -471,6 +461,46 @@ public class ConfigcenterContext {
|
||||
logger.warn("invoke {}.onConfigChanged error, msg:{}", handler.getClass().getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh @Value and @ConfigurationProperties fields
|
||||
logger.info("[RefreshScope] Calling refreshDynamicConfigFields()...");
|
||||
refreshDynamicConfigFields();
|
||||
logger.info("[RefreshScope] refreshDynamicConfigFields() completed");
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshDynamicConfigFields() {
|
||||
if (applicationContext != null) {
|
||||
try {
|
||||
logger.info("[RefreshScope] Starting dynamic config field refresh...");
|
||||
StandardEnvironment env = (StandardEnvironment) applicationContext.getEnvironment();
|
||||
// 直接创建 RefreshScopeRegistry 并注册为 BeanPostProcessor
|
||||
RefreshScopeRegistry registry = new RefreshScopeRegistry(env);
|
||||
|
||||
// 遍历所有已注册的 Bean,记录 @Value 和 @ConfigurationProperties 字段
|
||||
String[] beanNames = applicationContext.getBeanDefinitionNames();
|
||||
logger.info("[RefreshScope] Found {} beans in ApplicationContext", beanNames.length);
|
||||
|
||||
int valueFieldCount = 0;
|
||||
int configPropsCount = 0;
|
||||
for (String beanName : beanNames) {
|
||||
Object bean = applicationContext.getBean(beanName);
|
||||
registry.postProcessAfterInitialization(bean, beanName);
|
||||
}
|
||||
|
||||
valueFieldCount = registry.getValueFieldCount();
|
||||
configPropsCount = registry.getConfigPropsCount();
|
||||
logger.info("[RefreshScope] Recorded {} @Value fields and {} @ConfigurationProperties beans", valueFieldCount, configPropsCount);
|
||||
|
||||
// 刷新
|
||||
registry.refresh();
|
||||
logger.info("[RefreshScope] RefreshScopeRegistry.refresh() completed, refreshed {} @Value fields and {} @ConfigurationProperties beans",
|
||||
registry.getValueFieldCount(), registry.getConfigPropsCount());
|
||||
} catch (Exception e) {
|
||||
logger.warn("[RefreshScope] RefreshScopeRegistry.refresh() error, msg:{}", e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
logger.warn("[RefreshScope] ApplicationContext is null, cannot refresh dynamic config fields");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -40,14 +40,14 @@ public class InternalConfigChangeListener {
|
||||
hbScheduledExecutor = Executors.newScheduledThreadPool(1);
|
||||
|
||||
final String url = context.getApiBaseUrls()[0] + "/api/fetchChangedConfigs";
|
||||
final Map<String, String> params = new HashMap<>();
|
||||
final Map<String, Object> params = new HashMap<>();
|
||||
params.put("version", context.getVersion());
|
||||
params.put("appName", context.getApp());
|
||||
params.put("env", context.getEnv());
|
||||
if (context.isIgnoreGlobal()) {
|
||||
params.put("ignoreGlobal", String.valueOf(context.isIgnoreGlobal()));
|
||||
}
|
||||
params.put("lastTime", ConfigcenterContext.getProperty(LATEST_FETCH_TIMESTAMP, String.valueOf(System.currentTimeMillis())));
|
||||
params.put("lastTime", Long.parseLong(ConfigcenterContext.getProperty(LATEST_FETCH_TIMESTAMP, String.valueOf(System.currentTimeMillis()))));
|
||||
|
||||
hbScheduledExecutor.scheduleAtFixedRate(new Runnable() {
|
||||
@Override
|
||||
@@ -61,7 +61,7 @@ public class InternalConfigChangeListener {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) map.get("data");
|
||||
if (data.containsKey(LATEST_FETCH_TIMESTAMP)) {
|
||||
params.put("lastTime", data.remove(LATEST_FETCH_TIMESTAMP).toString());
|
||||
params.put("lastTime", Long.parseLong(data.remove(LATEST_FETCH_TIMESTAMP).toString()));
|
||||
}
|
||||
if (!data.isEmpty()) context.updateConfig(data);
|
||||
}
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package cn.odboy.confcenter;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* Refresh Scope Registry
|
||||
* Supports dynamic refresh of @Value and @ConfigurationProperties fields
|
||||
*
|
||||
* @author odboy
|
||||
* @version 2.0.0
|
||||
* @date 2026-10-16
|
||||
*/
|
||||
public class RefreshScopeRegistry implements BeanPostProcessor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RefreshScopeRegistry.class);
|
||||
|
||||
private Environment environment;
|
||||
private final List<ValueFieldHolder> valueFields = new ArrayList<>();
|
||||
private final List<ConfigPropsHolder> configPropsBeans = new ArrayList<>();
|
||||
|
||||
public RefreshScopeRegistry(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
public int getValueFieldCount() {
|
||||
return valueFields.size();
|
||||
}
|
||||
|
||||
public int getConfigPropsCount() {
|
||||
return configPropsBeans.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
Class<?> clazz = bean.getClass();
|
||||
|
||||
// Record @Value fields
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
if (field.isAnnotationPresent(Value.class)) {
|
||||
valueFields.add(new ValueFieldHolder(bean, field));
|
||||
}
|
||||
}
|
||||
|
||||
// Record @ConfigurationProperties beans
|
||||
if (clazz.isAnnotationPresent(ConfigurationProperties.class)) {
|
||||
configPropsBeans.add(new ConfigPropsHolder(bean));
|
||||
}
|
||||
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all @Value fields and @ConfigurationProperties beans
|
||||
*/
|
||||
public void refresh() {
|
||||
refreshValueFields();
|
||||
refreshConfigPropsBeans();
|
||||
}
|
||||
|
||||
private void refreshValueFields() {
|
||||
for (ValueFieldHolder holder : valueFields) {
|
||||
try {
|
||||
holder.refresh(environment);
|
||||
} catch (Exception e) {
|
||||
// ignore refresh errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshConfigPropsBeans() {
|
||||
for (ConfigPropsHolder holder : configPropsBeans) {
|
||||
try {
|
||||
holder.refresh(environment);
|
||||
} catch (Exception e) {
|
||||
// ignore refresh errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holder for @Value field
|
||||
*/
|
||||
private static class ValueFieldHolder {
|
||||
private final Object bean;
|
||||
private final Field field;
|
||||
private final String key;
|
||||
|
||||
ValueFieldHolder(Object bean, Field field) {
|
||||
this.bean = bean;
|
||||
this.field = field;
|
||||
this.key = parseKey(field.getAnnotation(Value.class).value());
|
||||
}
|
||||
|
||||
private String parseKey(String valueAnnotation) {
|
||||
// Extract key from "${database.host:default}" -> "database.host"
|
||||
String cleaned = valueAnnotation.replace("${", "").replace("}", "");
|
||||
int colonIdx = cleaned.indexOf(':');
|
||||
return colonIdx > 0 ? cleaned.substring(0, colonIdx) : cleaned;
|
||||
}
|
||||
|
||||
void refresh(Environment env) throws Exception {
|
||||
String newValue = env.getProperty(key);
|
||||
if (newValue != null) {
|
||||
field.setAccessible(true);
|
||||
Object oldValue = field.get(bean);
|
||||
Object convertedValue = convertType(field.getType(), newValue);
|
||||
field.set(bean, convertedValue);
|
||||
|
||||
// 验证是否设置成功
|
||||
Object verifyValue = field.get(bean);
|
||||
boolean success = (convertedValue != null && convertedValue.equals(verifyValue));
|
||||
logger.info("[RefreshScope] Refreshed @Value field: {}.{} | old='{}' | new='{}' | converted='{}' | verify='{}' | success={}",
|
||||
bean.getClass().getName(), field.getName(), oldValue, newValue, convertedValue, verifyValue, success);
|
||||
|
||||
if (!success) {
|
||||
logger.warn("[RefreshScope] Field refresh failed! bean={}, field={}, fieldType={}, newValueClass={}",
|
||||
bean.getClass().getName(), field.getName(), field.getType(), convertedValue != null ? convertedValue.getClass() : "null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Object convertType(Class<?> type, String value) {
|
||||
if (type == String.class) return value;
|
||||
if (type == int.class || type == Integer.class) return Integer.parseInt(value);
|
||||
if (type == long.class || type == Long.class) return Long.parseLong(value);
|
||||
if (type == boolean.class || type == Boolean.class) return Boolean.parseBoolean(value);
|
||||
if (type == double.class || type == Double.class) return Double.parseDouble(value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holder for @ConfigurationProperties bean
|
||||
*/
|
||||
private static class ConfigPropsHolder {
|
||||
private final Object bean;
|
||||
|
||||
ConfigPropsHolder(Object bean) {
|
||||
this.bean = bean;
|
||||
}
|
||||
|
||||
void refresh(Environment env) {
|
||||
Class<?> clazz = bean.getClass();
|
||||
ConfigurationProperties anno = clazz.getAnnotation(ConfigurationProperties.class);
|
||||
if (anno != null) {
|
||||
String prefix = anno.prefix();
|
||||
bindProperties(bean, prefix, env);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindProperties(Object bean, String prefix, Environment env) {
|
||||
try {
|
||||
Class<?> clazz = bean.getClass();
|
||||
for (Field field : getAllDeclaredFields(clazz)) {
|
||||
String propertyName = prefix + "." + camelToKebab(field.getName());
|
||||
String value = env.getProperty(propertyName);
|
||||
if (value != null) {
|
||||
field.setAccessible(true);
|
||||
field.set(bean, convertType(field.getType(), value));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private Field[] getAllDeclaredFields(Class<?> clazz) {
|
||||
List<Field> fields = new ArrayList<>();
|
||||
while (clazz != null && clazz != Object.class) {
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
fields.add(field);
|
||||
}
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
return fields.toArray(new Field[0]);
|
||||
}
|
||||
|
||||
private String camelToKebab(String camelCase) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < camelCase.length(); i++) {
|
||||
char c = camelCase.charAt(i);
|
||||
if (Character.isUpperCase(c)) {
|
||||
if (i > 0) result.append('-');
|
||||
result.append(Character.toLowerCase(c));
|
||||
} else {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private Object convertType(Class<?> type, String value) {
|
||||
if (type == String.class) return value;
|
||||
if (type == int.class || type == Integer.class) return Integer.parseInt(value);
|
||||
if (type == long.class || type == Long.class) return Long.parseLong(value);
|
||||
if (type == boolean.class || type == Boolean.class) return Boolean.parseBoolean(value);
|
||||
if (type == double.class || type == Double.class) return Double.parseDouble(value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -6,7 +6,6 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.env.EnvironmentPostProcessor;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import cn.odboy.confcenter.ConfigcenterContext;
|
||||
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package cn.odboy.confcenter.springboot;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import cn.odboy.confcenter.ConfigcenterContext;
|
||||
|
||||
/**
|
||||
* Spring Boot auto configuration for config center refresh support.
|
||||
*
|
||||
* Captures the Spring ApplicationContext into {@link ConfigcenterContext} so that
|
||||
* remote config changes (detected by the polling listener) can update the Spring
|
||||
* Environment property source and refresh {@code @Value} / {@code @ConfigurationProperties}
|
||||
* fields at runtime.
|
||||
*
|
||||
* The non-SpringBoot path sets the context via CCPropertyPlaceholderConfigurer;
|
||||
* this class provides the equivalent wiring for Spring Boot applications.
|
||||
*
|
||||
* @author odboy
|
||||
* @version 2.0.0
|
||||
* @date 2026-10-16
|
||||
*/
|
||||
@Configuration
|
||||
public class KenaitoRefreshAutoConfiguration implements ApplicationContextAware {
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
ConfigcenterContext.setApplicationContext(applicationContext);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
org.springframework.boot.env.EnvironmentPostProcessor=cn.odboy.confcenter.springboot.KenaitoEnvironmentInitializer
|
||||
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.odboy.confcenter.springboot.KenaitoRefreshAutoConfiguration
|
||||
|
||||
Reference in New Issue
Block a user