feat(springboot3): 添加 RefreshScope 自动配置支持

- 新增 KenaitoRefreshAutoConfiguration 类,实现 ApplicationContextAware
- 更新 spring.factories,注册 EnableAutoConfiguration
- 与 Spring Boot 2 客户端保持一致,支持 @Value 和 @ConfigurationProperties 动态刷新
This commit is contained in:
2026-03-23 21:04:15 +08:00
parent 82707de831
commit c2e599097e
38 changed files with 386 additions and 1232 deletions
+1 -1
View File
@@ -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>
@@ -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");
}
}
@@ -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);
}
@@ -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;
}
}
}
@@ -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;
@@ -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